android动态绘制自定义折线图(不使用第三方)

简介

很多软件中都会展示统计数据给用户,方式就是各种统计图,柱状图,扇形图,折线图等等。当然现在有很多第三方依赖很容易就可以实现上面说的各种图,此篇的目的就是介绍如何自己动手做一个简单的折线图(注释挺详细,话就不多了)。

效果图

思路

  1. 自定义属性
  2. 数据提供
  3. 绘制xy轴线,箭头
  4. 绘制xy轴刻度,文字
  5. 绘制折线
  6. 绘制折线上点位圆圈
  7. 绘制选中弹出框和x轴文字选中框
  8. x轴滑动

自定义属性

attrs中创建需要的属性

  <!-- xy轴线颜色 -->
    <attr name="xylinecolor" format="color" />
    <!-- xy轴线的宽度 -->
    <attr name="xylinewidth" format="dimension" />
    <!-- xy轴上的文字颜色 -->
    <attr name="xytextcolor" format="color" />
    <!-- xy轴上的文字大小 -->
    <attr name="xytextsize" format="dimension" />
    <!-- 折线的颜色 -->
    <attr name="linecolor" format="color" />
    <!-- x轴坐标点间距 -->
    <attr name="interval" format="dimension" />
    <!-- 折线点位信息颜色 -->
    <attr name="contentcolor" format="color" />
    <!-- 背景颜色 -->
    <attr name="bgcolor" format="color" />
    <!--滑动后手抬起时,折线是否跟随初速度左右滑动-->
    <attr name="isScroll" format="boolean" />
    <declare-styleable name="brokenLineView">
        <attr name="xylinecolor" />
        <attr name="xylinewidth" />
        <attr name="xytextcolor" />
        <attr name="xytextsize" />
        <attr name="linecolor" />
        <attr name="contentcolor" />
        <attr name="interval" />
        <attr name="bgcolor" />
        <attr name="isScroll" />
    </declare-styleable>

在自定义view类中初始化

/**
     * 初始化自定义属性
     */
    private void initPar(Context context, AttributeSet attrs, int defStyleAttr) {
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.brokenLineView, defStyleAttr, 0);
        int count = array.getIndexCount();
        for (int i = 0; i < count; i++) {
            int attr = array.getIndex(i);
            switch (attr) {
                case R.styleable.brokenLineView_xylinecolor://xy轴线颜色
                    xylinecolor = array.getColor(attr, xylinecolor);
                    break;
                case R.styleable.brokenLineView_xylinewidth://xy轴线的宽度
                    xylinewidth = (int) array.getDimension(attr, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, xylinewidth, getResources().getDisplayMetrics()));
                    break;
                case R.styleable.brokenLineView_xytextcolor://xy轴上的文字颜色
                    xytextcolor = array.getColor(attr, xytextcolor);
                    break;
                case R.styleable.brokenLineView_xytextsize://xy轴上的文字大小
                    xytextsize = (int) array.getDimension(attr, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, xytextsize, getResources().getDisplayMetrics()));
                    break;
                case R.styleable.brokenLineView_linecolor://折线的颜色
                    linecolor = array.getColor(attr, linecolor);
                    break;
                case R.styleable.brokenLineView_contentcolor://折线点位信息颜色
                    contentcolor = array.getColor(attr, contentcolor);
                    break;

                case R.styleable.brokenLineView_interval://x轴坐标点间距
                    interval = (int) array.getDimension(attr, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, interval, getResources().getDisplayMetrics()));
                    break;
                case R.styleable.brokenLineView_bgcolor: //背景颜色
                    bgcolor = array.getColor(attr, bgcolor);
                    break;
                case R.styleable.brokenLineView_isScroll://滑动后手抬起时,折线是否跟随初速度左右滑动
                    isScroll = array.getBoolean(attr, isScroll);
                    break;
            }
        }
        array.recycle();

    }

数据提供

数据使用三个list分别为x轴的数据,y轴数据,折线数据

    /**
     * x轴坐标对应的数据
     */
    private List<BrokenLineView.XValue> xValues = new ArrayList<>();
    /**
     * y轴坐标对应的数据
     */
    private List<BrokenLineView.YValue> yValues = new ArrayList<>();
    /**
     * 折线对应的数据
     */
    private List<BrokenLineView.LineValue> lineValues = new ArrayList<>();

绘制xy轴线,箭头

设置大小最好是dp转成px。

        //绘制Y轴线
        canvas.drawLine(xOrigin - xylinewidth / 2, 0, xOrigin - xylinewidth / 2, yOrigin, xyPaint);
        //绘制y轴箭头
        xyPaint.setStyle(Paint.Style.STROKE);
        Path path = new Path();
        path.moveTo(xOrigin - xylinewidth / 2 - dpToPx(5), dpToPx(12));
        path.lineTo(xOrigin - xylinewidth / 2, xylinewidth / 2);
        path.lineTo(xOrigin - xylinewidth / 2 + dpToPx(5), dpToPx(12));
        canvas.drawPath(path, xyPaint);



