Quantcast
Channel: CSDN博客移动开发推荐文章
Viewing all articles
Browse latest Browse all 5930

折线图实现

$
0
0

      这样的图用来做统计最方便了,今天,我们又要摆脱第三方的约束,自己来实现了,是不是很开心,现在就来动手吧。

      本文内容需要读者具备一定的自定义view基础,否则看起来可能比较费力,不过懂的看门道,不懂的可以凑个热闹,能看懂可以自己去改不好的地方,不懂的也可以直接拿来用。
先给个效果图:
这里写图片描述这里写图片描述

      看起来还可以吧,废话不多说,开始我们的绘制。

     首先要确定我们要达成的效果,下面列出来

1:数据的获取
2:折线的绘制
3:X坐标文字的绘制
4:数字的绘制
5:数字的位置判断,防止与折线重合
6:随手指滑动的效果

这样思路清晰了,下面逐个击破

 1    数据的获取
    这恐怕是最简单的一步了,我在里面定义了一个方法,包含三个参数,使用的时候直接传进去,再刷新试图即可,给出方法

    //设置整个坐标上的一些文字和数字
    public void setData(String[] coordinate, String[] number, float extremum) {
        this.coordinate = coordinate;
        this.number = number;
        this.extremum = extremum;
    }

    可以看见传入了三个参数,一个是坐标数组,一个是数字数组,这里用字符数组代替的,再一个就是Y轴上最大值

 2    折线的绘制
    绘制折线肯定是要使用到path,那么就要把path拿来,然后添上路径即可,那么主要就是路径坐标的问题了,X坐标我用的文字宽度加上文字和文字之间距离形成最后的坐标,然后遍历数组就能获得一个个X坐标,Y坐标我添加了一个功能,那就是能设置绘制图的上边距和下边距,其实X坐标我也设置了左边距,具体的使用大家可以去里面更改数值感受一下,如果上边距加上下编剧正好等于控件的高度的话,那就整个图就是一个直线,你懂的。
