Android 进阶自定义View(4)图表统计LineChartView曲线图的实现

简介: 接着上篇,今天介绍一下曲线图 / 折线图的实现方法,先上效果图:image.png曲线图很简单了,坐标轴跟刻度线跟上篇柱状图的绘制一样一样滴,绘制曲线图,关键就是确定好Y轴的每个点,然后用绘制曲线的方法,把点连起来就OK了。

接着上篇,今天介绍一下曲线图 / 折线图的实现方法,先上效果图:


img_fd12e7749a6b17c484fba6a5277aa628.png
image.png

曲线图很简单了,坐标轴跟刻度线跟上篇柱状图的绘制一样一样滴,绘制曲线图,关键就是确定好Y轴的每个点,然后用绘制曲线的方法,把点连起来就OK了。

(1)确定数据在坐标轴上对应的每个点

 /**
     * 根据传入的数据,确定绘制的点
     *
     * @return
     */
    private Point[] initPoint() {
        Point[] points = new Point[mDatas.size()];
        for (int i = 0; i < mDatas.size(); i++) {
            Integer ybean = mDatas.get(i);
            int drawHeight = (int) (startY * 1.0 - (ybean * yAxisSpace * 1.0 / yIncreaseValue));
            int startx = startX + xAxisSpace * i;
            points[i] = new Point(startx, drawHeight);
        }
        Log.e("TAG", "startX=" + startX + "---startY=" + startY);
        return points;
    }

(2)将点连接起来,这里使用cubicTo绘制贝塞尔曲线。


img_1111b5f62648835cf2c9d591f8376316.png
image.png

    /**
     * 绘制曲线-曲线图
     *
     * @param canvas
     */
    private void drawScrollLine(Canvas canvas) {
        Point startp;
        Point endp;
        for (int i = 0; i < mPoints.length - 1; i++) {
            startp = mPoints[i];
            endp = mPoints[i + 1];
            int wt = (startp.x + endp.x) / 2;
            Point p3 = new Point();
            Point p4 = new Point();
            p3.y = startp.y;
            p3.x = wt;
            p4.y = endp.y;
            p4.x = wt;
            Path path = new Path();
            path.moveTo(startp.x, startp.y);
            path.cubicTo(p3.x, p3.y, p4.x, p4.y, endp.x, endp.y);
            canvas.drawPath(path, mPaint);
        }
    }


 /**
     * 绘制直线-折线图
     *
     * @param canvas
     */
    private void drawLine(Canvas canvas) {
        Point startp;
        Point endp;
        for (int i = 0; i < mPoints.length - 1; i++) {
            startp = mPoints[i];
            endp = mPoints[i + 1];
            canvas.drawLine(startp.x, startp.y, endp.x, endp.y, mPaint);
        }
    }

完整代码:

package com.example.jojo.learn.customview;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import android.graphics.Rect;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

import com.example.jojo.learn.R;
import com.example.jojo.learn.utils.DP2PX;

import java.util.ArrayList;
import java.util.List;

/**
 * Created by JoJo on 2018/8/3.
 * wechat:18510829974
 * description: 曲线图/折线图
 */

public class LineChartView extends View {
    private Context mContext;
    //绘制坐标轴的画笔
    private Paint mAxisPaint;
    //绘制曲线的画笔
    private Paint mPaint;
    //绘制X轴上方的画笔
    private Paint mXAxisLinePaint;
    private Paint mPaintText;
    //向上的曲线图的绘制起点(px)
    private int startX;
    private int startY;
    //向下的曲线图的绘制起点(px)
    private int downStartX;
    private int downStartY;
    //上方Y轴每单位刻度所占的像素值
    private float YAxisUpUnitValue;
    //下方Y轴每单位刻度所占的像素值
    private float YAxisDownUnitValue;
    //根据具体传入的数据,在坐标轴上绘制点
    private Point[] mPoints;
    //传入的数据,决定绘制的纵坐标值
    private List<Integer> mDatas = new ArrayList<>();
    //Y轴刻度集合
    private List<Integer> mYAxisList = new ArrayList<>();
    //X轴刻度集合
    private List<String> mXAxisList = new ArrayList<>();
    //X轴的绘制距离
    private int mXAxisMaxValue;
    //Y轴的绘制距离
    private int mYAxisMaxValue;
    //Y轴刻度间距(px)
    private int yAxisSpace = 120;
    //X轴刻度间距(px)
    private int xAxisSpace = 200;
    //Y轴刻度线宽度
    private int mKeduWidth = 20;
    private float keduTextSize = 20;
    //刻度值距离坐标的padding距离
    private int textPadinng = 10;
    //Y轴递增的实际值
    private int yIncreaseValue;
    //true:绘制曲线 false:折线
    private boolean isCurve = true;
    private Rect mYMaxTextRect;
    private Rect mXMaxTextRect;
    private int mMaxTextHeight;
    private int mMaxTextWidth;