//绘制X轴坐标
        canvas.drawLine(xOrigin, yOrigin + xylinewidth / 2, width, yOrigin + xylinewidth / 2, xyPaint);
        //绘制x轴箭头
        xyPaint.setStyle(Paint.Style.STROKE);
        path = new Path();
        //整个X轴的长度
        float xLength = xInit + interval * (xValues.size() - 1) + (width - xOrigin) * 0.1f;
        if (xLength < width)
            xLength = width;
        path.moveTo(xLength - dpToPx(12), yOrigin + xylinewidth / 2 - dpToPx(5));
        path.lineTo(xLength - xylinewidth / 2, yOrigin + xylinewidth / 2);
        path.lineTo(xLength - dpToPx(12), yOrigin + xylinewidth / 2 + dpToPx(5));
        canvas.drawPath(path, xyPaint);

绘制xy轴刻度,文字

 //绘制y轴刻度
        int yLength = (int) (yOrigin * (1 - 0.1f) / (yValues.size() - 1));//y轴上面空出10%,计算出y轴刻度间距
        for (int i = 0; i < yValues.size(); i++) {
            //绘制Y轴刻度
            canvas.drawLine(xOrigin, yOrigin - yLength * i + xylinewidth / 2, xOrigin , yOrigin - yLength * i + xylinewidth / 2, xyPaint);
            xyTextPaint.setColor(xytextcolor);
            //绘制Y轴文本
            String text = yValues.get(i).value;
            Rect rect = getTextBounds(text, xyTextPaint);
            canvas.drawText(text, 0, text.length(), xOrigin - xylinewidth - dpToPx(2) - rect.width(), yOrigin - yLength * i + rect.height() / 2, xyTextPaint);
        }

      //绘制x轴刻度
        for (int i = 0; i < xValues.size(); i++) {
            float x = xInit + interval * i;
            if (x >= xOrigin) {//只绘制从原点开始的区域
                xyTextPaint.setColor(xytextcolor);
                canvas.drawLine(x, yOrigin, x, yOrigin , xyPaint);
                //绘制X轴文本
                String text = xValues.get(i).value;
                Rect rect = getTextBounds(text, xyTextPaint);
                if (i == selectIndex - 1) {
                    //绘制x轴选中文字和框
                    xyTextPaint.setStyle(Paint.Style.FILL);
                    xyTextPaint.setColor(contentcolor);
                    canvas.drawRoundRect(new RectF(x - xValueRect.width() / 2 - dpToPx(3), yOrigin + xylinewidth + dpToPx(1), x + xValueRect.width() / 2 + dpToPx(3), yOrigin + xylinewidth + dpToPx(2) + xValueRect.height() + dpToPx(2)), dpToPx(2), dpToPx(2), xyTextPaint);
                    xyTextPaint.setColor(Color.WHITE);
                    canvas.drawText(text, 0, text.length(), x - rect.width() / 2, yOrigin + xylinewidth + dpToPx(2) + rect.height(), xyTextPaint);
                } else {
                    canvas.drawText(text, 0, text.length(), x - rect.width() / 2, yOrigin + xylinewidth + dpToPx(2) + rect.height(), xyTextPaint);
                }
            }
        }

绘制折线

/**
     * 绘制折线
     */
    private void drawBrokenLine(Canvas canvas) {
        linePaint.setStyle(Paint.Style.STROKE);
        linePaint.setColor(linecolor);
        //绘制折线
        Path path = new Path();
        float x = xInit + interval * 0;
        float y = (float) (yOrigin - yOrigin * (1 - 0.1f) * lineValues.get(0).num / yValues.get(yValues.size() - 1).num);
        path.moveTo(x, y);
        for (int i = 1; i < xValues.size(); i++) {
            x = xInit + interval * i;
            y = (float) (yOrigin - yOrigin * (1 - 0.1f) * lineValues.get(i).num / yValues.get(yValues.size() - 1).num);
            if (x<0)x=0;
            if (y<0)y=0;
            path.lineTo(x, y);
        }
        canvas.drawPath(path, linePaint);
    }

