Android自定义View之年度账单曲线图

前言

一说到折线图曲线图,我马上就想到了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移动到某个座标但是移动过程没有痕迹。

动手

  1. 先定义常量
 /**
     * 折线画笔
     */
    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;
  1. 重写View的构造方法,使其最终指向带有3个参数的构造方法
this(context,null);
this(context, attrs,0);
super(context, attrs, defStyleAttr);
  1. 构造方法内初始化数据,此步代码略
  2. 重写onMeasure方法,确定宽高,因为高度一开始就是固定的,有需求的小伙伴可以在方法内确定高度。
  3. 重写onDraw方法
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //画渐变蓝色背景
        drawBackBlue(canvas);
        //画座标
        drawAxis(canvas);
        //画曲线
        drawCurve(canvas);
        //画小圆点和虚线
        drawPointsAndLine(canvas);
        //画水表详情框
        drawWaterDetailsText(canvas);
    }
  1. 写一个公开方法,用于设置元数据
 /**
     * 公开方法,用于设置元数据
     */
    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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章