前言
支付宝有个查看月账单的功能,最近一直在学习自定义View,于是就尝试着自己实现了一个类似的折线图。
下面是支付宝消费分析功能截图和自己实现的折线效果截图:
确定绘制内容
在绘制折线图之前首先要分析折线图需要绘制哪些部分以及如何绘制这些部分,确定了各绘制部分及绘制方法之后再开始具体的绘制。
1.确定需要绘制的区域
将折线图分为四个绘制区域:
- 月份文字
- 折线的边
- 折线数据点处的圆
- 消费金额文字
2.如何绘制相应的区域
- 月份文字和消费金额文字
Android 系统提供了 Canvas.drawText 方法用于绘制文字,我们只需要使用该方法绘制文字即可。关于该方法可以参考 Android中canvas.drawText参数的介绍以及绘制一个文本居中的案例 这篇文章 ,网上还有很多介绍该方法的文章,感兴趣的同学请自行搜索。
public void drawText(@NonNull String text, float x, float y, @NonNull Paint paint)
- 折线的边
折线的边使用 Canvas.drawPath方法绘制,如果有不了解这个方法中第二个参数Path含义和用法可以参考这篇文章 Path从懵逼到精通——基本操作,读完这篇文章基本可以掌握Path的相关操作。
public void drawPath(@NonNull Path path, @NonNull Paint paint)
- 折线数据点处的圆
Android提供下面的方法用于绘制圆形,如果有不了解这个方法的同学还请自行搜索学习。
public void drawCircle(float cx, float cy, float radius, @NonNull Paint paint)
现在已经知道了折线需要绘制哪些部分以及各部分的绘制方法,接下来要做的就是确定绘制折线各部分所需要的点的坐标。
计算绘制点坐标
1.确定折线数据点x坐标
从下面绘制分析图可以直观地看到月份文字中间点x坐标,数据点x坐标,数据点处圆的圆心x坐标以及消费金额文字中间点x坐标的值是相同的,所以只需要确定数据点的x坐标就行了。
观察支付宝的折线图可以看到第一个数据点距离屏幕左侧和最后一个数据点距离屏幕右侧的宽度是相等的,我们将这个宽度定义为基准宽度mBaseWidth。
将相邻两个数据点之间的宽度定义为mItemWidth,仔细观察可以看出这个宽度大概为 3*mBaseWidth,因为折线图固定显示5个月份的数据,所以mBaseWidth值就确定了:
// mWidth为当前整个View的显示宽度
// mWidth = 2 * mBaseWidth + 4 * mItemWidth = 14 * mBaseWidth;
mBaseWidth = mWidth / 14;
mItemWidth = mBaseWidth * 3;
得到mBaseWidth值后各个数据点的x坐标也就确定了:
for (int i = 0; i < pointCount; i++) {
float dx = mBaseWidth + mItemWidth * i;
}
2.确定各绘制点y坐标
从支付宝折线图上可以看到月份的y坐标是固定的,数据点的y坐标是通过消费金额确定的,圆心y坐标和消费金额文字y坐标都依赖于数据点的y坐标。
下面我们就来分别确定月份的y坐标和数据点的y坐标。在绘制分析图中定义了几个参考变量,各变量的含义如下:
/**
* 数据点最低y轴坐标
*/
private float mMinLineHeight;
/**
* 数据点最高y轴坐标
*/
private float mMaxLineHeight;
/**
* 折线最大可占用空间的高度,即折线波峰和波谷的差值,用来控制折线的陡峭程度
*/
private float mMaxLineSpace;
月份文字y坐标
从绘制分析图可以看到月份的y坐标范围在区间 [3/4*mHeight, mHeight] 之间,只要以 3/4*mHeight 为基准值加上一个偏移量就可得到月份的y轴坐标,那么如何确定这个偏移量的大小呢?
首先通过下面的方法计算月份文字的高度:
Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();
float fontHeight = fontMetrics.descent - fontMetrics.ascent;
......
float dy = mMaxLineHeight + fontHeight * 3 / 2;
计算出文字高度后选取 fontHeight * 3 / 2 作为偏移量,让月份文字的顶部距离 3/4*mWidth 基准线的高度刚好为 fontHeight。这个偏移量的值不是固定的,如果对最后绘制效果不满意,可以调整该值到自己满意的效果。
数据点y坐标
依然先定义一些控制参数:
/**
* 已出账单月份中最低消费金额
*/
private double mMinBillValue;
/**
* 已出账单月份中最高消费金额
*/
private double mMaxBillValue;
/**
* 消费金额的极差
*/
private double mBillRange;
mMaxBillValue = Utils.getMaxBillValue(mBillValues);
mMinBillValue = Utils.getMinBillValue(mBillValues);
mBillRange = mMaxBillValue - mMinBillValue;
mBillRange = (mBillRange == 0 ? 1 : mBillRange);
先说数据点y坐标的确定方法,接下来再给出分析:
float dy = (float) (mMaxLineHeight - (mBillValues[i] - mMinBillValue) / mBillRange * mMaxLineSpace);
从计算公式可以看出数据点y坐标是通过mMaxLineHeight减去一个偏移量得到的,只需要确定偏移量的计算方法就可以了。
首先计算出当前月消费金额和最低消费金额的差值,然后用这个差值除以消费金额的极差mBillRange得到一个比例值,再用这个比例值去乘以mMaxLineSpace就是偏移值,然后用mMaxLineHeight减去偏移值就是数据点的y轴坐标。
数据点y坐标确定之后其它几个点的坐标也就确定了。圆心的y坐标就是数据点y坐标,消费金额文字y坐标通过数据点y坐标减去消费金额文字的 fontHeight * 3 / 2得到。
上面得到的仅仅是在账单金额已经出来情况下数据点的y坐标,那么月份的消费金额数还没出来时数据点y坐标该如何确定呢?因为只是为了演示效果,所以我采用下面的计算方法来得到未出账单月份的数据点y坐标。
for (int i = pointCount; i < totalMonths; i++) {
float dx = mBaseWidth + mItemWidth * i;
float dy = mMinLineHeight + mMaxLineSpace * (i - pointCount + 1) / (totalMonths - pointCount);
mPath.lineTo(dx, dy);
}
绘制细节
确定绘制点的坐标之后,剩下的就是一些需要注意的细节,比如已出账单月份数据点处的圆是实心的,未出账单月份数据点处的圆是空心的。还有数据点之间的连线有虚线和实线之分,实线的颜色也有不同,绘制的时候注意这些细节就可以了。
顺带介绍一下Android中虚线绘制是通过设置画笔的路径效果来实现的:
DashPathEffect dashPathEffect = new DashPathEffect(new float[]{20, 20}, 1);
mLinePaint.setPathEffect(dashPathEffect);
关于DashPathEffect可以参考我的另一篇文章Android PathEffect - DashPathEffect 了解一下。
总结
折线图的目的是学习绘制自定义View,跟支付宝的实际逻辑肯定是完全不同的。另外目前项目并没有并没有在onMeasure进行特殊的绘制处理,等以后再优化改进。
项目地址:https://github.com/IvanRich/MonthBill
更新
2017-06-20 增加onMeasure测量过程的处理 diff