绘制折线上点位圆圈

 /**
     * 绘制折线对应的点位圆圈
     */
    private void drawBrokenPoint(Canvas canvas) {
        float dp2 = dpToPx(2);
        float dp4 = dpToPx(4);
        float dp7 = dpToPx(7);
        //绘制节点对应的原点
        for (int i = 0; i < xValues.size(); i++) {
            float x = xInit + interval * i;
            float y = (float) (yOrigin - yOrigin * (1 - 0.1f) * lineValues.get(i).num / yValues.get(yValues.size() - 1).num);
            //绘制选中的点
            if (i == selectIndex - 1) {
                linePaint.setStyle(Paint.Style.FILL);
                linePaint.setColor(contentcolor);
                canvas.drawCircle(x, y, dp7, linePaint);
                drawFloatTextBox(canvas, x, y - dp7, lineValues.get(i).value);
            }
            //绘制折线点
            linePaint.setStyle(Paint.Style.FILL);
            linePaint.setColor(Color.WHITE);
            canvas.drawCircle(x, y, dp2, linePaint);
            linePaint.setStyle(Paint.Style.STROKE);
            linePaint.setColor(linecolor);
            canvas.drawCircle(x, y, dp2, linePaint);

        }
    }

绘制选中弹出框和x轴文字选中框

       if (i == selectIndex - 1) {
                    //绘制x轴选中文字和框
                    xyTextPaint.setStyle(Paint.Style.FILL);
                    xyTextPaint.setColor(contentcolor);
                    canvas.drawRoundRect(new RectF(x - xValueRect.width() / 2 - dpToPx(3), yOrigin + xylinewidth + dpToPx(1), x + xValueRect.width() / 2 + dpToPx(3), yOrigin + xylinewidth + dpToPx(2) + xValueRect.height() + dpToPx(2)), dpToPx(2), dpToPx(2), xyTextPaint);
                    xyTextPaint.setColor(Color.WHITE);
                    canvas.drawText(text, 0, text.length(), x - rect.width() / 2, yOrigin + xylinewidth + dpToPx(2) + rect.height(), xyTextPaint);
                } 
   if (i == selectIndex - 1) {
                linePaint.setStyle(Paint.Style.FILL);
                linePaint.setColor(contentcolor);
                canvas.drawCircle(x, y, dp7, linePaint);
                drawFloatTextBox(canvas, x, y - dp7, lineValues.get(i).value);
            }    


/**
     * 绘制点位信息弹出框
     */
    private void drawFloatTextBox(Canvas canvas, float x, float y, String text) {
        int dp6 = dpToPx(6);
        int dp45 = dpToPx(45);
        Path path = new Path();
        path.moveTo(x, y);
        path.lineTo(x - dp6, y - dp6);
        path.lineTo(x - dp45, y - dp6);
        path.lineTo(x - dp45, y - dp6 - dp45);
        path.lineTo(x + dp45, y - dp6 - dp45);
        path.lineTo(x + dp45, y - dp6);
        path.lineTo(x + dp6, y - dp6);
        path.lineTo(x, y);
        canvas.drawPath(path, linePaint);
        //点位信息文字
        linePaint.setColor(Color.WHITE);
        linePaint.setTextSize(spToPx(14));
        Rect rect = getTextBounds(text + "", linePaint);
        canvas.drawText(text + "", x - rect.width() / 2, y - dp6 - (dp45 - rect.height()) / 2, linePaint);
    }

x轴滑动

/**
     * 滑动x轴
     */
    private void scroll() {
        if (!isScroll)
            return;
        final float velocity = getVelocity();//得到当前速度
        float scrollLength = maxXInit - minXInit;
        if (Math.abs(velocity) < 10000)//最大速度
            scrollLength = (maxXInit - minXInit) * Math.abs(velocity) / 10000;
        ValueAnimator animator = ValueAnimator.ofFloat(0, scrollLength);
        animator.setDuration((long) (scrollLength / (maxXInit - minXInit) * 1000));//滑动时间最大1000毫秒
        animator.setInterpolator(new DecelerateInterpolator());
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                float lineValues = (float) valueAnimator.getAnimatedValue();
                if (velocity < 0 && xInit > minXInit) {//向左滑动
                    if (xInit - lineValues <= minXInit)
                        xInit = minXInit;
                    else
                        xInit = xInit - lineValues;
                } else if (velocity > 0 && xInit < maxXInit) {//向右滑动
                    if (xInit + lineValues >= maxXInit)
                        xInit = maxXInit;
                    else
                        xInit = xInit + lineValues;
                }
                invalidate();
            }
        });
        animator.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animator) {
                isScrolling = true;//正在滑动
            }

            @Override
            public void onAnimationEnd(Animator animator) {
                isScrolling = false;
            }

            @Override
            public void onAnimationCancel(Animator animator) {
                isScrolling = false;
            }

            @Override
            public void onAnimationRepeat(Animator animator) {

            }
        });
        animator.start();

    }

完整类如下

