Android 自定义折线图

自定义一个简单的折线图,足够展示万条数据,(千条以上数据在1070的屏幕宽度上 只会是一片茫茫,颜色会挤到一块儿。。。。)


简单说一下实现思路,

1、获取当前View在当前屏幕中的位置(Android中屏幕的左上角为整个屏幕坐标的原点往右 、往下递增,我们自己的坐标系是符合数学逻辑的二维坐标系,原点在整个折线图的左下角)

2、首先计算坐标点(比如,我们自己的坐标系的Y轴长度为画布的高度canasWidth减去50[我们默认距顶50],我们把获取到  的高度数值定为原点的Y轴坐标,X的走向与Android原生一致,  所以想让原点距离屏幕左边多远 ,就设置所多大的值为原点在屏幕的X坐标,记得留出足够的空间,需要去绘制Y轴刻度)

3、然后先绘制出X轴、Y轴,并根据数据的长度计算Y、X轴刻度(有十条数据,X轴长度除以10就是X周刻度,Y轴同理,根据       所有数据里的最大数值计算Y轴刻度)

4、别忘了绘制刻度数据,位置的计算在上一步中有相同之处,可以获取上边的值使用

5、绘制折线与虚线(X轴刻度)使用drawLines去绘制,会节省资源,虚线也要用这个去绘制,若使用(drawLine)绘制,会很慢

以下为自定义View核心类,在java代码中添加,目前没有做在布局中直接使用,另:提供demo供大家参考(点击此处),若有问题欢迎添加QQ  :1017726485.共同讨论

csdn链接 Android自定义折线图



import android.app.Activity;

import android.content.Context;

import android.content.res.Resources;

import android.graphics.Canvas;

import android.graphics.DashPathEffect;

import android.graphics.Paint;

import android.graphics.Path;

import android.support.annotation.ColorRes;

import android.util.Log;

import android.view.MotionEvent;

import android.view.View;

import android.view.ViewGroup;

import android.view.WindowManager;

import android.widget.LinearLayout;

import com.jjf.customchartline.R;

import com.jjf.customchartline.utils.Arith;

import com.jjf.customchartline.been.chartline.ChartResultData;

import com.jjf.customchartline.been.chartline.LineBeen;

import com.jjf.customchartline.been.chartline.LinePain;

import java.math.BigDecimal;

import java.util.ArrayList;

/**

* @author: jjf

* @date: 2018/4/19

* @describe:

*/

public abstract class MYChartLine extends View {

    private float canasWidth;                //画布宽

    private float canasHeight;

    private float XPoint;                  //定义原点

    private float YPoint;

    private double XScale;                  //刻度间距

    private int YScale;

    private int topDim = 30;//距离顶部

    private int surplusHeight = 50;//Y轴超出最大刻度距离(计算Y轴时总长减去超出距离)

    private int Ylenth = 6;//Y轴刻度数量

    private int surplusWhith = 50;//X轴超出最大刻度距离

    private String TAG = "MYChartLine";

    private Context context;

    //容器宽高

    private int viewGroupWidth;

    private int viewGroupHeight;

    //虚线间隙

    int dottedspace = 3;//虚线间隙

    int dottedLength = 10;//虚线长度

    //坐标轴宽度

    private int axisWidth = 1;

    //坐标轴颜色

    private @ColorRes

    int axisColor = R.color.zuobiao;//#a6a6a6

    Resources resources;

    int color;//

    int colorBlue;

    public int getYlenth() {

        return Ylenth;

    }

    public void setYlenth(int ylenth) {

        Ylenth = ylenth;

    }

    //坐标轴线的宽度

    public void setAxisWidth(int axisWidth) {

        this.axisWidth = axisWidth;

    }

    //轴颜色

    public void setAxisColor(@ColorRes int axisColor) {

        this.axisColor = axisColor;

        color = context.getResources().getColor(axisColor);

    }

    boolean isRefresh = true;//  false  刷新数据  true  添加数据

    Activity activity;

