欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

新手如何画出自定义View(Android——自定义折线图)

程序员文章站 2022-05-30 22:45:34
...

在正式开始之前 我还是打算先说几句废话:

1、本文章是让初学者画自定义View所以不会对代码进行过多的解释

2、为什么不用现有强大的图表框架 列如:Android HelloChart 或者 MPAndroidChart

3、由于本人也是菜鸟、错误之处请多多包涵、另外大神勿喷

关于第二个问题有2个原因:1、总是有些奇葩需求导致那些强大的框架反而不好实现(也可能是我对那些框架理解不够)2、杀鸡焉用牛刀,自己写的虽然没那些框架的性能强大,但是却是轻量级的 说白了 就是可以让apk的大小减少辣么一丢丢。

首先先放个效果图展示一下,

顺便感谢一下在本资料中出现的各位大大的DemoDemo图表的链接(http://blog.csdn.net/u014544193/article/details/54313257

     修改之前的折线图                                              项目所需的折线图

新手如何画出自定义View(Android——自定义折线图)               新手如何画出自定义View(Android——自定义折线图) 新手如何画出自定义View(Android——自定义折线图)

项目中的折线图会根据滑动还改变Y轴的显示效果,宝宝不会做动图就截了2张图。

那么对于初学者我们应该如何迈出自定义View的第一步呢?

下面我以自己项目中自定义折线图为例来一步步说明身为菜鸟的我是如何画出自定义折线图的。

第一步:

这对于你开始可能是最难的一步 因为不知道如何开始。但现在这也将是最简单的一步,这一步就是百度。对于菜鸟的我们而言,可能根本无法开始一个简单的自定义View,更别谈什么折线图了,所以自然是看看别人怎么画的(其实不是看看那么简单 而是直接用了 迷之微笑)。

新手如何画出自定义View(Android——自定义折线图)

可以看到百度上已经有各种大大实现了这些功能,那么我们只要将他们的代码下载下来就可以了。到此为止,第一步已经结束,对于你要完成的折线图也已经完成大半,怎么样简单吧  不过对于菜鸟的我们而言别高兴太早  因为我们的难点可不是在这。

第二步:

下载完他们的Demo之后就是要将他们的代码改成自己项目需求中的样子了。这其实才是身为菜鸟的我们最难的一步。虽然是Demo但是要改代码稍微看懂这些代码的意思和逻辑,虽然各位大大的代码在demo中已经加上了各种注释,但是我们可能还是无法看懂。(首先别看到稍微复杂的东西就觉得 啊  不行了 好难啊 要死啦 完全看不懂呢的想法 要抱着我肯定看的懂的心态去看)这样的话怎么办呢?那么就去找更基础的Demo,就拿折线图来说,既然看不懂那么就去找更基础的东西。(这已经没有捷径可以解决了,无论如何 想要变强 除了变秃也只有一步步的努力了),话说我学习第一个自定义View是一个时钟,是比较好的学习对象,但是已经找不到了,可能淹没在了历史的尘埃中......

新手如何画出自定义View(Android——自定义折线图)

从这里可以发现我们又回到了第一步,没错在你能稍微理解demo代码的流程之前你必须要过这一关,当然只要稍微理解就好,没必要完全懂。再拿自定义的折线图来说 无非要你理解的就是以下这几步:

2.1   自定义控件首先执行的构造方法

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

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

    public ChartView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs, defStyleAttr);
        initPaint();
}

关于init(context, attrs, defStyleAttr)其实就是自定义属性 能懂的话 可以让你封装的View更加的完美  不懂也无伤大雅

关于initPaint()就是初始化画笔  其实稍微看过自定义View的一些基础demo这里都能明白

2.2接着就是走onLayout(boolean changed, int left, int top, int right, int bottom)onDraw(Canvas canvas)

这些都是自定义View的关键方法  但是从自定义折线图的demo中  作者都已经帮我们实现了这些方法  所以我们大可不必深究(其实能花时间看懂他们如何实现的就最好了)

这里我就对onDraw(Canvas canvas)进行稍微的解释  因为他是画出线的关键

drawXY(canvas);

drawBrokenLineAndPoint(canvas);

可以看到我们下载的Demo中在onDraw(Canvas canvas)里面有2个方法  从取名就可以看出 一个是画XY轴  一个是画折线和线上的点(解释结束,如何精准的画出线和点已经不属于我们的讨论范围了demo里面都已经帮我们计算好了)

第三步:

那就是改代码了,

3.1在我项目的需求中是Y轴是没有箭头的那么在DemodrawXY(Canvas canvas)中有这样一段代码

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

Demo作者的注释可谓完美  那么我们只需要注释掉就可以了

3.2在我项目的需求中 点击点会出现一个和Demo中不一样的图 在Demo中的drawBrokenPoint(Canvas canvas)