public class BrokenLineView extends View {
    //xy坐标轴颜色
    private int xylinecolor = 0;
    /**
     * xy坐标轴线的宽度
     */
    private int xylinewidth = 1;
    /**
     * xy坐标轴文字颜色
     */
    private int xytextcolor = 0;
    /**
     * xy坐标轴文字大小
     */
    private int xytextsize = spToPx(12);
    /**
     * 折线图中折线的颜色
     */
    private int linecolor = 0;
    /**
     * 折线图中点位信息颜色
     */
    private int contentcolor = 0;

    /**
     * x轴各个坐标点水平间距
     */
    private int interval = dpToPx(50);
    /**
     * 背景颜色
     */
    private int bgcolor = 0;
    /**
     * 是否在ACTION_UP时,根据速度进行自滑动,没有要求,建议关闭,过于占用GPU
     */
    private boolean isScroll = false;
    /**
     * 绘制XY轴坐标对应的画笔
     */
    private Paint xyPaint;
    /**
     * 绘制XY轴的文本对应的画笔
     */
    private Paint xyTextPaint;
    /**
     * 画折线对应的画笔
     */
    private Paint linePaint;
    private int width;
    private int height;
    /**
     * x轴的原点坐标
     */
    private int xOrigin;
    /**
     * y轴的原点坐标
     */
    private int yOrigin;
    /**
     * 第一个点X的坐标
     */
    private float xInit;
    /**
     * 第一个点对应的最大Y坐标
     */
    private float maxXInit;
    /**
     * 第一个点对应的最小X坐标
     */
    private float minXInit;
    /**
     * x轴坐标对应的数据
     */
    private List<XValue> xValues = new ArrayList<>();
    /**
     * y轴坐标对应的数据
     */
    private List<YValue> yValues = new ArrayList<>();
    /**
     * 折线对应的数据
     */
    private List<LineValue> lineValues = new ArrayList<>();
    /**
     * 当前选中点
     */
    private int selectIndex = 1;
    /**
     * X轴刻度文本对应的最大矩形,为了选中时,在x轴文本画的框框大小一致
     */
    private Rect xValueRect;
    /**
     * 速度检测
     */
    private VelocityTracker velocityTracker;

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