    public MYChartLine(Context context, ViewGroup viewGroup) {

        super(context);

        this.context = context;

        if (context instanceof Activity) {

            activity = (Activity) context;

        }

        resources = context.getResources();

        //获取屏幕宽(自适应屏幕宽度)

        WindowManager wm = ((Activity) context).getWindowManager();

        viewGroupWidth = wm.getDefaultDisplay().getWidth();

        // 取父控件高度(为了在布局中可以设置高度,不获取屏幕)

        LinearLayout.LayoutParams linearParams = (LinearLayout.LayoutParams) viewGroup

                .getLayoutParams();

        viewGroupHeight = linearParams.height;

        //画布宽高

        canasWidth = viewGroupWidth - 10;

        canasHeight = viewGroupHeight;

        //原点坐标

        XPoint = canasWidth / 12.0F;

        YPoint = canasHeight - surplusHeight;

        color = context.getResources().getColor(axisColor);

        colorBlue = context.getResources().getColor(R.color.zhexian_blue);

        //初始化画笔

        //标记线(随手势移动)

        paintYTag = new Paint();

        paintYTag.setColor(colorBlue);

        paintYTag.setStrokeWidth(2);

        paintYTag.setAntiAlias(true);

        paintYTag.setStyle(Paint.Style.STROKE);//设置画直线格式

        paintYTag.setPathEffect(new DashPathEffect(new float[]{dottedLength, dottedspace}, 0));//画虚线关键代码

        //轴画笔

        paintYX = new Paint();

        paintYX.setColor(color);

        paintYX.setTextSize(30);

        paintYX.setStrokeWidth(axisWidth);

        paintYX.setAntiAlias(true);

        //垂直虚线

        paintDottenY = new Paint();

        paintDottenY.setColor(color);

        paintDottenY.setTextSize(30);

        paintDottenY.setStrokeWidth(axisWidth);

        paintDottenY.setAntiAlias(true);

        paintDottenY.setStyle(Paint.Style.STROKE);//设置画直线格式

        paintDottenY.setPathEffect(new DashPathEffect(new float[]{5, 3}, 1));//画虚线关键代码

        //X轴数据

        paintXText = new Paint();

        paintXText.setColor(color);

        paintXText.setTextSize(30);

        paintXText.setAntiAlias(true);

        YScale = (int) ((canasHeight - canasHeight / Ylenth) / Ylenth);//Y轴刻度间距

        effectiveLenthX = (int) (canasWidth - XPoint - surplusWhith);

    }

    /**

    * 桌布

    */

    private Canvas canvas;

    //Y轴

    Paint paintYX;

    //Y轴数据

    Paint paintYText;

    //X轴数据

    Paint paintXText;

    //垂直虚线

    Paint paintDottenY;

    //与Y轴平行的、可移动标记线

    Paint paintYTag;

    //标记线

    Path tagPath = new Path();

    //清除画笔

    @Override

    protected void onDraw(Canvas canvas) {

        super.onDraw(canvas);

        this.canvas = canvas;

        //标记线(随手势移动)

        canvas.drawPath(tagPath, paintYTag);

        //轴画笔

        //Y轴

        canvas.drawLine(XPoint, topDim, XPoint, YPoint, paintYX);

        //X轴

        canvas.drawLine(XPoint, YPoint, canasWidth, YPoint, paintYX);

        //X轴数据

        YScale = (int) ((canasHeight - canasHeight / Ylenth) / Ylenth);//Y轴刻度间距

        this.onMDraw();

    }

    //画笔集合  记录每一条折线 //VALUE==折线与折线标注字体

    ArrayList<Paint> painLines = new ArrayList<>();

    //是否设置Y轴数据为10、100、1000倍数

    private boolean isYDataToMultiple = true;

    private LinePain maxLenthlinePain;//折线图最长的折线

    /**

    * 控制Y轴数据为10、100、1000的倍数(只有Y轴当前数据大与10倍才会抹去尾数  如 12 不会变成10    112会变成110,1234会变成1200,12345会变成12000)

    */

    public void setYDataToMultiple(boolean isYDataToMultiple) {

        this.isYDataToMultiple = isYDataToMultiple;

    }

    int xuLineNum = 0; //虚线个数、、X轴分割区数

    LinePain[] dataList;//所有条线的数据

    double maxValue = 0;

    /**

    * 添加折线

    * isRefresh true 刷新数据 全部重新绘制

    * false 添加数据  增加画笔

    */