    public LineChartView(Context context) {
        this(context, null);
    }

    public LineChartView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public LineChartView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.mContext = context;
        initData();
        initView();

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        if (heightMode == MeasureSpec.AT_MOST) {
            heightSize = (mYAxisList.size() - 1) * yAxisSpace + mMaxTextHeight * 2 + textPadinng * 2;
        }
        if (widthMode == MeasureSpec.AT_MOST) {

            widthSize = startX + (mDatas.size() - 1) * xAxisSpace + mMaxTextWidth;
        }
        //保存测量结果
        setMeasuredDimension(widthSize, heightSize);
    }

    private void initView() {
        //初始化画笔
        mPaint = new Paint();
        mPaint.setColor(ContextCompat.getColor(mContext, R.color.color_efaf34));
        mPaint.setStrokeWidth(2);
        mPaint.setAntiAlias(true);
        mPaint.setStyle(Paint.Style.STROKE);
        //绘制X,Y轴坐标的画笔
        mAxisPaint = new Paint();
        mAxisPaint.setColor(ContextCompat.getColor(mContext, R.color.color_274782));
        mAxisPaint.setStrokeWidth(2);
        mAxisPaint.setAntiAlias(true);
        mAxisPaint.setStyle(Paint.Style.STROKE);
        //绘制坐标轴上方的横线的画笔
        mXAxisLinePaint = new Paint();
        mXAxisLinePaint.setColor(ContextCompat.getColor(mContext, R.color.color_274782));
        mXAxisLinePaint.setStrokeWidth(1);
        mXAxisLinePaint.setAntiAlias(true);
        mXAxisLinePaint.setStyle(Paint.Style.STROKE);

        //绘制刻度值文字的画笔
        mPaintText = new Paint();
        mPaintText.setTextSize(keduTextSize);
        mPaintText.setColor(ContextCompat.getColor(mContext, R.color.color_a9c6d6));
        mPaintText.setAntiAlias(true);
        mPaintText.setStrokeWidth(1);


        mYMaxTextRect = new Rect();
        mXMaxTextRect = new Rect();
        mPaintText.getTextBounds(Integer.toString(mYAxisList.get(mYAxisList.size() - 1)), 0, Integer.toString(mYAxisList.get(mYAxisList.size() - 1)).length(), mYMaxTextRect);
        mPaintText.getTextBounds(mXAxisList.get(mXAxisList.size() - 1), 0, mXAxisList.get(mXAxisList.size() - 1).length(), mXMaxTextRect);
        //绘制的刻度文字的最大值所占的宽高
        mMaxTextWidth = mYMaxTextRect.width() > mXMaxTextRect.width() ? mYMaxTextRect.width() : mXMaxTextRect.width();
        mMaxTextHeight = mYMaxTextRect.height() > mXMaxTextRect.height() ? mYMaxTextRect.height() : mXMaxTextRect.height();


        //指定绘制的起始位置
        startX = mMaxTextWidth + textPadinng + mKeduWidth;
        //坐标原点Y的位置(+1的原因:X轴画笔的宽度为2 ; +DP2PX.dip2px(mContext, 5)原因:为刻度文字所占的超出的高度 )——>解决曲线画到最大刻度值时,显示高度不够,曲线显示扁扁的问题
        startY = yAxisSpace * (mYAxisList.size() - 1) + mMaxTextHeight;

        if (mYAxisList.size() >= 2) {
            yIncreaseValue = mYAxisList.get(1) - mYAxisList.get(0);
        }
        //X轴绘制距离
        mXAxisMaxValue = (mDatas.size() - 1) * xAxisSpace;
        //Y轴绘制距离
        mYAxisMaxValue = (mYAxisList.size() - 1) * yAxisSpace;

        //坐标起始点Y轴高度=(startY+mKeduWidth)  下方文字所占高度= DP2PX.dip2px(mContext, keduTextSize)
        int viewHeight = startY + 2 * mKeduWidth + DP2PX.dip2px(mContext, keduTextSize);
        //viewHeight=121
        Log.e("TAG", "viewHeight=" + viewHeight);
    }

    /**
     * 根据传入的数据,确定绘制的点
     *
     * @return
     */
    private Point[] initPoint() {
        Point[] points = new Point[mDatas.size()];
        for (int i = 0; i < mDatas.size(); i++) {
            Integer ybean = mDatas.get(i);
            int drawHeight = (int) (startY * 1.0 - (ybean * yAxisSpace * 1.0 / yIncreaseValue));
            int startx = startX + xAxisSpace * i;
            points[i] = new Point(startx, drawHeight);
        }
        Log.e("TAG", "startX=" + startX + "---startY=" + startY);
        return points;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        mPoints = initPoint();

        for (int i = 0; i < mYAxisList.size(); i++) {
            //Y轴方向递增的高度
            int yAxisHeight = startY - yAxisSpace * i;
            //绘制X轴和上方横线
            canvas.drawLine(startX - mKeduWidth, yAxisHeight, startX + (mDatas.size() - 1) * xAxisSpace, yAxisHeight, mXAxisLinePaint);
            //绘制左边Y轴刻度线
//                canvas.drawLine(startX, yAxisHeight, startX - mKeduWidth, yAxisHeight, mAxisPaint);
            //绘制文字时,Y轴方向递增的高度
            int yTextHeight = startY - yAxisSpace * i;
            //绘制Y轴刻度旁边的刻度文字值,10为刻度线与文字的间距
            mPaintText.setTextAlign(Paint.Align.RIGHT);
            canvas.drawText(mYAxisList.get(i) + "", (startX - mKeduWidth) - textPadinng, yTextHeight, mPaintText);
        }
        //绘制Y轴
        canvas.drawLine(startX, startY, startX, startY - mYAxisMaxValue, mAxisPaint);

        //绘制X轴下面显示的文字
        for (int i = 0; i < mXAxisList.size(); i++) {
            int xTextWidth = startX + xAxisSpace * i - mKeduWidth;
            //设置从起点位置的左边对齐绘制文字
            mPaintText.setTextAlign(Paint.Align.LEFT);
            Rect rect = new Rect();
            mPaintText.getTextBounds(mXAxisList.get(i), 0, mXAxisList.get(i).length(), rect);
            canvas.drawText(mXAxisList.get(i), startX - rect.width() / 2 + xAxisSpace * i, startY + rect.height() + textPadinng, mPaintText);
        }
        //连接所有的数据点,画曲线

        if (isCurve) {
            //画曲线
            drawScrollLine(canvas);
        } else {
            //画折线
            drawLine(canvas);
        }
    }

    /**
     * 绘制曲线-曲线图
     *
     * @param canvas
     */
    private void drawScrollLine(Canvas canvas) {
        Point startp;
        Point endp;
        for (int i = 0; i < mPoints.length - 1; i++) {
            startp = mPoints[i];
            endp = mPoints[i + 1];
            int wt = (startp.x + endp.x) / 2;
            Point p3 = new Point();
            Point p4 = new Point();
            p3.y = startp.y;
            p3.x = wt;
            p4.y = endp.y;
            p4.x = wt;
            Path path = new Path();
            path.moveTo(startp.x, startp.y);
            path.cubicTo(p3.x, p3.y, p4.x, p4.y, endp.x, endp.y);
            canvas.drawPath(path, mPaint);
        }
    }

    /**
     * 绘制直线-折线图
     *
     * @param canvas
     */
    private void drawLine(Canvas canvas) {
        Point startp;
        Point endp;
        for (int i = 0; i < mPoints.length - 1; i++) {
            startp = mPoints[i];
            endp = mPoints[i + 1];
            canvas.drawLine(startp.x, startp.y, endp.x, endp.y, mPaint);
        }
    }

    private void initData() {
        //外界传入的数据,即为绘制曲线的每个点
        mDatas.add(0);
        mDatas.add(10);
        mDatas.add(5);
        mDatas.add(20);
        mDatas.add(15);

        int[] mYAxisData = new int[]{0, 10, 20, 30, 40};
        for (int i = 0; i < mYAxisData.length; i++) {
            mYAxisList.add(mYAxisData[i]);
        }

        //X轴数据
        mXAxisList.add("01月");
        mXAxisList.add("02月");
        mXAxisList.add("03月");
        mXAxisList.add("04月");
        mXAxisList.add("05月");
    }

    /**
     * 传入数据,重新绘制图表
     *
     * @param datas
     * @param yAxisData
     */
    public void updateData(List<Integer> datas, List<String> xAxisData, List<Integer> yAxisData) {
        this.mDatas = datas;
        this.mXAxisList = xAxisData;
        this.mYAxisList = yAxisData;
        initView();
        postInvalidate();
    }
}

