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,小米日历

相关推荐
深海呐1 小时前
Android 最新的AndroidStudio引入依赖失败如何解决?如:Failed to resolve:xxxx
android·failed to res·failed to·failed to resol·failed to reso
解压专家6661 小时前
安卓解压软件推荐:高效处理压缩文件的实用工具
android·智能手机·winrar·7-zip
Rverdoser1 小时前
Android 老项目适配 Compose 混合开发
android
️ 邪神3 小时前
【Android、IOS、Flutter、鸿蒙、ReactNative 】标题栏
android·flutter·ios·鸿蒙·reatnative
努力遇见美好的生活4 小时前
Mysql每日一题(行程与用户,困难※)
android·数据库·mysql
图王大胜5 小时前
Android Framework AMS(17)APP 异常Crash处理流程解读
android·app·异常处理·ams·crash·binderdied·讣告
ch_s_t6 小时前
电子商务网站之首页设计
android
豆 腐8 小时前
MySQL【四】
android·数据库·笔记·mysql
想取一个与众不同的名字好难10 小时前
android studio导入OpenCv并改造成.kts版本
android·ide·android studio
Jewel10511 小时前
Flutter代码混淆
android·flutter·ios