    public void addLine(LinePain... linePain) {

        System.out.println("开始---");

        //onDraw 方法会运行两次,这里控制一段代码 只运行一次

        if (isRefresh) {

            chartResultData.setDatasY(chartYList);

            dataList = new LinePain[linePain.length];

        }

        try {

            //虚线个数、、X轴分割区数

            xuLineNum = 0;

            //所有Y轴数据中最大一条数据值--用最大值计算Y轴刻度平均值

            maxValue = 0;

            painLines.clear();

            //初始化、创建数据

            createData(linePain);

            //四舍五入求最大位数的整数

            //返回数据

            chartResultData.setDataX(maxLenthlinePain.getLineBeens().get(maxLenthlinePain.getLineBeens().size() - 1).getX() + "");

            //单位像素 代表的数据大小(Y轴)

            //Y轴实际单位长度(对应数据)

            double heightY = Arith.div(maxY - minY, maxValue, 10).doubleValue();

            //X轴刻度间距

            if (xuLineNum > 1) {

                XScale =  ( canasWidth - XPoint - surplusWhith)/(xuLineNum-1);

            } else {

                XScale = 1;

            }

            System.out.println("刻画Y轴---");

            //画 Y轴数据

            drawYData(linePain);

            System.out.println("刻画Y轴结束---");

            //计算日期间隔(最多显示7个日期,计算相邻两个日期中间间隔的单位个数)

            int inDex = xuLineNum / 6;

            System.out.println("计算虚线,折线开始---");

            //折线位置

            ArrayList<float[]> lineschart = new ArrayList<>();

            //计算虚线有多少截

            int dottedNum = (int) ((YPoint - topDim) / (dottedspace + dottedLength));

            //(每一条虚线的每一截都要计算出来)虚线位置

            float[] dottedLine = new float[xuLineNum * dottedNum * 4];

            //虚线个数--代表单条折线数据长度

            for (int j = 0; j < xuLineNum; j++) {

                int X = (int) (XPoint + j * XScale);  //X轴刻度位置

                //折线上数值 以及X轴刻度

                if (xuLineNum > 6) {

                    if (j % inDex == 0) {

                        //X轴坐标

                        canvas.drawText(maxLenthlinePain.getLineBeens().get(j).getX() + "",

                                (int) (XPoint + (j * XScale) - surplusWhith),

                                YPoint + 30, paintXText);

                    }

                } else {

                    //X轴坐标

                    canvas.drawText(maxLenthlinePain.getLineBeens().get(j).getX() + "",

                            (int) (XPoint + (j * XScale) - maxLenthlinePain.getLineBeens().get(j).getX().length() * 8),

                            YPoint + 30, paintXText);

                }

                //每条虚线的每段虚线节点

                double dottedPosition = 0;

                if (j < xuLineNum - 1) {

                    //计算每条虚线的每一节

                    for (int i = 0; i < dottedNum; i++) {

                        //Y轴平行虚线

                        dottedLine[j * dottedNum * 4 + i * 4] = (float) (X + XScale);

                        dottedLine[j * dottedNum * 4 + i * 4 + 1] = (float) (topDim + dottedPosition);

                        dottedLine[j * dottedNum * 4 + i * 4 + 2] = (float) (X + XScale);

                        dottedLine[j * dottedNum * 4 + i * 4 + 3] = (float) (topDim + dottedPosition + dottedLength);

                        dottedPosition += (dottedLength + dottedspace);

                    }

                }

                for (int c = 0; c < linePain.length; c++) {

                    if (lineschart.size() < c + 1) {//添加一条折线的容器(有几条折线,会添加几次)

                        lineschart.add(new float[xuLineNum * 4]);

                    }

                    //  折线Y点位置

                    double startY = 0;

                    //当前折线长度是否到达  j  脚标(折线长度不一定相等 对比是否达到最长折线)

                    if (linePain[c].getLineBeens().size() > j) {

                        // 折线Y点位置

                        startY = YPoint - Arith.mul(heightY + "", linePain[c].getLineBeens().get(j).getY() + "");

                        //折线上数值 以及X轴刻度

                        if (xuLineNum > 6) {

                            if (j % inDex == 0) {

                                //查看本折线是否展示连接点值,true显示折线上的值

                                if (linePain[c].isShowPrompt()) {

                                    canvas.drawText(linePain[c].getLineBeens().get(j).getY() + "", X - 10, (float) (startY - 20), painLines.get(c));

                                }

                            }

                        } else {

                            if (linePain[c].isShowPrompt()) {

                                //显示线上的值

                                canvas.drawText(linePain[c].getLineBeens().get(j).getY() + "", X - 15, (float) (startY - 20), painLines.get(c));

                            }

                        }

                        //折线(与虚线)在倒数第二次已经完成绘制----折线数据计算

                        if (j < linePain[c].getLineBeens().size() - 1) {

                            //实际高度

                            double z1 = Arith.mul(heightY + "", linePain[c].getLineBeens().get(j + 1).getY() + "");

                            lineschart.get(c)[j * 4] = X;

                            lineschart.get(c)[j * 4 + 1] = (float) startY;

                            lineschart.get(c)[j * 4 + 2] = (float) (XPoint + (j + 1) * XScale);

                            lineschart.get(c)[j * 4 + 3] = YPoint - ((float) z1);

                        }

                    }

                }

            }

            System.out.println("渲染虚线,折线开始---");

            //画虚线

            canvas.drawLines(dottedLine, paintDottenY);

            //  折线

            for (int i = 0; i < painLines.size(); i++) {

                canvas.drawLines(lineschart.get(i), painLines.get(i));

            }

            System.out.println("渲染虚线,折线结束---" + System.currentTimeMillis());

            this.postInvalidate();

            isRefresh = false;

        } catch (Exception e) {

            Log.i(TAG, "---------" + e.toString());

            e.printStackTrace();

        }

    }

