先来看看效果图
分析一波
固定的属性:
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,小米日历