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

先来看看效果图

SVID_20231215_102311 -original-original (1).gif

分析一波

固定的属性:

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

动态变化的属性:

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

看看整体的属性

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直接配置即可

// -------非自定义,计算得到的属性---------
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<>(); // 柱状条的值

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

其中数据模型如下:

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

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

public void setAxisInfo(List<String> axisXTxt, List<Integer> axisYTxt,
    String axisXTitle, String axisYTitle) {
    ...
}

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

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

setAxisInfo用于设置坐XY轴信息

setData则用于设置柱状条数据

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轴:

    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);

开始绘制

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

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

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

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

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轴位置的问题需要进行处理:

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轴,则只需要绘制矩形,再绘制顶部半圆即可。

添加动画

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

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

微信搜索“A查佣利小助手”,获取支付宝红包、TB/JD/PDD返利最新优惠资讯
请添加图片描述

  • 27
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

虾米~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值