    private int lastX = 0;//记录标记线的位置

    private int lastY = 0;

    private int startYLineX = 0;//Y轴的X坐标

    private int effectiveLenthY = 0;//Y轴有效长度

    private int effectiveLenthX = 0;//X轴有效长度

    @Override

    public boolean onTouchEvent(MotionEvent event) {

        //获取到手指处的横坐标和纵坐标

        int x = (int) event.getX();

        int y = (int) event.getY();

        switch (event.getAction()) {

            case MotionEvent.ACTION_DOWN:

                try {

                    getParent().requestDisallowInterceptTouchEvent(false);

                } catch (Exception e) {

                }

            case MotionEvent.ACTION_MOVE:

                System.out.println("effectiveLenthX=="+effectiveLenthX);

                if ((x > XPoint && x < effectiveLenthX + XPoint) && (y < YPoint && y > topDim)) {

                    lastX = x;

                    tagPath.reset();

                    tagPath.moveTo(lastX, topDim - 5);

                    tagPath.lineTo(lastX, YPoint);

//                    postInvalidate();

//

                    setToData(x);

                }

                break;

            case MotionEvent.ACTION_UP:

                try {

                    getParent().requestDisallowInterceptTouchEvent(true);

                } catch (Exception e) {

                }

                break;

        }

        return true;

    }

    //计算集合中最大值与最小值

    //最大值

    public double ArrayListMax(ArrayList<LineBeen> sampleList) {

        try {

            double maxDevation = 0.0;

            int totalCount = sampleList.size();

            if (totalCount >= 1) {

                double max = strToDouble(sampleList.get(0).getY());

                for (int i = 0; i < totalCount; i++) {

                    double temp = strToDouble(sampleList.get(i).getY());

                    if (temp > max) {

                        max = temp;

                    }

                }

                maxDevation = max;

            }

            return maxDevation;

        } catch (Exception ex) {

            throw ex;

        }

    }

    //调用本类的其他 需要实现本方法、在本方法去调用

    public abstract void onMDraw();

    private int minY = 0;

    private int maxY = 0;

    //监听手势 返回触摸到位置的值

    private ChartResultData chartResultData = new ChartResultData();

    //所有折线的Y值  添加到 chartResultData 中

    ArrayList<String> chartYList = new ArrayList();

    /**

    * \

    * <p>

    * 返回手势选中数据

    *

    * @return

    */

    public abstract ChartResultData setData(ChartResultData chartResultData);

    /**

    * 初始化数据

    * 创建、计算相关变量

    */