//绘制选中的点
if (i == selectIndex - 1) {
 linePaint.setStyle(Paint.Style.FILL);
      linePaint.setColor(0xfffd8989);
      canvas.drawCircle(x, y, dp7, linePaint);
      linePaint.setColor(0xffff3f40);
      canvas.drawCircle(x, y, dp4, linePaint);
      drawFloatTextBox(canvas, x, y - dp7, Xvalue, "0.11");
}

Demo作者依旧打了令人愉快的注释 一看就明白 其中drawFloatTextBox(canvas, x, y - dp7, Xvalue, "0.11")明显就是画出Demo中圆点上方框框的逻辑。那么我们如何要出我们想要的效果呢 其实我们项目中圆点上方不过是一张图片而已,那么问题就变成了如何画出一张图片了,什么你不会?没关系百度一下你就明白

private Bitmap getXBitmap() {
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.tubiao_tankuang);
        return bitmap;
}

百度之后马上找到了方法,那么在替换掉代码中的位置就好。(框框中的显示内容是2Demo中是一条 这个我就不用说怎么换了吧)

/**
     * 绘制显示Y值的浮动框
     *
     * @param canvas
     * @param x
     * @param y
     * @param text
     */
    private void drawFloatTextBox(Canvas canvas, float x, float y, float text, String text2) {
        int dp6 = dpToPx(6);
        int dp18 = dpToPx(18);
        //p1
//        Path path = new Path();
//        path.moveTo(x, y);
//        //p2
//        path.lineTo(x - dp6, y - dp6);
//        //p3
//        path.lineTo(x - dp18, y - dp6);
//        //p4
//        path.lineTo(x - dp18, y - dp6 - dp18);
//        //p5
//        path.lineTo(x + dp18, y - dp6 - dp18);
//        //p6
//        path.lineTo(x + dp18, y - dp6);
//        //p7
//        path.lineTo(x + dp6, y - dp6);
//        //p1
//        path.lineTo(x, y);
//        canvas.drawPath(path, linePaint);
        Bitmap xBitmap = getXBitmap();
        int width = xBitmap.getWidth();
        int height = xBitmap.getHeight();
        canvas.drawBitmap(getXBitmap(), x - width / 2, y - height, linePaint);
        linePaint.setColor(Color.WHITE);
        linePaint.setTextSize(spToPx(12));
        Rect rect = getTextBounds(text + "", linePaint);
        canvas.drawText(text + "", x - rect.width() / 2, y - height / 5 * 2, linePaint);
        canvas.drawText(text + "", x - rect.width() / 2, y - height / 5 * 3, linePaint);
    }

诸如此类的改动我就不一一明细了  反正就是复制粘贴改吧改吧就完成了。

3.3最后就说一下项目中最大的改动 就是我们的图表会随着左右滑动而改变Y轴的数据

代码复制粘贴到这里,或许我们应该停下来思考(休息)一下了,X轴会变化数据可不是因为他真的在变,而是画了一条很长的线(右边是屏幕外面看不见了而已)你往右滑动让他出现了而已,那么Y轴呢Demo中也没有什么方法可以改变呢。怎么办?还是老办法,百度一下你就。。。纳尼?竟然还是不知道。(再次说明 这时候千万不要有 要死啦 完全不会呢 好方啊之类的想法 话虽如此我已经涌现出这种想法了 明明最后一步了  马上就成功了有木有)

其实冷静下来就能想到一个非常简单的方法(抛开性能啥的不说  性能是啥好吃吗 ),就是折线图刚开始是怎么画出来的?

不就是我们调用setValue(Map<String, Double> value, List<String> xValue, List<Float> yValue, boolean first, int chartType)Y周和X轴的数据传递进去而画出来的吗?那么我们滑动的时候再次调用这个方法将Y轴改变后的数据传递进去不就可以了吗(我简直是个天才 哇哈哈哈)

可能你又不知道,怎么在滑动的时候调用什么的,我这里就直接说了(其实百度一下你就知道,只是我已经吐槽累了)

chartView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                if (mVisibilityFirstPosition != chartView.getFirstPosition()) {
                    mVisibilityFirstPosition = chartView.getFirstPosition();
                    int size = mVisibilityFirstPosition + 7 > xValue.size() ? xValue.size() - mVisibilityFirstPosition : 7;
                    double maxValueTemp = 0;
                    for (int i = 0; i < size; i++) {
                        Log.d("app", "mVisibilityFirstPosition=" + mVisibilityFirstPosition);
                        double visibileValue = value.get(xValue.get(mVisibilityFirstPosition+i));
                        if (maxValueTemp < visibileValue) {
                            maxValueTemp = visibileValue;
                        }
                    }
                    if (maxValueTemp != mMaxMoney) {
                        Log.d("app", "maxValueTemp=" + maxValueTemp);
                        mMaxMoney = maxValueTemp;
                        mInterval = mMaxMoney  * 1.1 / 5;
                        for (int i = 0; i < 6; i++) {
                            if (mInterval > 0.2) {
                                String format = mFnum.format(i * mInterval);
                                yValue.set(i, Float.valueOf(format));
                            }else {
                                yValue.set(i, i * 0.2f);
                            }
                        }
                        chartView.setValue(value, xValue, yValue, false, 1);
                    }
                }
                return false;
            }
        });

