前言
一说到折线图曲线图,我马上就想到了GitHub上的MPAndroidChart,扩展功能强大,本来想用,不过我转念一想,就一个年度账单,自己写不是几百行代码就搞定了?于是我开始了。先看一下设计图
思考
- 先考虑需要几种画笔Paint:曲线、字体、网格、背景色、详情背景色
- 再考虑几个重要的变量:折线单位高度差、横向和纵向Item的高宽度、当年的最高用量、当年的最低用量
- 需要确定的数据源:当前年份的水表数据集合
- 需要计算每个月点的座标位置集合List<PointF>
- 横向可滑动、选中的点pointFSelected以及点击范围的判定,在每个月峰值某个范围内允许点击。
必须先了解的知识点
- Canvas座标系与绘图座标系:简单点说Canvas座标系唯一不变,平常我们使用绘图座标系(例如canvas的drawLine)。
- Canvas的save和restore方法:了解了绘图座标系,应该很好了解,比如我先sava一下(保存当前矩阵),然后调用canvas的旋转,然后使用Paint去绘制,绘制完了,调用restore回到之前没有旋转的画板继续画。
- VelocityTracker:速度追踪器,主要用跟踪触摸屏事件(flinging事件和其他gestures手势事件)的速率。
- ViewConfiguration:包含用于UI的标准常量的方法,用于超时,大小和距离。用来判断是否是抛动。
- Paint使用PathEffect画出虚线:
//两个值分别为循环的实线长度、空白长度
float[] f = {dp2pxF(5f,context), dp2pxF(2f,context)};
PathEffect pathEffect = new DashPathEffect(f, 0);
paint.setPathEffect(pathEffect);
/**
* dp转pxF
*/
public static float dp2pxF(float dpValue,Context context) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue, context.getResources().getDisplayMetrics());
}
- 画渐变背景:通过设置画笔来实现。
/**
* 渐变背景画笔
*/
private Paint backGroundPaint;
backGroundPaint=new Paint(Paint.ANTI_ALIAS_FLAG);
//设置抗锯齿
backGroundPaint.setAntiAlias(true);
//为Paint设置渐变
LinearGradient linearGradient=new LinearGradient(pointFMaxCurve.x,pointFMaxCurve.y,pointFList.get(0).x,viewHeight-itemWidth,new int[]{
0xFFE1F1FF,0xFFEFF7FF,0xFFFAFCFF},
null, Shader.TileMode.CLAMP);
backGroundPaint.setShader(linearGradient);
- text宽度的自适应:因为数据变化大,所以详情框座标确认就需要获取text的宽度。思路是这样子:数据都是空字符串时背景框的宽度加上获取text的宽度就是上图用量读数弹框的宽度了。
/**
* 精确计算文字宽度
*
* @param paint
* @param str
* @return 文字长度,像素
*/
public static int getTextWidth(Paint paint, String str) {
int iRet = 0;
if (str != null && str.length() > 0) {
int len = str.length();
float[] widths = new float[len];
paint.getTextWidths(str, widths);
for (int j = 0; j < len; j++) {
iRet += (int) Math.ceil(widths[j]);
}
}
return iRet;
}
- Path常用Api:如cubicTo画三次贝塞尔曲线、reset清除当前路径、moveTo移动到某个座标但是移动过程没有痕迹。
动手
- 先定义常量
/**
* 折线画笔
*/
private Paint brokenLinePaint;
/**
* 座标画笔
*/
private Paint coordinatePaint;
/**
* 圆点画笔
*/
private Paint circlePaint;
/**
* 文字画笔(x、y轴)
*/
private Paint textPaint;
/**
* 文字画笔(水表详情)
*/
private Paint textPaintDetail;
/**
* 渐变背景画笔
*/
private Paint backGroundPaint;
/**
* 详情背景画笔
*/
private Paint backGroundDetailPaint;
/**
* 背景颜色
*/
private int backGroundColor= Color.WHITE;
/**
* 文字画笔颜色
* 默认:黑色
*/
private int colorTextPaint= Color.BLACK;
/**
* 折现画笔颜色
* 默认:蓝色 0xFF1281FD
*/
private int colorBrokenLinePaint=0xFF1281FD;
/**
* 座标画笔颜色
* 默认 灰色 0xFFEEEEEE
*/
private int colorCoordinatePaint=0xFFEEEEEE;
/**
* 详情文字背景颜色
*/
private int colorDetailTextBg=0x66000000;
/**
* 文字大小
* 默认11sp
*/
private float textSize;
/**
* 水表详情文字大小
* 默认10sp
*/
private float textSizeDetail;
/**
* 单位高度差
* 默认:itemWidth/unitYItem
*/
private float unitVerticalGap;
/**
* itemWidth对应的用水量(吨)
*/
private int unitYItem;
/**
* Y方向有数据的Item个数
* 默认4个
*/
private static final int itemYSize=4;
/**
* 折线图左和下的间距,同横纵单位间隔
* 默认:42dp
*/
private int defaultPadding;
private int itemWidth;
/**
* 控件期望高度
* 默认为8个itemWidth
*/
private int expectViewHeight;
/**
* 折线点的半径(默认2.5dp的像素)
*/
private float pointRadius;
private int viewWidth;
private int viewHeight;
private int screenWidth;
private int screenHeight;
/**
* 曲线路径
*/
private Path curvePath;
/**
* 水表详情背景路径
*/
private Path WaterDetailBgPath;
/**
* 水表详情背景图范围
*/
private RectF rectF;
/**
* 最多用量
*/
private float maxDosage;
/**
* 最少用量
*/
private float minDosage;
/**
* 数据列表data
*/
private List<WaterAndElectricMeterDetail> list=new ArrayList<>();
/**
* 每个月的座标点集
*/
private List<PointF> pointFList=new ArrayList<>();
/**
* 选中的那个点
*/
private PointF pointFSelected=null;
/**
* 速度追踪器
*/
private VelocityTracker velocityTracker;
/**
* 关于UI的标准常量
*/
private ViewConfiguration viewConfiguration;
/**
* Scroller
*/
private Scroller scroller;
- 重写View的构造方法,使其最终指向带有3个参数的构造方法
this(context,null);
this(context, attrs,0);
super(context, attrs, defStyleAttr);
- 构造方法内初始化数据,此步代码略
- 重写onMeasure方法,确定宽高,因为高度一开始就是固定的,有需求的小伙伴可以在方法内确定高度。
- 重写onDraw方法
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//画渐变蓝色背景
drawBackBlue(canvas);
//画座标
drawAxis(canvas);
//画曲线
drawCurve(canvas);
//画小圆点和虚线
drawPointsAndLine(canvas);
//画水表详情框
drawWaterDetailsText(canvas);
}
- 写一个公开方法,用于设置元数据
/**
* 公开方法,用于设置元数据
*/
public void setData(List<WaterAndElectricMeterDetail> data) {
if (data == null) {
return;
}
//数据清理
list.clear();
pointFList.clear();
pointFSelected=null;
this.list = data;
//计算单位高度差
calculateGap();
//获取数据点集
initPointFData();
invalidate();
}
附上GitHub上Demo地址:https://github.com/PengHaiZhuo/MyMiUiWeatherDemo
后话
因需求简单,所以很多变量值都写死了,有兴趣的朋友可以自行修改或者通过定义自定义属性来优化,主要是学习,加油。
非常感谢这个博主分享,提供了思路,链接:https://blog.csdn.net/ccy0122/article/details/76464825