Android 自定义可填充颜色的曲线图

这次自己动手写一个可填充颜色的曲线图,并且可以添加动画效果的。先上效果图。
这里写图片描述
有填充颜色的
没有填充颜色的
其中第一张是最终的效果,这里当某个点大于195的时候就在画多一条虚线,做特殊处理。第二张图是它的动态效果图,第三张图是没有填充颜色的动态效果图。gif图做得一般,有好的软件可以推荐给我。

一、思路

还是一开始说一下我的思路吧,代码的实现在后面。

1、坐标系的绘制

坐标系的绘制就是两条互相垂直的直线,难点在于要计算X,Y轴的刻度,并且每隔5个刻度要把它突出,我这里是分别用X,Y轴的总长度除以它们X,Y轴数据,就可以得出每隔刻度所对应的长度了。

2、曲线的绘制

曲线的绘制用到的知识点是三阶贝塞尔曲线,定义两个控制点X值为要绘制的两点的中点。

3、颜色的填充

在绘制曲线的时候,同时在绘制一条封闭的曲线(有填充颜色),只需要设置画笔的属性即可。至于颜色的渐变部分是用LinearGradient和PorterDuffXfermode来画一个相同大小的矩形。

4、动画效果的添加

这里依然是使用属性动画,它需要api大于11。当图像第一次上到最高点后,继续执行两次动画,让它有点缓冲的效果。

二、代码实现

1、重写onMeasure

处理为wrap_content情况,那么它的specMode是AT_MOST模式,在这种模式下它的宽/高等于spectSize,这种情况下view的spectSize是parentSize,而parentSize是父容器目前可以使用大小,就是父容器当前剩余的空间大小, 就相当于使用match_parent一样 的效果,因此我们可以设置一个默认的值。

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpectMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpectSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpectMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpectSize = MeasureSpec.getSize(heightMeasureSpec);
        if (widthSpectMode == MeasureSpec.AT_MOST
                && heightSpectMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(mWidth, mHeight);
        } else if (widthSpectMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(mWidth, heightSpectSize);
        } else if (heightSpectMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSpectSize, mHeight);
        }
    }

2、在构造方法里初始化一些属性

这里只是方便我测试,不建议在这里就初始化数据。

public CurveChart(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }
private void init(Context context, AttributeSet attrs) {
        //在画布上去除锯齿
        drawFilter = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG
                | Paint.FILTER_BITMAP_FLAG);
        paint = new Paint();
        paint.setColor(black);
        textPaint = new Paint();
        dashPaint = new Paint();
        scaleDistance = dip2px(context, scaleDistance);
        scaleLen = dip2px(context, scaleLen);
        isFillDownLineColor = true;
        xStart = 0;
        yStart = 150;
        xEnd = 30;
        yEnd = 200;
        xValues = new float[] { 0, 1, 2, 3, 5, 7, 8, 15, 20, 30 };
        yValues = new float[] { 185, 195, 197, 195, 193, 195, 198, 193, 199,192 };
        compareValue = 195;
    }

3、画坐标系

根据传进来的x,y的刻度值,计算它们每个刻度的所占长度,这里要考虑控件的padding值,不然就不好画xy轴的刻度了。而且要注意控件的左上角才是y轴的开始地方。

@Override
    protected void onDraw(Canvas canvas) {
        canvas.setDrawFilter(drawFilter);
        drawCoordinate(canvas);
        drawPoint(canvas);
    }

    /**
     * 画坐标系
     * 
     * @param canvas
     */
    private void drawCoordinate(Canvas canvas) {
        Rect rect = new Rect();
        textPaint.getTextBounds("300", 0, 3, rect);
        // 所画的坐标系的原点位置
        int startX = getPaddingLeft() + rect.width() + scaleDistance;
        int startY = mHeight - rect.height() - getPaddingBottom()- scaleDistance;
        // X轴的长度
        int lengthX = mWidth - getPaddingRight() - startX;
        // Y轴的长度
        int lengthY = startY - getPaddingTop();
        float countX, countY;
        countX = xEnd - xStart;
        countY = yEnd - yStart;
        // x轴每个刻度的长度
        perLengthX = 1.0f * lengthX / countX;
        // y轴每个刻度的长度
        perLengthY = 1.0f * lengthY / countY;
        // 画横坐标
        canvas.drawLine(startX, startY, mWidth - getPaddingRight(), startY,paint);
        // 画纵坐标
        canvas.drawLine(startX, startY, startX, getPaddingTop(), paint);
        // 画x轴的刻度
        for (int i = 0; i <= countX; i++) {
            if (i == 0) {
                // 画原点的数字
                canvas.drawText("" + (int) xStart, startX, mHeight- getPaddingBottom(), textPaint);
                continue;
            }
            float x = startX + i * perLengthX;
            float y1 = startY - scaleLen;
            float y2 = startY - 2 * scaleLen;
            if (i % 5 == 0) {
                // 加长一点
                canvas.drawLine(x, startY, x, y2, paint);
                // 画下面的数字
                canvas.drawText("" + (int) (xStart + i), x - rect.width() / 2,mHeight - getPaddingBottom(),textPaint);
            } else {
                canvas.drawLine(x, startY, x, y1, paint);
            }
        }
        // 画y轴的刻度
        for (int i = 0; i <= countY; i++) {
            if (i == 0) {
                canvas.drawText("" + (int) yStart, getPaddingLeft(), startY,textPaint);
                continue;
            }
            float y = startY - i * perLengthY;
            float x1 = startX + scaleLen;
            float x2 = startX + 2 * scaleLen;
            if (i % 5 == 0) {
                // 加长一点
                canvas.drawLine(startX, y, x2, y, paint);
                canvas.drawText("" + (int) (yStart + i), getPaddingLeft(), y+ rect.height() / 2, textPaint);
            } else {
                canvas.drawLine(startX, y, x1, y, paint);
            }
        }
    }