看着好像一大堆 其实是我对移动到下一个点时在再进行改变Y轴数据的一些逻辑,不然你每次一滑动就重新绘制折线图这样性能不是不要,而是要用糟糕来形容了,这些无非是计算的方法,打点log看看就可以写出来,关键的移动的方法就是setOnTouchListener  在里面进行chartView.setValue(value, xValue, yValue, false, 1);就可以达到刷新的效果

到这为止自定义的折线图终于已经完成了。。。。吗?在我放入项目前,这demo确实如我所愿达到了需求所要的效果,然后放入项目中后就出现了几个莫名其妙的问题。以下就我发现的问题来说下解决方法(说是解决 其实我压根不知道怎么解决  只是曲线救国了而已  果然很符合菜鸟程序员的本质啊)

问题1

问题描述:项目中的图表在一个Activity2Fragment中,在Fragment调用setValue(Map<String, Double> value, List<String> xValue, List<Float> yValue, boolean first, int chartType)这个时 我发现线并没有画出来(其实我在自己的Demo中也用了Fragment并没有出现此问题)。在项目中打了断点后发现走到onLayout时数据是没有的,那么drawXY(Canvas canvas) 自然就画不出来了,不是特别明白为什么(知道的大神可以告诉下)。

解决方法:

@SuppressLint("WrongCall")
 public void setValue(Map<String, Double> value, List<String> xValue, List<Float> yValue, boolean first, int chartType) {
        this.value = value;
        this.xValue = xValue;
        this.yValue = yValue;
        this.chartType = chartType;
//        requestLayout();
        if (first)
            onLayout(true, 0,0,width,height);
        invalidate();
}

在第一次调用时让他再次执行onLayout第一个参数要写True,不然又没数据了,注意setValue上方的注解一定要加

问题2

问题描述:有数据(Y轴数据较大的情况)但是折线还是显示不出来,虽然是同样的现象但是原因完全不通

解决方法:

for (int i = 1; i < xValue.size(); i++) {
            x = xInit + interval * i;
            double douValues = value.get(xValue.get(i));
            y = yOri - yOri * (1 - 0.1f) * (float)douValues / yValue.get(yValue.size() - 1);
            if (y < -height) {  //超出一定范围 就无法连成线了
                y = -height;
            }
            path.lineTo(x, y);
            shaderPath.lineTo(x, y);
}

原因是因为Y轴数据太大而导致线消失了(任然不明白为什么)只要将Y周的数据限制住就好,出现这个原因是由于项目中折线图的特性导致,该折线图需要Y轴根据当前显示数据而变化,例如1-7天的数据是0-10-10-10-10-10-10那么Y轴就显示最大的坐标是10但是第8天可能是100000那么Y轴上真是的点就在非常非常上面 而导致画不出线,这时只要对超出Y轴数据超出太多点进行限制就好(反正都看不见了 稍微给大点就好)

问题3

问题描述:有数据(都是0的情况)但是折线依旧依旧显示不出来(其实我只发现部分手机会这样)

解决方法:这个原因其实和第二个问题类似也是因为X轴太长而导致画不出来,但是X周总不能像Y周这样进行限制。处理方法有2

第一种:只画出X轴屏幕上可见的部分,这样不仅能提高效率而且有效解决问题。

第二种:在ChartView(Context context, AttributeSet attrs, int defStyleAttr)方法中加上

if(TextUtils.equals("M353", SystemUtil.getSystemModel())){	//android.os.Build.MODEL
    setLayerType(View.LAYER_TYPE_SOFTWARE, null);
}

因为是魅族M353这个手机看不见所以也可以加个判断,但是可以的话还是用第一种方法。

好了问题也结束了。这是我第一次写文章,本文章的目的是为了让我们学会如何去写出自定义的控件,而不是教你如何去画出折线图,而且感觉洋洋洒洒写了一大堆,不知道有没讲到重点。排版也有点乱,不足之处你倒是来打我啊,源码的Demo我就不放了(我会告诉你我写的太烂,导致拿不出手吗 贱贱的笑  其实是Demo不知道哪里去了)。刚好快过年了,这里也祝各位新年快乐了。如果有转载也请各位大大珍惜下我的劳动成果,加上链接就好。