    private void createData(LinePain... linePain) throws IllegalAccessException {

        int x = 0;

        for (int i = 0; i < linePain.length; i++) {

            dataList[i] = linePain[i];

            Paint paint = new Paint();

            paint.setStrokeWidth(linePain[i].getLineWidth());

            paint.setAntiAlias(true);

            paint.setColor(resources.getColor(linePain[i].getColorId()));

            paint.setTextSize(linePain[i].getNumberPromptSize());

            if (linePain[i].getLineBeens() != null && linePain[i].getLineBeens().size() > 0) {

                //找出最多数据的折线

                if (xuLineNum < linePain[i].getLineBeens().size()) {

                    xuLineNum = linePain[i].getLineBeens().size();

                }

                //Y轴最大刻度值

                if (maxValue < ArrayListMax(linePain[i].getLineBeens())) {

                    maxValue = ArrayListMax(linePain[i].getLineBeens());

                }

                int size = linePain[i].getLineBeens().size();

                if (x < size) {//找出最长的一条折线(多条折线不一定一样长短)

                    x = size;

                    maxLenthlinePain = linePain[i];

                }

                if (maxLenthlinePain == null) {

                    maxLenthlinePain = linePain[0];

                }

            } else {

                return;

            }

            painLines.add(paint);

        }

        if (maxValue > 1000) {

            maxValue = Arith.div(maxValue, (double) 1000, 3).setScale(0, BigDecimal.ROUND_HALF_UP).doubleValue() * 1000;

        } else if (maxValue > 100) {

            maxValue = Arith.div(maxValue, (double) 100, 3).setScale(0, BigDecimal.ROUND_HALF_UP).doubleValue() * 100;

        } else if (maxValue > 10) {

            maxValue = Arith.div(maxValue, (double) 10, 3).setScale(0, BigDecimal.ROUND_HALF_UP).doubleValue() * 10;

        } else if (maxValue < 5) {

            maxValue = 5;

        }

    }

    public double strToDouble(String str) {

        return Double.parseDouble(str);

    }

    /**

    * 刻画Y轴数据

    */

    public void drawYData(LinePain... linePain) {

        //Y轴数据

        if (paintYText == null) {

            paintYText = new Paint();

        }

        paintYText.setColor(color);

        paintYText.setTextSize(30);

        paintYText.setAntiAlias(true);

        String strY;

        //Y轴一个刻度 等于的数据大小

        int v1 = (int) (new BigDecimal(maxValue / (Ylenth - 1)).setScale(0, BigDecimal.ROUND_HALF_UP).doubleValue());

        //Y轴刻度数据

        for (int i = 0; i < Ylenth; i++) {

            int i1 = (int) (maxValue - i * v1);

            if (i1 < 0) {

                i1 = 0;

            }

            /**

            *使数据为10、100、1000的倍数---纯属为了数据好看,

            * 可以通过 {@link com.jjf.customchartline.view.MYChartLine#setYDataToMultiple(boolean)} 设置

            */

            if (isYDataToMultiple) {

                if (i1 > 1000) {

                    strY = i1 / 100 * 100 + "";

                } else if (i1 > 100) {

                    strY = i1 / 10 * 10 + "";

                } else {

                    strY = i1 + "";

                }

            } else {

                strY = i1 + "";

            }

            // Y轴文字

            int heigY = i * YScale + (linePain[0].getNumberPromptSize() / 2) + topDim + surplusHeight;

            if (i < Ylenth - 1) {

                float v = XPoint - strY.length() * (linePain[0].getNumberPromptSize() / 2) - 10;

                if (v < 0) {

                    v = 0;

                }

                canvas.drawText(strY, v, heigY, paintYText);

            }

            if (i == 0) {

                minY = heigY;

            } else if (i == Ylenth - 1) {

                maxY = heigY;

            }

        }

    }


    //根据手势位置返回数据到页面

    public void setToData(int x) {

        try {

            double mx = XScale / 2;

            x = (int) (x + mx);

            int div = (int) Arith.div((x - XPoint), XScale == 0 ? 1 : XScale, 2).doubleValue();

            chartResultData.getDatasY().clear();

            for (int j = 0; j < dataList.length; j++) {

                if (dataList[j].getLineBeens().size() > div) {

                    chartResultData.setDataX(dataList[j].getLineBeens().get(div).getX());

                    chartResultData.getDatasY().add(dataList[j].getLineBeens().get(div).getY() + "");

                } else {

                    chartResultData.getDatasY().add("0");

                }

            }

            setData(chartResultData);

//            MYChartLine.this.postInvalidate();

        } catch (IllegalAccessException e) {

            e.printStackTrace();

        }

    }

}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容