4、画曲线和填充渐变颜色

使用LinearGradient 和PorterDuffXfermode结合来填充渐变的颜色。曲线的效果是用三阶的贝赛尔曲线(path.cubicTo方法)。在这里其实画了两条曲线,一条是黑色的曲线(path2路径),一条是封闭的有填充颜色的曲线(path路径),当不需要填充效果时候就剩下黑色的曲线了。

private void drawPoint(Canvas canvas) {
        // 用于保存y值大于compareValue的值
        float[] storageX = new float[xValues.length];
        float[] storageY = new float[xValues.length];
        Rect rect = new Rect();
        textPaint.getTextBounds("300", 0, 3, rect);
        int startX = getPaddingLeft() + rect.width() + scaleDistance;
        int startY = mHeight - rect.height() - getPaddingBottom()
                - scaleDistance;
        linePaint = new Paint();
        linePaint.setColor(fillColor);
        // 把拐点设置成圆的形式,参数为圆的半径,这样就可以画出曲线了
        PathEffect pe = new CornerPathEffect(45);
        // linePaint.setPathEffect(pe);
        if (!isFillDownLineColor) {
            linePaint.setStyle(Paint.Style.STROKE);
        }
        Path path = new Path();
        Path path2 = new Path();
        path.moveTo(startX + (xValues[0] - xStart) * perLengthX, startY
                - (yValues[0] - yStart) * perLengthY * fraction);
        int count = xValues.length;
        for (int i = 0; i < count - 1; i++) {
            float x, y, x2, y2, x3, y3, x4, y4;
            x = startX + (xValues[i] - xStart) * perLengthX;
            x4 = (startX + (xValues[i + 1] - xStart) * perLengthX);
            x2 = x3 = (x + x4) / 2;
            // 乘以这个fraction是为了添加动画特效
            y = startY - (yValues[i] - yStart) * perLengthY * fraction;
            y4 = startY - (yValues[i + 1] - yStart) * perLengthY * fraction;
            y2 = y;
            y3 = y4;
            if (yValues[i] > compareValue) {
                storageX[i] = x;
                storageY[i] = y;
            }
            if (!isFillDownLineColor && i == 0) {
                path2.moveTo(x, y);
                path.moveTo(x, y);
                continue;
            }
            // 填充颜色
            if (isFillDownLineColor && i == 0) {
                // 形成封闭的图形
                path2.moveTo(x, y);
                path.moveTo(x, startY);
                path.lineTo(x, y);
            }
            // // 填充颜色
            // if (isFillDownLineColor && i == count - 1) {
            // path.lineTo(x, startY);
            // }
            path.cubicTo(x2, y2, x3, y3, x4, y4);
            path2.cubicTo(x2, y2, x3, y3, x4, y4);
        }
        if (isFillDownLineColor) {
            // 形成封闭的图形
            path.lineTo(startX + (xValues[count - 1] - xStart) * perLengthX,startY);
        }
        Paint rectPaint = new Paint();
        rectPaint.setColor(blue);
        float left = startX + (xValues[0] - xStart) * perLengthX;
        float top = getPaddingTop();
        float right = startX + (xValues[count - 1] - xStart) * perLengthX;
        float bottom = startY;
        // 渐变的颜色
        LinearGradient lg = new LinearGradient(left, top, left, bottom,Color.parseColor("#00ffffff"),Color.parseColor("#bFffffff"),Shader.TileMode.CLAMP);// CLAMP重复最后一个颜色至最后
        rectPaint.setShader(lg);
        rectPaint.setXfermode(new PorterDuffXfermode(
                android.graphics.PorterDuff.Mode.SRC_ATOP));
        if (isFillDownLineColor) {
            canvas.drawPath(path, linePaint);
        }
        canvas.drawRect(left, top, right, bottom, rectPaint);
        // canvas.restoreToCount(layerId);
        rectPaint.setXfermode(null);
        linePaint.setStyle(Paint.Style.STROKE);
        linePaint.setColor(lineColor);
        canvas.drawPath(path2, linePaint);
        linePaint.setPathEffect(null);
        drawDashAndPoint(storageX, storageY, startY, canvas);
        if (!stop)
            performAnimator();
        if (fraction > 0.99) {
            performAnimator();
        }
    }