    public BrokenLineView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public BrokenLineView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initPar(context, attrs, defStyleAttr);
        initPaint();
    }

    /**
     * 初始化画笔
     */
    private void initPaint() {
        xyPaint = new Paint();
        xyPaint.setAntiAlias(true);
        xyPaint.setStrokeWidth(xylinewidth);
        xyPaint.setStrokeCap(Paint.Cap.ROUND);
        xyPaint.setColor(xylinecolor);

        xyTextPaint = new Paint();
        xyTextPaint.setAntiAlias(true);
        xyTextPaint.setTextSize(xytextsize);
        xyTextPaint.setStrokeCap(Paint.Cap.ROUND);
        xyTextPaint.setColor(xytextcolor);
        xyTextPaint.setStyle(Paint.Style.STROKE);

        linePaint = new Paint();
        linePaint.setAntiAlias(true);
        linePaint.setStrokeWidth(xylinewidth);
        linePaint.setStrokeCap(Paint.Cap.ROUND);
        linePaint.setColor(linecolor);
        linePaint.setStyle(Paint.Style.STROKE);
    }

    /**
     * 初始化自定义属性
     */
    private void initPar(Context context, AttributeSet attrs, int defStyleAttr) {
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.brokenLineView, defStyleAttr, 0);
        int count = array.getIndexCount();
        for (int i = 0; i < count; i++) {
            int attr = array.getIndex(i);
            switch (attr) {
                case R.styleable.brokenLineView_xylinecolor://xy轴线颜色
                    xylinecolor = array.getColor(attr, xylinecolor);
                    break;
                case R.styleable.brokenLineView_xylinewidth://xy轴线的宽度
                    xylinewidth = (int) array.getDimension(attr, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, xylinewidth, getResources().getDisplayMetrics()));
                    break;
                case R.styleable.brokenLineView_xytextcolor://xy轴上的文字颜色
                    xytextcolor = array.getColor(attr, xytextcolor);
                    break;
                case R.styleable.brokenLineView_xytextsize://xy轴上的文字大小
                    xytextsize = (int) array.getDimension(attr, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, xytextsize, getResources().getDisplayMetrics()));
                    break;
                case R.styleable.brokenLineView_linecolor://折线的颜色
                    linecolor = array.getColor(attr, linecolor);
                    break;
                case R.styleable.brokenLineView_contentcolor://折线点位信息颜色
                    contentcolor = array.getColor(attr, contentcolor);
                    break;

                case R.styleable.brokenLineView_interval://x轴坐标点间距
                    interval = (int) array.getDimension(attr, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, interval, getResources().getDisplayMetrics()));
                    break;
                case R.styleable.brokenLineView_bgcolor: //背景颜色
                    bgcolor = array.getColor(attr, bgcolor);
                    break;
                case R.styleable.brokenLineView_isScroll://滑动后手抬起时,折线是否跟随初速度左右滑动
                    isScroll = array.getBoolean(attr, isScroll);
                    break;
            }
        }
        array.recycle();

    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        if (changed) {
            //得到宽度高度
            width = getWidth();
            height = getHeight();
            //Y轴字宽度
            float textYWdith = getTextBounds("000", xyTextPaint).width();
            for (int i = 0; i < yValues.size(); i++) {//求取y轴文本最大的宽度
                float temp = getTextBounds(yValues.get(i).value, xyTextPaint).width();
                if (temp > textYWdith)
                    textYWdith = temp;
            }
            int dp2 = dpToPx(2);
            int dp3 = dpToPx(3);
            xOrigin = (int) (dp2 + textYWdith + dp2 + xylinewidth);//dp2是y轴文本距离左边,以及距离y轴的距离
            xValueRect = getTextBounds("000", xyTextPaint);
            //X轴字高度
            float textXHeight = xValueRect.height();
            for (int i = 0; i < xValues.size(); i++) {//求取x轴文本最大的高度
                Rect rect = getTextBounds(xValues.get(i).value + "", xyTextPaint);
                if (rect.height() > textXHeight)
                    textXHeight = rect.height();
                if (rect.width() > xValueRect.width())
                    xValueRect = rect;
            }

            yOrigin = (int) (height - dp2 - textXHeight - dp3 - xylinewidth);//dp3是x轴文本距离底边,dp2是x轴文本距离x轴的距离
            xInit = interval + xOrigin;
            minXInit = width - (width - xOrigin) * 0.1f - interval * (xValues.size() - 1);//减去0.1f是因为最后一个X周刻度距离右边的长度为X轴可见长度的10%
            maxXInit = xInit;
        }
        super.onLayout(changed, left, top, right, bottom);
    }

    @Override
    protected void onDraw(Canvas canvas) {
//        super.onDraw(canvas);
        canvas.drawColor(bgcolor);
        drawXY(canvas);
        drawBrokenLineAndPoint(canvas);
    }

    /**
     * 绘制折线和折线点
     */
    private void drawBrokenLineAndPoint(Canvas canvas) {
        if (xValues.size() <= 0)
            return;
        //设置显示折线的图层
        int layer = canvas.saveLayer(0, 0, width, height, null, Canvas.ALL_SAVE_FLAG);
        drawBrokenLine(canvas);
        drawBrokenPoint(canvas);
        // 将折线超出x轴坐标的部分截取掉
        linePaint.setStyle(Paint.Style.FILL);
        linePaint.setColor(bgcolor);
        linePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
        RectF rectF = new RectF(0, 0, xOrigin, height);
        canvas.drawRect(rectF, linePaint);
        linePaint.setXfermode(null);
        //保存图层
        canvas.restoreToCount(layer);
    }

    /**
     * 绘制折线对应的点位圆圈
     */
    private void drawBrokenPoint(Canvas canvas) {
        float dp2 = dpToPx(2);
        float dp4 = dpToPx(4);
        float dp7 = dpToPx(7);
        //绘制节点对应的原点
        for (int i = 0; i < xValues.size(); i++) {
            float x = xInit + interval * i;
            float y = (float) (yOrigin - yOrigin * (1 - 0.1f) * lineValues.get(i).num / yValues.get(yValues.size() - 1).num);
            //绘制折线点
            linePaint.setStyle(Paint.Style.FILL);
            linePaint.setColor(Color.WHITE);
            canvas.drawCircle(x, y, dp2, linePaint);
            linePaint.setStyle(Paint.Style.STROKE);
            linePaint.setColor(linecolor);
            canvas.drawCircle(x, y, dp2, linePaint);
            //绘制选中的点
            if (i == selectIndex - 1) {
                linePaint.setStyle(Paint.Style.FILL);
                linePaint.setColor(contentcolor);
                canvas.drawCircle(x, y, dp7, linePaint);
                drawFloatTextBox(canvas, x, y - dp7, lineValues.get(i).value);
            }
        }
    }

    /**
     * 绘制点位信息弹出框
     */
    private void drawFloatTextBox(Canvas canvas, float x, float y, String text) {
        int dp6 = dpToPx(6);
        int dp45 = dpToPx(45);
        Path path = new Path();
        path.moveTo(x, y);
        path.lineTo(x - dp6, y - dp6);
        path.lineTo(x - dp45, y - dp6);
        path.lineTo(x - dp45, y - dp6 - dp45);
        path.lineTo(x + dp45, y - dp6 - dp45);
        path.lineTo(x + dp45, y - dp6);
        path.lineTo(x + dp6, y - dp6);
        path.lineTo(x, y);
        canvas.drawPath(path, linePaint);
        //点位信息文字
        linePaint.setColor(Color.WHITE);
        linePaint.setTextSize(spToPx(14));
        Rect rect = getTextBounds(text + "", linePaint);
        canvas.drawText(text + "", x - rect.width() / 2, y - dp6 - (dp45 - rect.height()) / 2, linePaint);
    }

    /**
     * 绘制折线
     */
    private void drawBrokenLine(Canvas canvas) {
        linePaint.setStyle(Paint.Style.STROKE);
        linePaint.setColor(linecolor);
        //绘制折线
        Path path = new Path();
        float x = xInit + interval * 0;
        float y = (float) (yOrigin - yOrigin * (1 - 0.1f) * lineValues.get(0).num / yValues.get(yValues.size() - 1).num);
        path.moveTo(x, y);
        for (int i = 1; i < xValues.size(); i++) {
            x = xInit + interval * i;
            y = (float) (yOrigin - yOrigin * (1 - 0.1f) * lineValues.get(i).num / yValues.get(yValues.size() - 1).num);
            if (x < 0) x = 0;
            if (y < 0) y = 0;
            path.lineTo(x, y);
        }
        canvas.drawPath(path, linePaint);
    }

    /**
     * 绘制XY坐标
     *
     * @param canvas
     */
    private void drawXY(Canvas canvas) {
        //绘制Y轴线
        canvas.drawLine(xOrigin - xylinewidth / 2, 0, xOrigin - xylinewidth / 2, yOrigin, xyPaint);
        //绘制y轴箭头
        xyPaint.setStyle(Paint.Style.STROKE);
        Path path = new Path();
        path.moveTo(xOrigin - xylinewidth / 2 - dpToPx(5), dpToPx(12));
        path.lineTo(xOrigin - xylinewidth / 2, xylinewidth / 2);
        path.lineTo(xOrigin - xylinewidth / 2 + dpToPx(5), dpToPx(12));
        canvas.drawPath(path, xyPaint);
        //绘制y轴刻度
        int yLength = (int) (yOrigin * (1 - 0.1f) / (yValues.size() - 1));//y轴上面空出10%,计算出y轴刻度间距
        for (int i = 0; i < yValues.size(); i++) {
            //绘制Y轴刻度
            canvas.drawLine(xOrigin, yOrigin - yLength * i + xylinewidth / 2, xOrigin, yOrigin - yLength * i + xylinewidth / 2, xyPaint);
            xyTextPaint.setColor(xytextcolor);
            //绘制Y轴文本
            String text = yValues.get(i).value;
            Rect rect = getTextBounds(text, xyTextPaint);
            canvas.drawText(text, 0, text.length(), xOrigin - xylinewidth - dpToPx(2) - rect.width(), yOrigin - yLength * i + rect.height() / 2, xyTextPaint);
        }
        //绘制X轴坐标
        canvas.drawLine(xOrigin, yOrigin + xylinewidth / 2, width, yOrigin + xylinewidth / 2, xyPaint);
        //绘制x轴箭头
        xyPaint.setStyle(Paint.Style.STROKE);
        path = new Path();
        //整个X轴的长度
        float xLength = xInit + interval * (xValues.size() - 1) + (width - xOrigin) * 0.1f;
        if (xLength < width)
            xLength = width;
        path.moveTo(xLength - dpToPx(12), yOrigin + xylinewidth / 2 - dpToPx(5));
        path.lineTo(xLength - xylinewidth / 2, yOrigin + xylinewidth / 2);
        path.lineTo(xLength - dpToPx(12), yOrigin + xylinewidth / 2 + dpToPx(5));
        canvas.drawPath(path, xyPaint);
        //绘制x轴刻度
        for (int i = 0; i < xValues.size(); i++) {
            float x = xInit + interval * i;
            if (x >= xOrigin) {//只绘制从原点开始的区域
                xyTextPaint.setColor(xytextcolor);
                canvas.drawLine(x, yOrigin, x, yOrigin, xyPaint);
                //绘制X轴文本
                String text = xValues.get(i).value;
                Rect rect = getTextBounds(text, xyTextPaint);
                if (i == selectIndex - 1) {
                    //绘制x轴选中文字和框
                    xyTextPaint.setStyle(Paint.Style.FILL);
                    xyTextPaint.setColor(contentcolor);
                    canvas.drawRoundRect(new RectF(x - xValueRect.width() / 2 - dpToPx(3), yOrigin + xylinewidth + dpToPx(1), x + xValueRect.width() / 2 + dpToPx(3), yOrigin + xylinewidth + dpToPx(2) + xValueRect.height() + dpToPx(2)), dpToPx(2), dpToPx(2), xyTextPaint);
                    xyTextPaint.setColor(Color.WHITE);
                    canvas.drawText(text, 0, text.length(), x - rect.width() / 2, yOrigin + xylinewidth + dpToPx(2) + rect.height(), xyTextPaint);
                } else {
                    canvas.drawText(text, 0, text.length(), x - rect.width() / 2, yOrigin + xylinewidth + dpToPx(2) + rect.height(), xyTextPaint);
                }
            }
        }
    }

    private float startX;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (isScrolling)
            return super.onTouchEvent(event);
        this.getParent().requestDisallowInterceptTouchEvent(true);//当该view获得点击事件,就请求父控件不拦截事件
        obtainVelocityTracker(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                startX = event.getX();
                break;
            case MotionEvent.ACTION_MOVE:
                if (interval * xValues.size() > width - xOrigin) {//当期的宽度不足以呈现全部数据
                    float dis = event.getX() - startX;
                    startX = event.getX();
                    if (xInit + dis < minXInit) {
                        xInit = minXInit;
                    } else if (xInit + dis > maxXInit) {
                        xInit = maxXInit;
                    } else {
                        xInit = xInit + dis;
                    }
                    invalidate();
                }
                break;
            case MotionEvent.ACTION_UP:
                clickAction(event);
                scroll();
                this.getParent().requestDisallowInterceptTouchEvent(false);
                recycleVelocityTracker();
                break;
            case MotionEvent.ACTION_CANCEL:
                this.getParent().requestDisallowInterceptTouchEvent(false);
                recycleVelocityTracker();
                break;
        }
        return true;
    }

    //是否正在滑动
    private boolean isScrolling = false;

    /**
     * 滑动x轴
     */
    private void scroll() {
        if (!isScroll)
            return;
        final float velocity = getVelocity();//得到当前速度
        float scrollLength = maxXInit - minXInit;
        if (Math.abs(velocity) < 10000)//最大速度
            scrollLength = (maxXInit - minXInit) * Math.abs(velocity) / 10000;
        ValueAnimator animator = ValueAnimator.ofFloat(0, scrollLength);
        animator.setDuration((long) (scrollLength / (maxXInit - minXInit) * 1000));//滑动时间最大1000毫秒
        animator.setInterpolator(new DecelerateInterpolator());
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                float lineValues = (float) valueAnimator.getAnimatedValue();
                if (velocity < 0 && xInit > minXInit) {//向左滑动
                    if (xInit - lineValues <= minXInit)
                        xInit = minXInit;
                    else
                        xInit = xInit - lineValues;
                } else if (velocity > 0 && xInit < maxXInit) {//向右滑动
                    if (xInit + lineValues >= maxXInit)
                        xInit = maxXInit;
                    else
                        xInit = xInit + lineValues;
                }
                invalidate();
            }
        });
        animator.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animator) {
                isScrolling = true;//正在滑动
            }

            @Override
            public void onAnimationEnd(Animator animator) {
                isScrolling = false;
            }

            @Override
            public void onAnimationCancel(Animator animator) {
                isScrolling = false;
            }

            @Override
            public void onAnimationRepeat(Animator animator) {

            }
        });
        animator.start();

    }

    /**
     * 获取速度
     */
    private float getVelocity() {
        if (velocityTracker != null) {
            velocityTracker.computeCurrentVelocity(1000);
            return velocityTracker.getXVelocity();
        }
        return 0;
    }

    /**
     * 点击X轴坐标或者折线节点
     *
     * @param event
     */
    private void clickAction(MotionEvent event) {
        int dp8 = dpToPx(8);
        float eventX = event.getX();
        float eventY = event.getY();
        for (int i = 0; i < xValues.size(); i++) {
            //节点
            float x = xInit + interval * i;
            float y = (float) (yOrigin - yOrigin * (1 - 0.1f) * lineValues.get(i).num / yValues.get(yValues.size() - 1).num);
            if (eventX >= x - dp8 && eventX <= x + dp8 &&
                    eventY >= y - dp8 && eventY <= y + dp8 && selectIndex != i + 1) {//每个节点周围8dp都是可点击区域
                selectIndex = i + 1;
                invalidate();
                return;
            }
            //X轴刻度
            String text = xValues.get(i).value;
            Rect rect = getTextBounds(text, xyTextPaint);
            x = xInit + interval * i;
            y = yOrigin + xylinewidth + dpToPx(2);
            if (eventX >= x - rect.width() / 2 - dp8 && eventX <= x + rect.width() + dp8 / 2 &&
                    eventY >= y - dp8 && eventY <= y + rect.height() + dp8 && selectIndex != i + 1) {
                selectIndex = i + 1;
                invalidate();
                return;
            }
        }
    }


    /**
     * 获取速度跟踪器
     *
     * @param event
     */
    private void obtainVelocityTracker(MotionEvent event) {
        if (!isScroll)
            return;
        if (velocityTracker == null) {
            velocityTracker = VelocityTracker.obtain();
        }
        velocityTracker.addMovement(event);
    }

    /**
     * 回收速度跟踪器
     */
    private void recycleVelocityTracker() {
        if (velocityTracker != null) {
            velocityTracker.recycle();
            velocityTracker = null;
        }
    }

    public int getSelectIndex() {
        return selectIndex;
    }

    public void setSelectIndex(int selectIndex) {
        this.selectIndex = selectIndex;
        invalidate();
    }

    public void setxValue(List<XValue> xValues) {
        this.xValues = xValues;
    }

    public void setyValue(List<YValue> yValues) {
        this.yValues = yValues;
        invalidate();
    }

    public void setValue(List<LineValue> lineValues) {
        this.lineValues = lineValues;
        invalidate();
    }

    public void setValue(List<LineValue> lineValues, List<XValue> xValues, List<YValue> yValues) {
        this.lineValues = lineValues;
        this.xValues = xValues;
        this.yValues = yValues;
        invalidate();
    }

    public List<XValue> getxValue() {
        return xValues;
    }

    public List<YValue> getyValue() {
        return yValues;
    }

    public List<LineValue> getValue() {
        return lineValues;
    }

    /**
     * 获取丈量文本的矩形
     *
     * @param text
     * @param paint
     * @return
     */
    private Rect getTextBounds(String text, Paint paint) {
        Rect rect = new Rect();
        paint.getTextBounds(text, 0, text.length(), rect);
        return rect;
    }

    /**
     * dp转化成为px
     *
     * @param dp
     * @return
     */
    private int dpToPx(int dp) {
        float density = getContext().getResources().getDisplayMetrics().density;
        return (int) (dp * density + 0.5f * (dp >= 0 ? 1 : -1));
    }

    /**
     * sp转化为px
     *
     * @param sp
     * @return
     */
    private int spToPx(int sp) {
        float scaledDensity = getContext().getResources().getDisplayMetrics().scaledDensity;
        return (int) (scaledDensity * sp + 0.5f * (sp >= 0 ? 1 : -1));
    }

    public static class XValue {
        public double num;
        public String value;
        public XValue(double num, String value) {
            this.num = num;
            this.value = value;
        }
    }
    public static class YValue {
        public double num;
        public String value;
        public YValue(double num, String value) {
            this.num = num;
            this.value = value;
        }
    }
    public static class LineValue {
        //具体数值
        public double num;
        //显示值
        public String value;
        public LineValue(double num, String value) {
            this.num = num;
            this.value = value;
        }
    }
}