参考学习:
Android中moveTo、lineTo、quadTo、cubicTo、arcTo详解

相关文章
|
10天前
|
Android开发 开发者
安卓应用开发中的自定义视图
【9月更文挑战第37天】在安卓开发的海洋中,自定义视图犹如一座座小岛,等待着勇敢的探索者去发现其独特之处。本文将带领你踏上这段旅程,从浅滩走向深海,逐步揭开自定义视图的神秘面纱。
26 3
|
3天前
|
数据可视化 Android开发 开发者
安卓应用开发中的自定义View组件
【10月更文挑战第5天】在安卓应用开发中,自定义View组件是提升用户交互体验的利器。本篇将深入探讨如何从零开始创建自定义View,包括设计理念、实现步骤以及性能优化技巧,帮助开发者打造流畅且富有创意的用户界面。
15 0
|
4天前
|
XML 前端开发 Android开发
Android View的绘制流程和原理详细解说
Android View的绘制流程和原理详细解说
14 3
|
3天前
|
XML 前端开发 Java
安卓应用开发中的自定义View组件
【10月更文挑战第5天】自定义View是安卓应用开发的一块基石,它为开发者提供了无限的可能。通过掌握其原理和实现方法,可以创造出既美观又实用的用户界面。本文将引导你了解自定义View的创建过程,包括绘制技巧、事件处理以及性能优化等关键步骤。
|
28天前
|
Android开发 开发者
安卓开发中的自定义视图:从入门到精通
【9月更文挑战第19天】在安卓开发的广阔天地中,自定义视图是一块充满魔力的土地。它不仅仅是代码的堆砌,更是艺术与科技的完美结合。通过掌握自定义视图,开发者能够打破常规,创造出独一无二的用户界面。本文将带你走进自定义视图的世界,从基础概念到实战应用,一步步展示如何用代码绘出心中的蓝图。无论你是初学者还是有经验的开发者,这篇文章都将为你打开一扇通往创意和效率的大门。让我们一起探索自定义视图的秘密,将你的应用打造成一件艺术品吧!
52 10
|
1月前
|
XML 编解码 Android开发
安卓开发中的自定义视图控件
【9月更文挑战第14天】在安卓开发中,自定义视图控件是一种高级技巧,它可以让开发者根据项目需求创建出独特的用户界面元素。本文将通过一个简单示例,引导你了解如何在安卓项目中实现自定义视图控件,包括创建自定义控件类、处理绘制逻辑以及响应用户交互。无论你是初学者还是有经验的开发者,这篇文章都会为你提供有价值的见解和技巧。
35 3
|
2月前
|
XML 前端开发 Android开发
Android面试高频知识点(3) 详解Android View的绘制流程
View的绘制和事件处理是两个重要的主题,上一篇《图解 Android事件分发机制》已经把事件的分发机制讲得比较详细了,这一篇是针对View的绘制,View的绘制如果你有所了解,基本分为measure、layout、draw 过程,其中比较难理解就是measure过程,所以本篇文章大幅笔地分析measure过程,相对讲得比较详细,文章也比较长,如果你对View的绘制还不是很懂,对measure过程掌握得不是很深刻,那么耐心点,看完这篇文章,相信你会有所收获的。
95 2
|
3月前
|
消息中间件 前端开发 Android开发
Android面试题自定义View之Window、ViewRootImpl和View的三大流程
Android开发中,View的三大核心流程包括measure(测量)、layout(布局)和draw(绘制)。MeasureSpec类在测量过程中起到关键作用,它结合尺寸大小和模式(EXACTLY、AT_MOST、UNSPECIFIED)来指定View应如何测量。onMeasure方法用于自定义View的测量,布局阶段,ViewGroup调用onLayout确定子元素位置,而draw阶段按照特定顺序绘制背景、内容、子元素和装饰。整个流程始于ViewRootImpl的performTraversals,该方法触发测量、布局和绘制。
66 0