Android自定义View-柱状图(java)

先来看看效果图

分析一波

固定的属性:

XY坐标轴位置、坐标轴末尾标题位置

动态变化的属性:

坐标轴值、虚线的位置对应Y轴值、半圆柱高度

看看整体的属性

ini 复制代码
private int mWidth, mHeight;
// -------自定义可修改属性,配置attrs------------
private int mStrokeWidth = 2; // 坐标轴线宽
private int mDashStrokeWidth = 2; // 虚线线宽
private int mAxisTxtSize = 39;//坐标轴字体大小
private int mHistogramWidth = 30;// 柱状的宽度
private int mAxisXLastSpace = 100;// X轴最后一个刻度离末尾的空白距离
private int mAxisYLastSpace = 50;// Y轴最后一个刻度离末尾的空白距离
private int mAxisTxtSpace = 20;// 坐标轴与文字的间距
private int mAxisTitleSpace = 20;// 坐标轴与标题的间距
private int mDashEffect = 6;// 虚线的线长
private int mDashEffectSpace = 12;// 虚线的间隔

private int mAxisColor = Color.LTGRAY;
private int mAxisTxtColor = Color.BLACK;
private int mDashLineColor = Color.LTGRAY;
private int mHistogramColor = Color.GREEN;

以上自定义属性由xml直接配置即可

arduino 复制代码
// -------非自定义,计算得到的属性---------
private int mXIntervalSize;// X轴两个刻度之间的间距
private int mYIntervalSize;// Y轴两个刻度之间的间距
private int mAxisY_XPoi;// Y轴的X位置
private int mAxisX_YPoi;// X轴的Y位置
private String mAxisXTitle, mAxisYTitle; // XY轴末尾的标题
private final List<String> mAxisXTxt = new ArrayList<>(); // X轴的值
private final List<Integer> mAxisYTxt = new ArrayList<>(); // Y轴的值
private int mAxisYIntervalValue; // Y轴两个刻度值的差
private final List<HistogramBean> data = new ArrayList<>(); // 柱状条的值

以上属性则需要通过数据进行设置并计算读取了

其中数据模型如下:

arduino 复制代码
public class HistogramBean {
    public String axisX = ""; // 对应X轴的值
    public int axisY; // Y轴的值
}

通过数据进行计算相应坐标位置

typescript 复制代码
public void setAxisInfo(List<String> axisXTxt, List<Integer> axisYTxt,
    String axisXTitle, String axisYTitle) {
    ...
}

public void setData(List<HistogramBean> data) {
    ...
    calculateInfo();
}

首先需要调用以上两个方法,将坐标轴相应的信息传递进来

setAxisInfo用于设置坐XY轴信息

setData则用于设置柱状条数据

scss 复制代码
private void calculateInfo() {
    mPath.reset();
    if (mAxisXTxt.size() == 0 || mAxisYTxt.size() == 0) return;

    // 计算X轴Y轴位置
    mAxisY_XPoi = 0;
    for (int txt : mAxisYTxt) {
        mAxisY_XPoi = (int) Math.max(mAxisY_XPoi, mPaint.measureText(String.valueOf(txt)) + mAxisTxtSpace);
    }
    mAxisX_YPoi = (int) (mHeight - mPaint.getTextSize() - mAxisTxtSpace);

    // 计算刻度间距
    float xTitleWidth = mPaint.measureText(mAxisXTitle);
    mXIntervalSize = (int) ((mWidth - mAxisY_XPoi - mAxisXLastSpace - xTitleWidth - mAxisTitleSpace) / mAxisXTxt.size());
    mYIntervalSize = (int) ((mAxisX_YPoi - mAxisYLastSpace - mPaint.getTextSize() - mAxisTitleSpace) / mAxisYTxt.size());

    mPath.moveTo(mAxisY_XPoi, mAxisX_YPoi);
    mPath.lineTo(mWidth - xTitleWidth - mAxisTitleSpace, mAxisX_YPoi);

    mPath.moveTo(mAxisY_XPoi, mAxisX_YPoi);
    mPath.lineTo(mAxisY_XPoi, mPaint.getTextSize() + mAxisTitleSpace);
    invalidate();
}

之后根据设置的数据进行计算:

首先计算0点的位置:mAxisX_YPoi为X轴的Y坐标位置,计算为高度mHeight - 底部X轴文本大小 - X轴文本到X轴的间距即可;mAxisY_XPoi则为Y轴的X坐标位置,计算为Y轴文本大小 + Y轴文本到Y轴的间距,关键点在于Y轴的文本长度不一样,需要循环遍历获取最长的文本大小。

其次计算X轴刻度间距mXIntervalSize:总的宽度mWidth - 0点X位置mAxisY_XPoi - X轴末尾留空大小mAxisXLastSpace - 末尾标题大小xTitleWidth - 末尾标题与X轴间距mAxisTitleSpace,再除以总的X轴刻度数,即为X轴刻度间距

最后计算Y轴刻度间距mYIntervalSize:0点Y位置mAxisX_YPoi - Y轴末尾留空大小mAxisYLastSpace - 末尾标题大小mPaint.getTextSize() - 末尾标题与Y轴间距mAxisTitleSpace,再除以总的Y轴刻度数,即为Y轴刻度间距

mPath进行连接XY轴:

ini 复制代码
    mPath.moveTo(mAxisY_XPoi, mAxisX_YPoi);
    mPath.lineTo(mWidth - xTitleWidth - mAxisTitleSpace, mAxisX_YPoi);
    mPath.moveTo(mAxisY_XPoi, mAxisX_YPoi);
    mPath.lineTo(mAxisY_XPoi, mPaint.getTextSize() + mAxisTitleSpace);