然后把y值大于某个值(这里是195)的点用虚线特别画出。用到的是DashPathEffect方法要注意:画笔设置不是填充的,不然画一条虚线是没显示出来的
dashPaint.setStyle(Paint.Style.STROKE);

private void drawDashAndPoint(float[] x, float[] y, float startY,
            Canvas canvas) {
        PathEffect pe = new DashPathEffect(new float[] { 10, 10 }, 1);
        // 要设置不是填充的,不然画一条虚线是没显示出来的
        dashPaint.setStyle(Paint.Style.STROKE);
        dashPaint.setPathEffect(pe);
        dashPaint.setColor(lineColor);
        Paint pointPaint = new Paint();
        pointPaint.setColor(lineColor);
        for (int i = 0; i < x.length; i++) {
            if (y[i] > 1) {
                canvas.drawCircle(x[i], y[i], 2, pointPaint);
                Path path = new Path();
                path.moveTo(x[i], startY);
                path.lineTo(x[i], y[i]);
                canvas.drawPath(path, dashPaint);
            }
        }
    }

5、使用属性动画添加效果

这里就执行了3次动画,让它有点缓冲的效果。在下面的onAnimationUpdate方法里获取到当前的变量值fraction ,然后重新绘制控件。

public void performAnimator() {
        if (numCount > 3)
            return;
        ValueAnimator va = ValueAnimator.ofFloat(0, 1);
        if (numCount == 1) {
            va = ValueAnimator.ofFloat(0, 1);
        } else if (numCount == 2) {
            va = ValueAnimator.ofFloat(0.85f, 1);
        } else if (numCount == 3) {
            va = ValueAnimator.ofFloat(0.95f, 1);
        }
        numCount++;
        va.addUpdateListener(new AnimatorUpdateListener() {

            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                fraction = (float) animation.getAnimatedValue();
                stop = true;
                postInvalidate();
            }
        });
        va.setDuration(1000);
        va.start();
    }

6、使用builder方式进行封装

public static class CurveChartBuilder {
        private static CurveChart curveChart;
        private static CurveChartBuilder cBuilder;
        private CurveChartBuilder(){
        }
        public static CurveChartBuilder createBuilder(CurveChart curve){
            curveChart = curve;
            synchronized (CurveChartBuilder.class) {
                if(cBuilder==null){
                    cBuilder = new CurveChartBuilder();
                }
            }
            return cBuilder;
        }
        /**设置x,y轴的刻度
         * @param xStart X轴开始的刻度
         * @param xEnd X轴结束的刻度
         * @param yStart
         * @param yEnd
         * @return
         */
        public static CurveChartBuilder setXYCoordinate(float xStart,float xEnd,float yStart,float yEnd){
            curveChart.setxStart(xStart, xEnd);
            curveChart.setyStart(yStart, yEnd);
            return cBuilder;
        }
        /**是否填充曲线下面的颜色,默认值为true,
         * @param isFillDownLineColor
         * @return
         */
        public static CurveChartBuilder setIsFillDownColor(boolean isFillDownLineColor){
            curveChart.setFillDownLineColor(isFillDownLineColor);
            return cBuilder;
        }
        /**设置填充的颜色
         * @param fillColor
         * @return
         */
        public static CurveChartBuilder setFillDownColor(int fillColor){
            curveChart.setFillColor(fillColor);
            return cBuilder;
        }
        /**比较的值,比这个值大就把这个点也绘制出来
         * @param compareValue 
         * @return
         */
        public static CurveChartBuilder setCompareValue(float compareValue){
            curveChart.setCompareValue(compareValue);
            return cBuilder;
        }
        public static CurveChartBuilder setXYValues(float[] xValues,float[] yValues){
            curveChart.setxValues(xValues);
            curveChart.setyValues(yValues);
            return cBuilder;
        }
        public void show(){
            if(curveChart==null){
                throw new NullPointerException("CurveChart is null");
            }
            curveChart.show();
        }
    }

7、其它的工具方法

/**
     * 根据手机的分辨率从 dp 的单位 转成为 px(像素)
     */
    public int dip2px(Context context, float dpValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }

三、总结

动手去尝试了才会遇到各种问题,有时候一个问题都卡了很久,不过这次自己实现了这一控件,收获还是挺多的。

源码下载

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值