美文网首页
股票图或虚拟币图的绘制 【第二章:K线图】

股票图或虚拟币图的绘制 【第二章:K线图】

作者: Small_Cake | 来源:发表于2018-03-14 18:23 被阅读251次

    原文链接:https://www.jianshu.com/p/e2c0c8a31b09

    最开始我是打算用自定义View来实现这个功能的,都绘制得差不多了,但是在滑动手势处理,和缩放,还有x轴的时间显现等一系列问题上久战不下。最后还是在github上面找到了一个非常强大且start上2万星星的图表框架:MPAndroidChart
    看首页的各种图片我就知道,绘制K线图,我就靠它了。其实网上我也看了很多博客和帖子,但是很多都过时了,而且有的重点并没有说出来,比如如何绘制显示区间的最大和最小值标记。但是他们的方法和说明也给了我很多启发:

    一步一步教你写股票走势图

    MPAndroidChart 教程

    Android安卓使用MPAndroidChart绘制K线图和股票指标

    看了这些我相信你也能很好的绘制出来图表了,但是这里用的MP库估计是以前的,很多方法不对,且没有说到我关心的如何绘制最大最小值标记。废话很多,下面我们就开始代码走起。

    首先我们知道由于我们绘制的K线图包含了蜡烛图和曲线图,所以我们单独的使用MP库的CandleStickChart和LineChart是画不出来我们想要的效果的。不得不说MP库的强大,它提供了一种组合图CombinedChart,它就非常强大了,可以把5种图(BAR, BUBBLE, LINE, CANDLE, SCATTER)都融合进来,然后循环调用不同的渲染器来绘制就可以了。

    但是我们要的最大和最小值标记,却不是MP能完成的,所以我们需要继承这个集合图表来重写对应方法绘制。

    我们要先理解它是如何绘制蜡烛图的,看CombinedChart源码:

      @Override
        protected void init() {
            super.init();
    
            // Default values are not ready here yet
            mDrawOrder = new DrawOrder[]{
                    DrawOrder.BAR, DrawOrder.BUBBLE, DrawOrder.LINE, DrawOrder.CANDLE, DrawOrder.SCATTER
            };
    
            setHighlighter(new CombinedHighlighter(this, this));
    
            // Old default behaviour
            setHighlightFullBarEnabled(true);
    
            mRenderer = new CombinedChartRenderer(this, mAnimator, mViewPortHandler);
        }
    

    通过查看我们发现组合图CombinedChart在init方法中初始化了一个CombinedChartRenderer渲染器,渲染器就是绘制图表的关键。

    mRenderer = new CombinedChartRenderer(this, mAnimator, mViewPortHandler);
    那我们进入到CombinedChartRenderer渲染器中去看看,会发现

     public CombinedChartRenderer(CombinedChart chart, ChartAnimator animator, ViewPortHandler viewPortHandler) {
            super(animator, viewPortHandler);
            mChart = new WeakReference<Chart>(chart);
            createRenderers();
        }
    
        /**
         * Creates the renderers needed for this combined-renderer in the required order. Also takes the DrawOrder into
         * consideration.
         */
        public void createRenderers() {
    
            mRenderers.clear();
    
            CombinedChart chart = (CombinedChart)mChart.get();
            if (chart == null)
                return;
    
            DrawOrder[] orders = chart.getDrawOrder();
    
            for (DrawOrder order : orders) {
    
                switch (order) {
                    case BAR:
                        if (chart.getBarData() != null)
                            mRenderers.add(new BarChartRenderer(chart, mAnimator, mViewPortHandler));
                        break;
                    case BUBBLE:
                        if (chart.getBubbleData() != null)
                            mRenderers.add(new BubbleChartRenderer(chart, mAnimator, mViewPortHandler));
                        break;
                    case LINE:
                        if (chart.getLineData() != null)
                            mRenderers.add(new LineChartRenderer(chart, mAnimator, mViewPortHandler));
                        break;
                    case CANDLE:
                        if (chart.getCandleData() != null)
                            mRenderers.add(new CandleStickChartRenderer(chart, mAnimator, mViewPortHandler));
                        break;
                    case SCATTER:
                        if (chart.getScatterData() != null)
                            mRenderers.add(new ScatterChartRenderer(chart, mAnimator, mViewPortHandler));
                        break;
                }
            }
        }
    

    我们发现它的构造方法中通过createRenderers()方法创建并添加了各种5大渲染器,它是根据当前chart获取数据的不同来创建不同的渲染器的,如 if (chart.getBarData() != null)
    mRenderers.add(new BarChartRenderer(chart, mAnimator, mViewPortHandler));如果当前获取的Bar数据不为null,就创建相关BarChartRenderer渲染器,并添加到这个mRenderers渲染器集合中。

    那绘制数据是在哪里呢,我们还看到了:

      @Override
        public void drawData(Canvas c) {
    
            for (DataRenderer renderer : mRenderers)
                renderer.drawData(c);
        }
    

    这里我们就看到它通过循环遍历出渲染器,并调用了renderer的drawData方法来绘制对应的图表的。
    我们要绘制的最大值和最小值肯定就是蜡烛图的最高值和最低值了 。所以我们需要去看蜡烛图渲染器里面的方法,才知道蜡烛图是如何绘制的。

    进入CandleStickChartRenderer看看,会发现

     @Override
        public void drawData(Canvas c) {
    
            CandleData candleData = mChart.getCandleData();
    
            for (ICandleDataSet set : candleData.getDataSets()) {
    
                if (set.isVisible())
                    drawDataSet(c, set);
            }
        }
    
    

    我们发现CandleStickChartRenderer绘制数据就是得到当前数据,然后循环得到ICandleDataSet,如果是显示的就绘制它,下面重点来了: drawDataSet(c, set);

     @SuppressWarnings("ResourceAsColor")
        protected void drawDataSet(Canvas c, ICandleDataSet dataSet) {
    
            Transformer trans = mChart.getTransformer(dataSet.getAxisDependency());
    
            float phaseY = mAnimator.getPhaseY();
            float barSpace = dataSet.getBarSpace();
            boolean showCandleBar = dataSet.getShowCandleBar();
    
            mXBounds.set(mChart, dataSet);
    
            mRenderPaint.setStrokeWidth(dataSet.getShadowWidth());
    
            // draw the body
            for (int j = mXBounds.min; j <= mXBounds.range + mXBounds.min; j++) {
    
                // get the entry
                CandleEntry e = dataSet.getEntryForIndex(j);
    
                if (e == null)
                    continue;
    
                final float xPos = e.getX();
    
                final float open = e.getOpen();
                final float close = e.getClose();
                final float high = e.getHigh();
                final float low = e.getLow();
    
                if (showCandleBar) {
                    // calculate the shadow
    
                    mShadowBuffers[0] = xPos;
                    mShadowBuffers[2] = xPos;
                    mShadowBuffers[4] = xPos;
                    mShadowBuffers[6] = xPos;
    
                    if (open > close) {
                        mShadowBuffers[1] = high * phaseY;
                        mShadowBuffers[3] = open * phaseY;
                        mShadowBuffers[5] = low * phaseY;
                        mShadowBuffers[7] = close * phaseY;
                    } else if (open < close) {
                        mShadowBuffers[1] = high * phaseY;
                        mShadowBuffers[3] = close * phaseY;
                        mShadowBuffers[5] = low * phaseY;
                        mShadowBuffers[7] = open * phaseY;
                    } else {
                        mShadowBuffers[1] = high * phaseY;
                        mShadowBuffers[3] = open * phaseY;
                        mShadowBuffers[5] = low * phaseY;
                        mShadowBuffers[7] = mShadowBuffers[3];
                    }
    
                    trans.pointValuesToPixel(mShadowBuffers);
    
                    // draw the shadows
    
                    if (dataSet.getShadowColorSameAsCandle()) {
    
                        if (open > close)
                            mRenderPaint.setColor(
                                    dataSet.getDecreasingColor() == ColorTemplate.COLOR_NONE ?
                                            dataSet.getColor(j) :
                                            dataSet.getDecreasingColor()
                            );
    
                        else if (open < close)
                            mRenderPaint.setColor(
                                    dataSet.getIncreasingColor() == ColorTemplate.COLOR_NONE ?
                                            dataSet.getColor(j) :
                                            dataSet.getIncreasingColor()
                            );
    
                        else
                            mRenderPaint.setColor(
                                    dataSet.getNeutralColor() == ColorTemplate.COLOR_NONE ?
                                            dataSet.getColor(j) :
                                            dataSet.getNeutralColor()
                            );
    
                    } else {
                        mRenderPaint.setColor(
                                dataSet.getShadowColor() == ColorTemplate.COLOR_NONE ?
                                        dataSet.getColor(j) :
                                        dataSet.getShadowColor()
                        );
                    }
    
                    mRenderPaint.setStyle(Paint.Style.STROKE);
    
                    c.drawLines(mShadowBuffers, mRenderPaint);
    
                    // calculate the body
    
                    mBodyBuffers[0] = xPos - 0.5f + barSpace;
                    mBodyBuffers[1] = close * phaseY;
                    mBodyBuffers[2] = (xPos + 0.5f - barSpace);
                    mBodyBuffers[3] = open * phaseY;
    
                    trans.pointValuesToPixel(mBodyBuffers);
    
                    // draw body differently for increasing and decreasing entry
                    if (open > close) { // decreasing
    
                        if (dataSet.getDecreasingColor() == ColorTemplate.COLOR_NONE) {
                            mRenderPaint.setColor(dataSet.getColor(j));
                        } else {
                            mRenderPaint.setColor(dataSet.getDecreasingColor());
                        }
    
                        mRenderPaint.setStyle(dataSet.getDecreasingPaintStyle());
    
                        c.drawRect(
                                mBodyBuffers[0], mBodyBuffers[3],
                                mBodyBuffers[2], mBodyBuffers[1],
                                mRenderPaint);
    
                    } else if (open < close) {
    
                        if (dataSet.getIncreasingColor() == ColorTemplate.COLOR_NONE) {
                            mRenderPaint.setColor(dataSet.getColor(j));
                        } else {
                            mRenderPaint.setColor(dataSet.getIncreasingColor());
                        }
    
                        mRenderPaint.setStyle(dataSet.getIncreasingPaintStyle());
    
                        c.drawRect(
                                mBodyBuffers[0], mBodyBuffers[1],
                                mBodyBuffers[2], mBodyBuffers[3],
                                mRenderPaint);
                    } else { // equal values
    
                        if (dataSet.getNeutralColor() == ColorTemplate.COLOR_NONE) {
                            mRenderPaint.setColor(dataSet.getColor(j));
                        } else {
                            mRenderPaint.setColor(dataSet.getNeutralColor());
                        }
    
                        c.drawLine(
                                mBodyBuffers[0], mBodyBuffers[1],
                                mBodyBuffers[2], mBodyBuffers[3],
                                mRenderPaint);
                    }
                } else {
    
                    mRangeBuffers[0] = xPos;
                    mRangeBuffers[1] = high * phaseY;
                    mRangeBuffers[2] = xPos;
                    mRangeBuffers[3] = low * phaseY;
    
                    mOpenBuffers[0] = xPos - 0.5f + barSpace;
                    mOpenBuffers[1] = open * phaseY;
                    mOpenBuffers[2] = xPos;
                    mOpenBuffers[3] = open * phaseY;
    
                    mCloseBuffers[0] = xPos + 0.5f - barSpace;
                    mCloseBuffers[1] = close * phaseY;
                    mCloseBuffers[2] = xPos;
                    mCloseBuffers[3] = close * phaseY;
    
                    trans.pointValuesToPixel(mRangeBuffers);
                    trans.pointValuesToPixel(mOpenBuffers);
                    trans.pointValuesToPixel(mCloseBuffers);
    
                    // draw the ranges
                    int barColor;
    
                    if (open > close)
                        barColor = dataSet.getDecreasingColor() == ColorTemplate.COLOR_NONE
                                ? dataSet.getColor(j)
                                : dataSet.getDecreasingColor();
                    else if (open < close)
                        barColor = dataSet.getIncreasingColor() == ColorTemplate.COLOR_NONE
                                ? dataSet.getColor(j)
                                : dataSet.getIncreasingColor();
                    else
                        barColor = dataSet.getNeutralColor() == ColorTemplate.COLOR_NONE
                                ? dataSet.getColor(j)
                                : dataSet.getNeutralColor();
    
                    mRenderPaint.setColor(barColor);
                    c.drawLine(
                            mRangeBuffers[0], mRangeBuffers[1],
                            mRangeBuffers[2], mRangeBuffers[3],
                            mRenderPaint);
                    c.drawLine(
                            mOpenBuffers[0], mOpenBuffers[1],
                            mOpenBuffers[2], mOpenBuffers[3],
                            mRenderPaint);
                    c.drawLine(
                            mCloseBuffers[0], mCloseBuffers[1],
                            mCloseBuffers[2], mCloseBuffers[3],
                            mRenderPaint);
                }
            }
        }
    
    

    这个就是绘制的蜡烛图的主要方法了,但是这个方法200行太长了点,我们只看关键会发现这个mXBounds,这个是什么东西呢?带着疑问我们先看一下mXBounds,发现它是CandleStickChartRenderer的父类的父类BarLineScatterCandleBubbleRenderer的一个包级对象:

     protected class XBounds {
    
            /**
             * minimum visible entry index
             */
            public int min;
    
            /**
             * maximum visible entry index
             */
            public int max;
    
            /**
             * range of visible entry indices
             */
            public int range;
    
            /**
             * Calculates the minimum and maximum x values as well as the range between them.
             *
             * @param chart
             * @param dataSet
             */
            public void set(BarLineScatterCandleBubbleDataProvider chart, IBarLineScatterCandleBubbleDataSet dataSet) {
                float phaseX = Math.max(0.f, Math.min(1.f, mAnimator.getPhaseX()));
    
                float low = chart.getLowestVisibleX();
                float high = chart.getHighestVisibleX();
    
                Entry entryFrom = dataSet.getEntryForXValue(low, Float.NaN, DataSet.Rounding.DOWN);
                Entry entryTo = dataSet.getEntryForXValue(high, Float.NaN, DataSet.Rounding.UP);
    
                min = entryFrom == null ? 0 : dataSet.getEntryIndex(entryFrom);
                max = entryTo == null ? 0 : dataSet.getEntryIndex(entryTo);
                range = (int) ((max - min) * phaseX);
            }
        }
    

    我们看到这个类包含了最大,最小和范围三个属性,联想一下,难道它就是我们滚动图表显示的最大值,最小值,范围。我们在

     // draw the body
            for (int j = mXBounds.min; j <= mXBounds.range + mXBounds.min; j++) {
    
                // get the entry
                CandleEntry e = dataSet.getEntryForIndex(j);
            }
    

    这个循环里面打印一下蜡烛图对象数据CandleEntry的最高和最低,发现,还真是随着视图滚动,最大值和最小值也是一直在变化的!既然绘制是在这里的,那么就可用通过获取最大和最小值得CandleEntry对象,从而拿到对应的x值和y值,来绘制!

    所以我们在自定义的KLineCombinedChart的onDraw方法中来绘制就可以了,

     @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            setMaxMinMarker(canvas);
        }
    
    
        /**
         *  设置最大值蜡烛图标记
         * @param canvas
         */
        public void setMaxMinMarker(Canvas canvas) {
            CandleData candleData = this.getCandleData();
            if (candleData!=null)for (ICandleDataSet set : candleData.getDataSets()) {
                if (set.isVisible())drawMaxMinCandleMarker(canvas, set);
            }
        }
     /**
         * 绘制最大最小值蜡烛图标记
         * @param c
         * @param dataSet
         */
        protected void drawMaxMinCandleMarker(Canvas c, ICandleDataSet dataSet) {
            Transformer trans = this.getTransformer(dataSet.getAxisDependency());
    
            float phaseY = mAnimator.getPhaseY();
            float barSpace = dataSet.getBarSpace();
            mXBounds.set(this, dataSet);
            float maxFloat = Float.MIN_VALUE;
            float minFloat = Float.MAX_VALUE;
    
            CandleEntry maxEntry = null;
            CandleEntry minEntry  = null ;
            for (int j = mXBounds.min; j <= mXBounds.range + mXBounds.min; j++) {
                // get the entry
                CandleEntry e = dataSet.getEntryForIndex(j);
                if (e == null) continue;
                final float high = e.getHigh();
                final float low = e.getLow();
                //求最大值和最小值
                if (high>maxFloat){
                    maxEntry = e;
                    maxFloat = high;
                }
                if (low<minFloat){
                    minEntry = e;
                    minFloat = low;
                }
            }
            L.i("最大值=="+maxFloat+" 最小值=="+minFloat);
    
            if (maxEntry!=null){
                final float xPos = maxEntry.getX();
                final float open = maxEntry.getOpen();
                final float close = maxEntry.getClose();
                final float high = maxEntry.getHigh();
                final float low = maxEntry.getLow();
                mBodyBuffers[0] = xPos - 0.5f + barSpace;
                mBodyBuffers[1] = close * phaseY;
                mBodyBuffers[2] = (xPos + 0.5f - barSpace);
                mBodyBuffers[3] = open * phaseY;
                trans.pointValuesToPixel(mBodyBuffers);
    
                float left = mBodyBuffers[0];
                float top = mBodyBuffers[3];
                float right = mBodyBuffers[2];
                float bottom = mBodyBuffers[1];
    
                float x = left+(right-left)/2;
    
                int textWidth = Utils.calcTextWidth(paintWhite, "" + maxFloat);
                int textHeight = Utils.calcTextHeight(paintWhite, "" + maxFloat);
    
    
                int triangleWidth=24;
                Path path = new Path();
                path.moveTo(x,mViewPortHandler.contentTop()-1);
                path.lineTo(x+triangleWidth+1,mViewPortHandler.contentTop()-1);
                path.lineTo(x+triangleWidth+1,mViewPortHandler.contentTop()-textHeight*2-1);
                path.close();
                //绘制三角形,矩形,文字
                c.drawPath(path,paintRed);
                c.drawRect(x+triangleWidth,mViewPortHandler.contentTop()-textHeight*2-1,x+textWidth+8+triangleWidth,mViewPortHandler.contentTop()-1,paintRed);
                c.drawText(""+maxFloat,x+4+triangleWidth,mViewPortHandler.contentTop()-textHeight/2-1,paintWhite);
    
            }
            if (minEntry!=null){
                final float xPos = minEntry.getX();
                final float open = minEntry.getOpen();
                final float close = minEntry.getClose();
                final float high = minEntry.getHigh();
                final float low = minEntry.getLow();
                mBodyBuffers[0] = xPos - 0.5f + barSpace;
                mBodyBuffers[1] = low * phaseY;
                mBodyBuffers[2] = (xPos + 0.5f - barSpace);
                mBodyBuffers[3] = open * phaseY;
                trans.pointValuesToPixel(mBodyBuffers);
    
    
                float left = mBodyBuffers[0];
                float top = mBodyBuffers[3];
                float right = mBodyBuffers[2];
                float bottom = mBodyBuffers[1];
    
                float x = left+(right-left)/2;
                int textWidth = Utils.calcTextWidth(paintWhite, "" + minFloat);
                int textHeight = Utils.calcTextHeight(paintWhite, "" + minFloat);
    
                int triangleWidth=23;
    
                float v = mViewPortHandler.contentBottom() - bottom;
    
                Path path = new Path();
                path.moveTo(x,bottom);
                path.lineTo(x+triangleWidth+1,bottom);
                path.lineTo(x+triangleWidth+1,bottom+textHeight*2);
                path.close();
                //绘制三角形,矩形,文字
                c.drawPath(path,paintGreen);
    
                c.drawRect(x+triangleWidth,bottom,x+textWidth+8+triangleWidth,bottom+textHeight*2,paintGreen);
                c.drawText(""+minFloat,x+4+triangleWidth,bottom+textHeight+textHeight/2,paintWhite);
            }
    
    
        }
    
    

    提示:可能有的同学发现XBounds这个对象无法提取出来,那么我们仿照源码中的XBounds新建一个这样的对象就可以了:

    protected XBounds mXBounds = new XBounds();
     /**
         * Class representing the bounds of the current viewport in terms of indices in the values array of a DataSet.
         */
        protected class XBounds {
    
            /**
             * minimum visible entry index
             */
            public int min;
    
            /**
             * maximum visible entry index
             */
            public int max;
    
            /**
             * range of visible entry indices
             */
            public int range;
    
            /**
             * Calculates the minimum and maximum x values as well as the range between them.
             *
             * @param chart
             * @param dataSet
             */
            public void set(BarLineScatterCandleBubbleDataProvider chart, IBarLineScatterCandleBubbleDataSet dataSet) {
                float phaseX = Math.max(0.f, Math.min(1.f, mAnimator.getPhaseX()));
    
                float low = chart.getLowestVisibleX();
                float high = chart.getHighestVisibleX();
    
                Entry entryFrom = dataSet.getEntryForXValue(low, Float.NaN, DataSet.Rounding.DOWN);
                Entry entryTo = dataSet.getEntryForXValue(high, Float.NaN, DataSet.Rounding.UP);
    
                min = entryFrom == null ? 0 : dataSet.getEntryIndex(entryFrom);
                max = entryTo == null ? 0 : dataSet.getEntryIndex(entryTo);
                range = (int) ((max - min) * phaseX);
            }
        }
    

    这样就可以绘制出最大最小值的标记了,最后上一张效果图:


    QQ图片20180314182221.png

    下一章我将给大家介绍如何绘制选中后的高亮时间和右侧y轴标记,敬请期待。。。

    相关文章

      网友评论

          本文标题:股票图或虚拟币图的绘制 【第二章:K线图】

          本文链接:https://www.haomeiwen.com/subject/oeuzfftx.html