开始绘制

scss 复制代码
@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    // 绘制坐标轴
    drawAxis(canvas);
    // 绘制刻度和虚线
    drawAxisTxtAndLine(canvas);
    // 绘制数据柱状
    drawData(canvas);
}

根据计算出来的位置进行布局,基本都相对简单了,就不全部列出来了

其中绘制虚线的画笔需要加个PathEffect进行设置:

ini 复制代码
mDashPaint = new Paint();
mDashPaint.setColor(mDashLineColor);
mDashPaint.setAntiAlias(true);
mDashPaint.setStrokeWidth(mDashStrokeWidth);
mDashPaint.setStyle(Paint.Style.STROKE);
// 绘制虚线
mDashPaint.setPathEffect(new DashPathEffect(new float[]{mDashEffect, mDashEffectSpace}, 0));

其次主要是画圆柱时顶部上半圆可能超出X轴位置的问题需要进行处理:

css 复制代码
x = mAxisY_XPoi + (i + 1) * mXIntervalSize;
y = mAxisX_YPoi;

float histogramHeight = mYIntervalSize * ((float) (datum.axisY / mAxisYIntervalValue) +
        datum.axisY % mAxisYIntervalValue / (float) mAxisYIntervalValue);

float left = x - mHistogramWidth / 2f;
float top = y - histogramHeight + mHistogramWidth / 2f;
if (top > y) {
    // 顶部半圆超出了x轴
    Path path = new Path();
    path.moveTo(left, y);
    path.cubicTo(left, y, left + mHistogramWidth / 2f, top - mHistogramWidth / 2f,
            left + mHistogramWidth, y);
    path.close();
    canvas.drawPath(path, mPaint);
} else {
    canvas.drawRect(left, top, left + mHistogramWidth, y, mPaint);
    canvas.drawArc(left, top - mHistogramWidth / 2f, left + mHistogramWidth, 
        top + mHistogramWidth / 2f, 180, 180, true, mPaint);
}

顶部半圆的高度为mHistogramWidth/2,先计算出单项圆柱高度histogramHeight,然后计算矩形顶部位置top = 0点Y轴位置mAxisX_YPoi - histogramHeight - 半圆高度mHistogramWidth / 2f,判断矩形top位置是否超出了X轴的Y位置y,超出了则顶部半圆需要使用贝塞尔函数cubicTo进行绘制,避免超出X轴。如果top没超出X轴,则只需要绘制矩形,再绘制顶部半圆即可。

添加动画

动画,其实就是分进度绘制,如下:

ini 复制代码
public void setDataWithAnim(List<HistogramBean> data) {
    List<HistogramBean> preList = new ArrayList<>(this.data);
    int duration = 300;
    ValueAnimator animator = ObjectAnimator.ofInt(0, duration).setDuration(duration);
    animator.addUpdateListener(animation -> {
        int preValue;
        int addValue;
        HistogramBean bean;
        HistogramView.this.data.clear();
        for (int i = 0; i < data.size(); i++) {
            if (i < preList.size())
                preValue = preList.get(i).axisY;
            else
                preValue = 0;

            addValue = (data.get(i).axisY - preValue) * (int) animation.getAnimatedValue() / duration;
            bean = new HistogramBean();
            bean.axisX = data.get(i).axisX;
            bean.axisY = preValue + addValue;
            HistogramView.this.data.add(bean);
        }
        calculateInfo();
    });
    animator.start();
}

通过ObjectAnimator.ofInt(0,duration).setDuration(duration)分段获取值,进行获取进度百分比:(int) animation.getAnimatedValue() / duration,然后乘以需要绘制的值data.get(i).axisY,就可以计算出当前需要绘制的值,然后进行绘制就可以了

完结撒花

简单吧,没有什么复杂的逻辑操作呢

项目源码:Calendar: 周日历、月日历 用于记录展示事件列表 (gitee.com) 首页点击右上角更多按钮,点击查看柱状图就是了哟。

项目里面的ncalendar模块,使用的是GitHub - yannecer/NCalendar: 一款安卓日历,仿miui,钉钉,华为的日历,万年历、365、周日历,月日历,月视图、周视图滑动切换,农历,节气,Andriod Calendar , MIUI Calendar,小米日历

相关推荐
太空漫步112 小时前
android社畜模拟器
android
海绵宝宝_4 小时前
【HarmonyOS NEXT】获取正式应用签名证书的签名信息
android·前端·华为·harmonyos·鸿蒙·鸿蒙应用开发
凯文的内存6 小时前
android 定制mtp连接外设的设备名称
android·media·mtp·mtpserver
天若子6 小时前
Android今日头条的屏幕适配方案
android
林的快手8 小时前
伪类选择器
android·前端·css·chrome·ajax·html·json
望佑8 小时前
Tmp detached view should be removed from RecyclerView before it can be recycled
android
xvch10 小时前
Kotlin 2.1.0 入门教程(二十四)泛型、泛型约束、绝对非空类型、下划线运算符
android·kotlin
人民的石头14 小时前
Android系统开发 给system/app传包报错
android
yujunlong391914 小时前
android,flutter 混合开发,通信,传参
android·flutter·混合开发·enginegroup
rkmhr_sef15 小时前
万字详解 MySQL MGR 高可用集群搭建
android·mysql·adb