给出绘制路径代码:

            float deviationY = (height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM;   //这里式子没有简化,是为了方便你们清楚这是怎么一个过程
            float deviationX = movedistance + OFFSETX + txtrect.width() * i + txtpading * i;  //这里式子没有简化,是为了方便你们清楚这是怎么一个过程
            //画出折线图的路径
            path.lineTo(deviationX, deviationY);
            canvas.drawPath(path, linepaint);      //绘制折线

    解释一下,这里的movedistance就是随手指滑动会产生的位移,OFFSETX则是X轴左边距,OFFSETYTOP是Y轴上边距,OFFSETYBOTTOM是Y轴下边距。

 3    X坐标文字的绘制
    文字的绘制比较简单了,主要就是坐标的把握,Y轴没难度,都用问题的高度即可,X轴呢,其实就是刚才绘制折线的X坐标再减去文字宽度的一半即可,给出代码参考:

canvas.drawText(coordinate[i] + "", deviationX - txtrect.width() / 2, height - txtrect.height() / 2, extremumpaint);     //绘制X轴上的坐标

 4    数字的绘制
    数字的绘制相对于文字的绘制就是多了一个Y轴上的变换,当然了,X轴这里要减去的是数字的宽度一半,而不是文字了,高度我们其实用之前折线的高度减去上我们数字的高度的一半即可,给出代码:

canvas.drawText(number[i] + "", deviationX - numberrect.width() / 2, deviationY - numberrect.height() / 2, txtpaint);   //绘制数字

 5    数字的位置判断,防止与折线重合
    这个问题呢我们可以从折线的走势判断解决,那么当前的数字所在的折线坐标会有以下情况:
    1: 中间最高
    2: 左边最高,右边最低
    3: 左边最低,右边最高
    4: 中间最低
就这四种情况,我们可以从Y轴进行判断,判断我提取了成了一个方法,给出代码:

    /*这个函数的作用是判断当前数字坐标的y轴和其左右两边的数字坐标的y进行比较,从而判断数字显示的位置,避免折线和数字重复在一起的情况
     */
    public int judgmentposition(int i) {
        int status = 1;
        if (((height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i - 1]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM) >
                (height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM &&
                (height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM <
                        (height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i + 1]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM) {
            status = 1;
        } else if (((height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i - 1]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM) <
                (height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM &&
                (height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM <
                        (height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i + 1]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM) {
            status = 2;
        } else if (((height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i - 1]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM) >
                (height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM &&
                (height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM >
                        (height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i + 1]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM) {
            status = 3;
        } else {
            status = 4;
        }
        Log.d("statusnumbers", status + "");
        return status;
    }

使用时的代码:

            //下面的绘制要保证文字在合适的位置,想了解的可参考我另一个博客:http://blog.csdn.net/wanxuedong/article/details/69396732
            if (i > 0 && i < number.length - 1) {
                switch (judgmentposition(i)) {
                    case 1:    //中间最高
                        canvas.drawText(number[i] + "", deviationX - numberrect.width() / 2, deviationY - numberrect.height(), txtpaint);   //绘制数字
                        break;
                    case 2:    //左边最高,右边最低
                        canvas.drawText(number[i] + "", deviationX, deviationY - numberrect.height() / 2, txtpaint);   //绘制数字
                        break;
                    case 3:    //左边最低,右边最高
                        canvas.drawText(number[i] + "", deviationX - numberrect.width(), deviationY - numberrect.height() / 2, txtpaint);   //绘制数字
                        break;
                    case 4:    //中间最低
                        canvas.drawText(number[i] + "", deviationX - numberrect.width() / 2, deviationY + numberrect.height() * 2, txtpaint);   //绘制数字
                        break;
                }
            } else {
                canvas.drawText(number[i] + "", deviationX - numberrect.width() / 2, deviationY - numberrect.height() / 2, txtpaint);   //绘制数字
            }

 6    随手指滑动的效果
    和手势有关的就不得不使用onTouchEvent方法了,给出代码:

    //这里面我们实现了让该控件可以随手指挥动而滚动,但是没写滚动条(因为觉得有滚动条也不好看)
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                positionx = event.getRawX();
                break;
            case MotionEvent.ACTION_MOVE:
                //如果是往右滑动,那么左侧距离就要判断不能过大
                if (event.getRawX() - positionx > 0) {
                    if (!(movedistance > leftdistance)) {
                        movedistance += event.getRawX() - positionx;
                        positionx = event.getRawX();
                        invalidate();
                    }
                } else {    //如果是往右滑动,那么左侧距离也要判断不能过大
                    if (!(movedistance < -rightdistance)) {
                        movedistance += event.getRawX() - positionx;
                        positionx = event.getRawX();
                        invalidate();
                    }
                }
                positionx = event.getRawX();
                break;
            case MotionEvent.ACTION_UP:
                break;

        }
        return true;
    }

    有两个值需要解释一下,leftdistance和rightdistance,这是我们可以再开始时设置的左右滑动的最大距离,具体可以自己进去设置。

    好了,总的效果也就这样完成了,解剖后是不是看起来又有点简单了,学习就是这样,能者不惧,你就是能者。
下面给出整个代码(可以直接粘贴复制进行使用):

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

/**
 * Created by wanxuedong on 2017/08/30.
 * 联系QQ:2381144912
 */

public class LineChart extends View {

    //这三个值是我们传进来的,绘图的数据从这里来
    private String[] coordinate; //X坐标上的标记
    private String[] number;      //折线图上的一个个数字,传值的时候只能传数字,别瞎传哈
    private float extremum;     //Y坐标的最大值


//    private int width;           //控件的宽度
    private int height;         //控件的高度
    private Path path;          //折线的路径
    private Paint linepaint;    //折线的画笔
    private Paint rectpaint;    //方块的画笔
    private Paint extremumpaint;  //坐标的画笔
    private Paint txtpaint;      //数值文字的画笔
    //    private Rect rect;          //方块的模型
    private Rect txtrect;     //利用rect获取X坐标上文字的宽度,好精确的放置文字位置
    private Rect numberrect;   //利用rect获取数字的宽度,好精确的放置文字位置

    //下面两个属性用来操作手势有关的
    private float positionx = 0;     //手指移动的时候,记录X坐标的作用
    private float movedistance;     //手指每次移动的位移,整个图像会根据手势进行移动


    /*
    * 上面的数据基本上不需要动,下面的属性我们可以自行更改
    * */

    //    private int RECTWIDTH = 5;       //小方块的宽度
    private int RECTHEIGHT = 6;     //小方块的高度和圆的半径
    private int OFFSETYTOP = 100;    //Y轴方向顶端留下的长度
    private int OFFSETYBOTTOM = 100;    //Y轴方向底端留下的长度
    private int OFFSETX = 100;    //x轴方向左端留下的长度
    private int XTXTSIZE = 24;   //X轴文字大小
    private int NUMBERSIZE = 18; //数字文字大小
    private int LINEWIDTH = 3;     //折线粗度
    private int txtpading = 80;    //X坐标轴上每个文字之间的间隔大小,这个决定了你的控件有多宽

    //下面两个属性需要根据实际情况自己改动,如果有好的方案,望请告知
    private int leftdistance = 50;     //X轴手势往右滑动的最大距离,这个数字会和OFFSETX加起来,总的数值才是左侧看起来的距离
    private int rightdistance = 500; //X轴手势往左滑动的最大距离


    public LineChart(Context context) {
        super(context);
        path = new Path();
        initpaint();
    }

    public LineChart(Context context, AttributeSet attrs) {
        super(context, attrs);
        path = new Path();
        initpaint();
    }

    //测量获取控件宽高
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//        width = getMeasuredWidth();
        height = getMeasuredHeight();
    }

    //这里面开始绘制文字,数字,和折线
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //定位折线的起始位置,也就是第一个数字坐标点
        path.moveTo(OFFSETX + movedistance, (height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[0]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM);

        for (int i = 0; i < number.length; i++) {
//            rect = new Rect();
            txtrect = new Rect();
            numberrect = new Rect();
            extremumpaint.getTextBounds(coordinate[i], 0, coordinate[i].length(), txtrect);
            txtpaint.getTextBounds(number[i], 0, number[i].length(), numberrect);
            float deviationY = (height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM;   //这里式子没有简化,是为了方便你们清楚这是怎么一个过程
            float deviationX = movedistance + OFFSETX + txtrect.width() * i + txtpading * i;  //这里式子没有简化,是为了方便你们清楚这是怎么一个过程
            //画出折线图的路径
            path.lineTo(deviationX, deviationY);
            canvas.drawPath(path, linepaint);      //绘制折线
            //设置方块的位置和宽高属性
//            rect.set(width / number.length * i - RECTWIDTH / 2 + OFFSETX, (int) (deviationY - RECTHEIGHT / 2), width / number.length * i + RECTWIDTH / 2 + OFFSETX, (int) (deviationY + RECTHEIGHT / 2));
//            canvas.drawRect(rect, rectpaint);      //绘制小方块
//            下面是绘制小圆球,上面是绘制小方块,有需要的可以恢复上面的小方块(不过坐标自己去设置了,懒的再去看去改了)
            canvas.drawCircle(deviationX, deviationY, RECTHEIGHT, rectpaint);
            //下面的绘制要保证文字在合适的位置,想了解的可参考我另一个博客:http://blog.csdn.net/wanxuedong/article/details/69396732
            if (i > 0 && i < number.length - 1) {
                switch (judgmentposition(i)) {
                    case 1:    //中间最高
                        canvas.drawText(number[i] + "", deviationX - numberrect.width() / 2, deviationY - numberrect.height(), txtpaint);   //绘制数字
                        break;
                    case 2:    //左边最高,右边最低
                        canvas.drawText(number[i] + "", deviationX, deviationY - numberrect.height() / 2, txtpaint);   //绘制数字
                        break;
                    case 3:    //左边最低,右边最高
                        canvas.drawText(number[i] + "", deviationX - numberrect.width(), deviationY - numberrect.height() / 2, txtpaint);   //绘制数字
                        break;
                    case 4:    //中间最低
                        canvas.drawText(number[i] + "", deviationX - numberrect.width() / 2, deviationY + numberrect.height() * 2, txtpaint);   //绘制数字
                        break;
                }
            } else {
                canvas.drawText(number[i] + "", deviationX - numberrect.width() / 2, deviationY - numberrect.height() / 2, txtpaint);   //绘制数字
            }
            //上面为了数字位置进行了判断设置,但是不包括第一个数字和最后一个数字,有需要的可以自行添加
            canvas.drawText(coordinate[i] + "", deviationX - txtrect.width() / 2, height - txtrect.height() / 2, extremumpaint);     //绘制X轴上的坐标
        }
        path.reset();   //每次重绘的时候需要把path恢复成空的路径,否则,所有路径会叠加在一起,那到时就热闹了
    }

    //这里面我们实现了让该控件可以随手指挥动而滚动,但是没写滚动条(因为觉得有滚动条也不好看)
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                positionx = event.getRawX();
                break;
            case MotionEvent.ACTION_MOVE:
                //如果是往右滑动,那么左侧距离就要判断不能过大
                if (event.getRawX() - positionx > 0) {
                    if (!(movedistance > leftdistance)) {
                        movedistance += event.getRawX() - positionx;
                        positionx = event.getRawX();
                        invalidate();
                    }
                } else {    //如果是往右滑动,那么左侧距离也要判断不能过大
                    if (!(movedistance < -rightdistance)) {
                        movedistance += event.getRawX() - positionx;
                        positionx = event.getRawX();
                        invalidate();
                    }
                }
                positionx = event.getRawX();
                break;
            case MotionEvent.ACTION_UP:
                break;

        }
        return true;
    }

    //初始化画笔,读者看的时候可略过
    public void initpaint() {

        linepaint = new Paint();
        rectpaint = new Paint();
        extremumpaint = new Paint();
        txtpaint = new Paint();

        //折线的一些属性
        linepaint.setColor(Color.argb(255, 245, 133, 61));     //设置颜色
        linepaint.setStyle(Paint.Style.STROKE);    //设置空心填充
        linepaint.setAntiAlias(true);  //设置抗锯齿
        linepaint.setStrokeWidth(LINEWIDTH);    //设置粗度

        //方块的一些属性
        rectpaint.setColor(Color.argb(255, 245, 133, 61));

        //X坐标轴上文字的一些属性
        extremumpaint.setColor(Color.argb(255, 51, 51, 51));
        extremumpaint.setTextSize(XTXTSIZE);    //设置坐标文字大小
        extremumpaint.setAntiAlias(true);

        //数字的一些属性
        txtpaint.setColor(Color.argb(255, 145, 145, 145));
        txtpaint.setTextSize(NUMBERSIZE);    //设置数字大小
        txtpaint.setAntiAlias(true);
    }

    /*这个函数的作用是判断当前数字坐标的y轴和其左右两边的数字坐标的y进行比较,从而判断数字显示的位置,避免折线和数字重复在一起的情况
     */
    public int judgmentposition(int i) {
        int status = 1;
        if (((height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i - 1]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM) >
                (height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM &&
                (height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM <
                        (height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i + 1]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM) {
            status = 1;
        } else if (((height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i - 1]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM) <
                (height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM &&
                (height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM <
                        (height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i + 1]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM) {
            status = 2;
        } else if (((height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i - 1]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM) >
                (height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM &&
                (height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM >
                        (height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i + 1]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM) {
            status = 3;
        } else {
            status = 4;
        }
        Log.d("statusnumbers", status + "");
        return status;
    }

    //设置整个坐标上的一些文字和数字
    public void setData(String[] coordinate, String[] number, float extremum) {
        this.coordinate = coordinate;
        this.number = number;
        this.extremum = extremum;
    }

}

最后还是给出demo,走你:http://download.csdn.net/download/wanxuedong/9958951

作者:wanxuedong 发表于2017/8/31 16:40:33 原文链接
阅读:0 评论:0 查看评论

Viewing all articles
Browse latest Browse all 5930

Trending Articles