使用方法

布局里面添加BrokenLineView

 <test.com.view.BrokenLineView
        android:id="@+id/chartview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginBottom="8dp"
        app:bgcolor="@color/white"
        app:interval="50dp"
        app:isScroll="false"
        app:layout_constraintBottom_toBottomOf="parent"
        app:linecolor="@color/red"
        app:contentcolor="@color/red"
        app:xylinecolor="#494848"
        app:xylinewidth="1.5dp"
        app:xytextcolor="#494848"
        app:xytextsize="15sp"/>

activity里面数据准备

    private void initData() {
        //模拟折线数据
        for (int i = 1; i <= 30; i++){
            BrokenLineView.XValue xValue = new BrokenLineView.XValue(i,i+"号");
            xValues.add(xValue);
            double value =new BigDecimal(Math.random()*10).setScale(2,BigDecimal.ROUND_HALF_UP).doubleValue();
            BrokenLineView.LineValue lineValue = new BrokenLineView.LineValue(value,i+"号点位="+value);
            lineValues.add(lineValue);
        }
        for (int i = 0; i <=10; i++) {
            BrokenLineView.YValue yValue = new BrokenLineView.YValue(i,i+"");
            yValues.add(yValue);
        }
        brokenLineView.setValue(lineValues, xValues, yValues);
    }

    private void initView() {
        brokenLineView = (BrokenLineView) findViewById(R.id.brokenLineView);
    }

总结

到此自定义的折线图就介绍完了,具体细节的地方讲的不是很详细望包涵,如果要求不是太高可以直接复制上述类代码拿来使用,要求高的就不适用了,因为只是一个测试的折线图,功能不是很完善,当然也可以在上面基础修改增加自己需要的东西。

发布了40 篇原创文章 · 获赞 70 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/weixin_35959554/article/details/103999655