1. 程式人生 > >使用MPAndroidChart實現K線圖(5)——高亮聯動、橫豎屏切換

使用MPAndroidChart實現K線圖(5)——高亮聯動、橫豎屏切換

目錄

由於前面畫出的K線圖已經預設實現了左右手勢滑動圖表,此時再滑動高亮的話,會造成衝突。因此把高亮的邏輯設定成只有長按才可以觸發高亮,高亮時,其他手勢無效,此時高亮跟隨手指滑動,手勢結束時高亮結束;未觸發高亮時僅執行預設邏輯。由於Chart的手勢監聽器OnChartGestureListener只會觸發部分手勢效果,且滯後於View的OnTouchListener,因此需要自定義監聽器ChartFingerTouchListener,繼承View.OnTouchListener。

public class ChartFingerTouchListener implements View.OnTouchListener {

    private BarLineChartBase mChart;
    private GestureDetector mDetector;
    private HighlightListener mListener;
    private boolean mIsLongPress = false;

    public ChartFingerTouchListener(BarLineChartBase chart, HighlightListener listener) {
        mChart = chart;
        mListener = listener;
        mDetector = new GestureDetector(mChart.getContext(), new GestureDetector.SimpleOnGestureListener() {
            @Override
            public void onLongPress(MotionEvent e) {
                super.onLongPress(e);
                mIsLongPress = true;
                if (mListener != null) {
                    mListener.enableHighlight();
                }
                Highlight h = mChart.getHighlightByTouchPoint(e.getX(), e.getY());
                if (h != null) {
                    h.setDraw(e.getX(), e.getY());
                    mChart.highlightValue(h, true);
                    mChart.disableScroll();
                }
            }
        });
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        mDetector.onTouchEvent(event);
        if (event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL) {
            mIsLongPress = false;
            mChart.highlightValue(null, true);
            if (mListener != null) {
                mListener.disableHighlight();
            }
            mChart.enableScroll();
        }
        if (mIsLongPress && event.getAction() == MotionEvent.ACTION_MOVE) {
            if (mListener != null) {
                mListener.enableHighlight();
            }
            Highlight h = mChart.getHighlightByTouchPoint(event.getX(), event.getY());
            if (h != null) {
                h.setDraw(event.getX(), event.getY());
                mChart.highlightValue(h, true);
                mChart.disableScroll();
            }
            return true;
        }
        return false;
    }

    public interface HighlightListener {
        void enableHighlight();
        void disableHighlight();
    }
}

其中,設定回撥介面HighlightListener,當高亮有效和關閉高亮時回撥到外部。高亮線跟隨手指滑動,需要在繪製高亮線之前把手指接觸點的座標儲存下來,高亮物件Highlight已經使用元素mX、mY、mXPx、mYPx儲存了高亮點的座標(蠟燭圖的高亮點為接觸蠟燭的中心點),不方便直接改動,而其中還有不太重要的元素mDrawX、mDrawY,使用其setDraw(float x, float y)方法,可以用來儲存接觸點的座標。

由於ChartFingerTouchListener僅處理一個圖表(稱為圖A)的手勢互動,並觸發高亮,此時還需要在圖A有高亮變化時,主動去通知相關聯的圖表(稱為圖B)去處理相應的高亮。因此需要自定義高亮監聽器CoupleChartValueSelectedListener

,繼承OnChartValueSelectedListener。

public class CoupleChartValueSelectedListener implements OnChartValueSelectedListener {

    private BarLineChartBase srcChart;
    private BarLineChartBase[] dstCharts;
    private ValueSelectedListener mListener;

    public CoupleChartValueSelectedListener(BarLineChartBase srcChart, BarLineChartBase... dstCharts) {
        this(null, srcChart, dstCharts);
    }

    public CoupleChartValueSelectedListener(ValueSelectedListener mListener,
                                            BarLineChartBase srcChart, BarLineChartBase... dstCharts) {
        this.mListener = mListener;
        this.srcChart = srcChart;
        this.dstCharts = dstCharts;
    }

    @Override
    public void onValueSelected(Entry e, Highlight h) {
        if (dstCharts != null) {
            for (BarLineChartBase chart : dstCharts) {
                float touchY = h.getDrawY();//手指接觸點在srcChart上的Y座標,即手勢監聽器中儲存資料
                float y = h.getY();
                if (chart instanceof BarChart) {
                    y = touchY - srcChart.getHeight();
                } else if (chart instanceof CombinedChart) {
                    y = touchY + chart.getHeight();
                }
                Highlight hl = new Highlight(h.getX(), Float.NaN, h.getDataSetIndex());
                hl.setDraw(h.getX(), y);
                chart.highlightValues(new Highlight[]{hl});
            }
        }
        if (mListener != null) {
            mListener.valueSelected(e);
        }
    }

    @Override
    public void onNothingSelected() {
        if (dstCharts != null) {
            for (BarLineChartBase chart : dstCharts) {
                chart.highlightValues(null);
            }
        }
        if (mListener != null) {
            mListener.nothingSelected();
        }
    }

    public interface ValueSelectedListener {
        void valueSelected(Entry e);
        void nothingSelected();
    }
}

關於手指接觸點的Y座標,在圖表範圍內,從上邊緣到下邊緣,從0開始依次遞增,最大為圖表的高度;在圖表以外的上面區域,其值為負,越往上越小;在圖表以外的下面區域,從圖表下邊緣開始,越往下越大。對於同一個點,它在圖A和在圖B中的Y座標是不同的(圖A和圖B不重疊),因此圖A通知圖B高亮時,需要計算接觸點在圖B中的座標,然後再使圖B高亮。我們這裡的K線圖,CombinedChart和BarChart是上下相連且無間隙的,如果長按觸發高亮的點在CombinedChart中,接觸點在CombinedChart中的Y座標減去CombinedChart的高度,即為接觸點在BarChart中的Y座標;如果觸發高亮的點在BarChart中,接觸點在BarChart中的Y座標加上CombinedChart的高度,即為接觸點在CombinedChart中的Y座標。另外,新建Highlight時把y值設定為Float.NaN可保證Highlight不出現異常(繪製高亮時並不使用其y值)。

把監聽器設定給CombinedChart和BarChart,並實現其介面中的方法。

        //初始化方法中設定
        cc.setOnChartValueSelectedListener(new CoupleChartValueSelectedListener(this, cc, bc));//高亮聯動監聽
        bc.setOnChartValueSelectedListener(new CoupleChartValueSelectedListener(this, bc, cc));
        cc.setOnTouchListener(new ChartFingerTouchListener(cc, this));//手指長按滑動高亮
        bc.setOnTouchListener(new ChartFingerTouchListener(bc, this));

    @Override
    public void valueSelected(Entry e) {
        //高亮時邏輯
    }

    @Override
    public void nothingSelected() {
        //高亮關閉時邏輯
    }

    @Override
    public void enableHighlight() {
        if (!barSet.isHighlightEnabled()) {
            candleSet.setHighlightEnabled(true);
            lineSetMin.setHighlightEnabled(true);
            barSet.setHighlightEnabled(true);
        }
    }

    @Override
    public void disableHighlight() {
        if (barSet.isHighlightEnabled()) {
            candleSet.setHighlightEnabled(false);
            lineSetMin.setHighlightEnabled(false);
            barSet.setHighlightEnabled(false);
            if (ccGesture != null) {
                ccGesture.setHighlight(true);
            }
            if (bcGesture != null) {
                bcGesture.setHighlight(true);
            }
        }
    }

在互動結束時,Chart在回撥ChartFingerTouchListener後,會繼續回撥CoupleChartGestureListener。也就是說,當圖表停留在邊緣時長按觸發高亮,手勢結束高亮關閉後會回撥CoupleChartGestureListener,繼續執行滑到邊緣載入更多的邏輯。應該避免這種情況,由於只有在手勢結束的時候才會觸發載入更多,且在此之前會執行ChartFingerTouchListener的disableHighlight()方法,所以可以在這裡使CoupleChartGestureListener停止執行載入更多。給CoupleChartGestureListener新增isHighlight元素,並修改以下方法:

    private boolean isHighlight;//是否高亮  -- 高亮時不再載入更多

    @Override
    public void onChartGestureEnd(MotionEvent me, ChartTouchListener.ChartGesture lastPerformedGesture) {
        if (isHighlight) {
            isHighlight = false;
        } else {
            if (isLoadMore) {
                float leftX = srcChart.getLowestVisibleX();
                float rightX = srcChart.getHighestVisibleX();
                if (leftX == srcChart.getXChartMin()) {//滑到最左端
                    canLoad = false;
                    if (edgeListener != null) {
                        edgeListener.edgeLoad(leftX, true);
                    }
                } else if (rightX == srcChart.getXChartMax()) {//滑到最右端
                    canLoad = false;
                    if (edgeListener != null) {
                        edgeListener.edgeLoad(rightX, false);
                    }
                } else {
                    canLoad = true;
                }
            }
        }
        syncCharts();
        chartGestureEnd(me, lastPerformedGesture);
    }

    public void setHighlight(boolean highlight) {
        isHighlight = highlight;
    }

對於高亮,預想的效果是:在手指接觸點畫一根豎線、一根橫線,在豎線的最上端(CombinedChart內)顯示時間,在橫線的最右端顯示接觸點的Y值,如果接觸點在圖表區域外,則不畫橫線。對CombinedChart和BarChart分開來考慮的話,BarChart只畫豎線,不畫豎線上的文字,有橫線時畫橫線和文字;CombinedChart需要同時畫豎線和文字,有橫線時畫橫線和文字。

先自定義繪製BarChart的高亮效果,在之前自定義過的渲染器OffsetBarRenderer中重寫繪製高亮的方法drawHighlighted(),實現高亮效果。

    protected float highlightWidth, highlightSize;//高亮線寬度 單位:dp  /  高亮文字大小 單位:px
    private DecimalFormat format = new DecimalFormat("0.0000");//保留小數點後四位

    public OffsetBarRenderer setHighlightWidthSize(float width, float textSize) {
        highlightWidth = Utils.convertDpToPixel(width);
        highlightSize = textSize;
        return this;
    }

    @Override
    public void drawHighlighted(Canvas c, Highlight[] indices) {
        BarData barData = mChart.getBarData();
        for (Highlight high : indices) {
            IBarDataSet set = barData.getDataSetByIndex(high.getDataSetIndex());
            if (set == null || !set.isHighlightEnabled())
                continue;
            BarEntry e = set.getEntryForXValue(high.getX(), high.getY());
            if (!isInBoundsX(e, set))
                continue;

            mHighlightPaint.setColor(set.getHighLightColor());
            mHighlightPaint.setStrokeWidth(highlightWidth);
            mHighlightPaint.setTextSize(highlightSize);

            //畫豎線
            float barWidth = barData.getBarWidth();
            Transformer trans = mChart.getTransformer(set.getAxisDependency());
            prepareBarHighlight(e.getX() + barOffset, 0, 0, barWidth / 2, trans);

            float xp = mBarRect.centerX();
            c.drawLine(xp, mViewPortHandler.getContentRect().bottom, xp, 0, mHighlightPaint);

            //判斷是否畫橫線
            float y = high.getDrawY();
            float yMaxValue = mChart.getYChartMax();
            float yMin = getYPixelForValues(xp, yMaxValue);
            float yMax = getYPixelForValues(xp, 0);
            if (y >= 0 && y <= yMax) {//在區域內即繪製橫線
                float xMax = mChart.getWidth();
                int halfPaddingVer = 5;//豎向半個padding
                int halfPaddingHor = 8;
                //先繪製文字框
                mHighlightPaint.setStyle(Paint.Style.STROKE);
                float yValue = (yMax - y) / (yMax - yMin) * yMaxValue;
                String text = format.format(yValue);
                int width = Utils.calcTextWidth(mHighlightPaint, text);
                int height = Utils.calcTextHeight(mHighlightPaint, text);
                float top = Math.max(0, y - height / 2F - halfPaddingVer);//考慮間隙
                float bottom = top + height + halfPaddingVer * 2;
                if (bottom > yMax) {
                    bottom = yMax;
                    top = bottom - height - halfPaddingVer * 2;
                }
                RectF rect = new RectF(xMax - width - halfPaddingHor * 2, top, xMax, bottom);
                c.drawRect(rect, mHighlightPaint);
                //再繪製文字
                mHighlightPaint.setStyle(Paint.Style.FILL);
                Paint.FontMetrics metrics = mHighlightPaint.getFontMetrics();
                float baseY = (top + bottom - metrics.top - metrics.bottom) / 2;
                c.drawText(text, xMax - width - halfPaddingHor, baseY, mHighlightPaint);
                //繪製橫線
                c.drawLine(0, y, xMax - width - halfPaddingHor * 2, y, mHighlightPaint);
            }
        }
    }

    protected float getYPixelForValues(float x, float y) {
        MPPointD pixels = mChart.getTransformer(YAxis.AxisDependency.LEFT).getPixelForValues(x, y);
        return (float) pixels.y;
    }

一個點在圖表上有兩種座標,一種是MPAndroidChart定義的座標軸上的座標,一種是Android系統定義的在手機螢幕內的座標(控制元件內的座標是相對於控制元件而言的)。MPAndroidChart提供了方法,把座標軸座標轉換成螢幕座標,即getYPixelForValues(float x, float y)方法中的語句。對於點的Y座標,在座標軸內是從下往上遞增的,在手機螢幕內是從上往下遞增的。實際情況中,座標軸上的可見Y值範圍,和螢幕座標的可見範圍不是完全對應的,可能出現座標軸上Y最大值對應的螢幕座標是不可見的。在前面選中高亮時所儲存的座標是螢幕座標,而高亮的橫線上顯示的數字是座標軸座標,因此要把Highlight中的mDrawY轉換成座標軸座標,這裡使用的是按比例計算的方式。

給OffsetBarRenderer設定高亮線的寬度,以及文字大小,執行程式,效果圖如下:

上部分CombinedChart中的高亮線是其中的CandleStickChart預設的高亮效果。

接下來就是自定義CandleStickChart的渲染器HighlightCandleRenderer,繼承CandleStickChartRenderer,重寫高亮方法。

public class HighlightCandleRenderer extends CandleStickChartRenderer {

    private float highlightSize;//圖表高亮文字大小 單位:px
    private DecimalFormat format = new DecimalFormat("0.0000");

    public HighlightCandleRenderer(CandleDataProvider chart, ChartAnimator animator,
                                   ViewPortHandler viewPortHandler) {
        super(chart, animator, viewPortHandler);
    }

    public HighlightCandleRenderer setHighlightSize(float textSize) {
        highlightSize = textSize;
        return this;
    }

    @Override
    public void drawHighlighted(Canvas c, Highlight[] indices) {
        CandleData candleData = mChart.getCandleData();
        for (Highlight high : indices) {
            ICandleDataSet set = candleData.getDataSetByIndex(high.getDataSetIndex());
            if (set == null || !set.isHighlightEnabled())
                continue;

            CandleEntry e = set.getEntryForXValue(high.getX(), high.getY());
            if (!isInBoundsX(e, set))
                continue;

            float lowValue = e.getLow() * mAnimator.getPhaseY();
            float highValue = e.getHigh() * mAnimator.getPhaseY();
            MPPointD pix = mChart.getTransformer(set.getAxisDependency())
                    .getPixelForValues(e.getX(), (lowValue + highValue) / 2f);
            float xp = (float) pix.x;

            mHighlightPaint.setColor(set.getHighLightColor());
            mHighlightPaint.setStrokeWidth(set.getHighlightLineWidth());
            mHighlightPaint.setTextSize(highlightSize);

            float xMin = mViewPortHandler.contentLeft();
            float xMax = mViewPortHandler.contentRight();
            float contentBottom = mViewPortHandler.contentBottom();
            //畫豎線
            int halfPaddingVer = 5;//豎向半個padding
            int halfPaddingHor = 8;
            float textXHeight = 0;

            String textX;//高亮點的X顯示文字
            Object data = e.getData();
            if (data != null && data instanceof String) {
                textX = (String) data;
            } else {
                textX = e.getX() + "";
            }
            if (!TextUtils.isEmpty(textX)) {//繪製x的值
                //先繪製文字框
                mHighlightPaint.setStyle(Paint.Style.STROKE);
                int width = Utils.calcTextWidth(mHighlightPaint, textX);
                int height = Utils.calcTextHeight(mHighlightPaint, textX);
                float left = Math.max(xMin, xp - width / 2F - halfPaddingVer);//考慮間隙
                float right = left + width + halfPaddingHor * 2;
                if (right > xMax) {
                    right = xMax;
                    left = right - width - halfPaddingHor * 2;
                }
                textXHeight = height + halfPaddingVer * 2;
                RectF rect = new RectF(left, 0, right, textXHeight);
                c.drawRect(rect, mHighlightPaint);
                //再繪製文字
                mHighlightPaint.setStyle(Paint.Style.FILL);
                Paint.FontMetrics metrics = mHighlightPaint.getFontMetrics();
                float baseY = (height + halfPaddingVer * 2 - metrics.top - metrics.bottom) / 2;
                c.drawText(textX, left + halfPaddingHor, baseY, mHighlightPaint);
            }
            //繪製豎線
            c.drawLine(xp, textXHeight, xp, contentBottom, mHighlightPaint);

            //判斷是否畫橫線
            float y = high.getDrawY();
            float yMaxValue = mChart.getYChartMax();
            float yMinValue = mChart.getYChartMin();
            float yMin = getYPixelForValues(xp, yMaxValue);
            float yMax = getYPixelForValues(xp, yMinValue);
            if (y >= 0 && y <= contentBottom) {//在區域內即繪製橫線
                //先繪製文字框
                mHighlightPaint.setStyle(Paint.Style.STROKE);
                float yValue = (yMax - y) / (yMax - yMin) * (yMaxValue - yMinValue) + yMinValue;
                String text = format.format(yValue);
                int width = Utils.calcTextWidth(mHighlightPaint, text);
                int height = Utils.calcTextHeight(mHighlightPaint, text);
                float top = Math.max(0, y - height / 2F - halfPaddingVer);//考慮間隙
                float bottom = top + height + halfPaddingVer * 2;
                if (bottom > contentBottom) {
                    bottom = contentBottom;
                    top = bottom - height - halfPaddingVer * 2;
                }
                RectF rect = new RectF(xMax - width - halfPaddingHor * 2, top, xMax, bottom);
                c.drawRect(rect, mHighlightPaint);
                //再繪製文字
                mHighlightPaint.setStyle(Paint.Style.FILL);
                Paint.FontMetrics metrics = mHighlightPaint.getFontMetrics();
                float baseY = (top + bottom - metrics.top - metrics.bottom) / 2;
                c.drawText(text, xMax - width - halfPaddingHor, baseY, mHighlightPaint);
                //繪製橫線
                c.drawLine(0, y, xMax - width - halfPaddingHor * 2, y, mHighlightPaint);
            }
        }
    }

    protected float getYPixelForValues(float x, float y) {
        MPPointD pixels = mChart.getTransformer(YAxis.AxisDependency.LEFT).getPixelForValues(x, y);
        return (float) pixels.y;
    }
}

豎線上的日期不可以由x值直接轉換,因為對於同一個點,在追加資料前後的x值是不同的,所以這個日期應在新建CandleEntry時作為附加物件傳入。在繪製方法中,橫線的繪製和OffsetBarRenderer中的橫線繪製幾乎一樣。

由於CandleStickChart是被包含在CombinedChart中的,因此應將CandleStickChart的渲染器替換為自定義渲染器HighlightCandleRenderer。自定義CombinedChart的渲染器HighlightCombinedRenderer,繼承CombinedChartRenderer。

public class HighlightCombinedRenderer extends CombinedChartRenderer {

    private float highlightSize;//圖表高亮文字大小 單位:px

    public HighlightCombinedRenderer(CombinedChart chart, ChartAnimator animator,
                                     ViewPortHandler viewPortHandler, float highlightSize) {
        super(chart, animator, viewPortHandler);
        this.highlightSize = highlightSize;
    }

    @Override
    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 HighlightCandleRenderer(chart, mAnimator, mViewPortHandler)
                                .setHighlightSize(highlightSize));
                    break;
                case SCATTER:
                    if (chart.getScatterData() != null)
                        mRenderers.add(new ScatterChartRenderer(chart, mAnimator, mViewPortHandler));
                    break;
            }
        }
    }
}

給CombinedChart設定此渲染器,並在新建CandleEntry時傳入日期。

        //初始化方法中設定
        cc.setRenderer(new HighlightCombinedRenderer(cc, cc.getAnimator(), cc.getViewPortHandler(), sp8));

        //配置圖表方法中設定
        candleValues.add(new CandleEntry(i, Float.parseFloat(k.get(2)),
                Float.parseFloat(k.get(3)), open, close, x));

效果圖如下:

同樣,時間間隔為1m的分時圖也需要自定義高亮。分時圖是LineChart,自定義其渲染器HighlightLineRenderer,繼承LineChartRenderer,重寫高亮方法。

public class HighlightLineRenderer extends LineChartRenderer {

    private float highlightSize;//圖表高亮文字大小 單位:px
    private DecimalFormat format = new DecimalFormat("0.0000");

    public HighlightLineRenderer(LineDataProvider chart, ChartAnimator animator,
                                 ViewPortHandler viewPortHandler) {
        super(chart, animator, viewPortHandler);
    }

    public HighlightLineRenderer setHighlightSize(float textSize) {
        highlightSize = textSize;
        return this;
    }

    @Override
    public void drawHighlighted(Canvas c, Highlight[] indices) {
        LineData lineData = mChart.getLineData();
        for (Highlight high : indices) {
            ILineDataSet set = lineData.getDataSetByIndex(high.getDataSetIndex());
            if (set == null || !set.isHighlightEnabled())
                continue;

            Entry e = set.getEntryForXValue(high.getX(), high.getY());
            if (!isInBoundsX(e, set))
                continue;

            MPPointD pix = mChart.getTransformer(set.getAxisDependency()).getPixelForValues(e.getX(),
                    e.getY() * mAnimator.getPhaseY());
            float xp = (float) pix.x;

            mHighlightPaint.setColor(set.getHighLightColor());
            mHighlightPaint.setStrokeWidth(set.getHighlightLineWidth());
            mHighlightPaint.setTextSize(highlightSize);

            float xMin = mViewPortHandler.contentLeft();
            float xMax = mViewPortHandler.contentRight();
            float contentBottom = mViewPortHandler.contentBottom();
            //畫豎線
            int halfPaddingVer = 5;//豎向半個padding
            int halfPaddingHor = 8;
            float textXHeight = 0;

            String textX;//高亮點的X顯示文字
            Object data = e.getData();
            if (data != null && data instanceof String) {
                textX = (String) data;
            } else {
                textX = e.getX() + "";
            }
            if (!TextUtils.isEmpty(textX)) {//繪製x的值
                //先繪製文字框
                mHighlightPaint.setStyle(Paint.Style.STROKE);
                int width = Utils.calcTextWidth(mHighlightPaint, textX);
                int height = Utils.calcTextHeight(mHighlightPaint, textX);
                float left = Math.max(xMin, xp - width / 2F - halfPaddingVer);//考慮間隙
                float right = left + width + halfPaddingHor * 2;
                if (right > xMax) {
                    right = xMax;
                    left = right - width - halfPaddingHor * 2;
                }
                textXHeight = height + halfPaddingVer * 2;
                RectF rect = new RectF(left, 0, right, textXHeight);
                c.drawRect(rect, mHighlightPaint);
                //再繪製文字
                mHighlightPaint.setStyle(Paint.Style.FILL);
                Paint.FontMetrics metrics = mHighlightPaint.getFontMetrics();
                float baseY = (height + halfPaddingVer * 2 - metrics.top - metrics.bottom) / 2;
                c.drawText(textX, left + halfPaddingHor, baseY, mHighlightPaint);
            }
            //繪製豎線
            c.drawLine(xp, textXHeight, xp, contentBottom, mHighlightPaint);

            //判斷是否畫橫線
            float y = high.getDrawY();
            float yMaxValue = mChart.getYChartMax();
            float yMinValue = mChart.getYChartMin();
            float yMin = getYPixelForValues(xp, yMaxValue);
            float yMax = getYPixelForValues(xp, yMinValue);
            if (y >= 0 && y <= contentBottom) {//在區域內即繪製橫線
                //先繪製文字框
                mHighlightPaint.setStyle(Paint.Style.STROKE);
                float yValue = (yMax - y) / (yMax - yMin) * (yMaxValue - yMinValue) + yMinValue;
                String text = format.format(yValue);
                int width = Utils.calcTextWidth(mHighlightPaint, text);
                int height = Utils.calcTextHeight(mHighlightPaint, text);
                float top = Math.max(0, y - height / 2F - halfPaddingVer);//考慮間隙
                float bottom = top + height + halfPaddingVer * 2;
                if (bottom > contentBottom) {
                    bottom = contentBottom;
                    top = bottom - height - halfPaddingVer * 2;
                }
                RectF rect = new RectF(xMax - width - halfPaddingHor * 2, top, xMax, bottom);
                c.drawRect(rect, mHighlightPaint);
                //再繪製文字
                mHighlightPaint.setStyle(Paint.Style.FILL);
                Paint.FontMetrics metrics = mHighlightPaint.getFontMetrics();
                float baseY = (top + bottom - metrics.top - metrics.bottom) / 2;
                c.drawText(text, xMax - width - halfPaddingHor, baseY, mHighlightPaint);
                //繪製橫線
                c.drawLine(0, y, xMax - width - halfPaddingHor * 2, y, mHighlightPaint);
            }
        }
    }

    protected float getYPixelForValues(float x, float y) {
        MPPointD pixels = mChart.getTransformer(YAxis.AxisDependency.LEFT).getPixelForValues(x, y);
        return (float) pixels.y;
    }
}

方式和HighlightCandleRenderer一樣。接著把HighlightCombinedRenderer中的LineChartRenderer替換成HighlightLineRenderer。

        //createRenderers()方法中
        case LINE:
            if (chart.getLineData() != null)
                mRenderers.add(new HighlightLineRenderer(chart, mAnimator, mViewPortHandler)
                        .setHighlightSize(highlightSize));
        break;

在新建分時線的Entry時傳入日期。

        //配置圖表方法中設定
        minValues.add(new Entry(i, close, x));

效果圖如下:

另外,當頁面重啟時,如果圖表處於最右端,則向右獲取最新資料。

    @Override
    protected void onRestart() {
        super.onRestart();
        float rightX = cc.getHighestVisibleX();
        if (rightX == cc.getXChartMax()) {//停留在最右端
            edgeLoad(rightX, false);
        }
    }

接下來是橫豎屏切換效果,其基本邏輯是:預設情況下豎屏,此時雙擊圖表或點選切換按鈕,則切換成橫屏;橫屏時,按後退鍵或點選切換按鈕,則切換成豎屏;切換前後,最右端為同一個資料。

通常情況下,橫屏和豎屏的佈局類似但不完全相同,需要分別設定佈局。將豎屏的佈局放在res下的layout-port資料夾中,橫屏的佈局放在layout-land資料夾中,注意將佈局名設定成相同,相同控制元件的id設定成相同。橫豎屏切換時不需要重新執行生命週期,需將AndroidManifest.xml中的當前Activity進行如下設定:

        <activity android:name=".activity.home.KLineActivity"
            android:configChanges="orientation|keyboardHidden|screenSize" />

豎屏時雙擊圖表切換成橫屏,則需要在手勢監聽器中處理雙擊事件。

        //初始化方法中設定
        ccGesture = new CoupleChartGestureListener(this, cc, bc) {
            @Override
            public void chartDoubleTapped(MotionEvent me) {
                doubleTapped();
            }
        };
        cc.setOnChartGestureListener(ccGesture);//設定手勢聯動監聽
        bcGesture = new CoupleChartGestureListener(this, bc, cc) {
            @Override
            public void chartDoubleTapped(MotionEvent me) {
                doubleTapped();
            }
        };
        bc.setOnChartGestureListener(bcGesture);

    private void doubleTapped() {
        if (isPort()) {
            highVisX = cc.getHighestVisibleX();//記錄切換前最右端的X
            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
        }
    }

    /**
     * 當前是否是豎屏
     */
    public boolean isPort() {
        return getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT;
    }

還有切換按鈕的監聽,以及後退鍵處理。

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.iv_klBack://後退鍵
                onBackPressed();
                break;
            case R.id.iv_klOrientation://切換橫豎屏
                highVisX = cc.getHighestVisibleX();
                setRequestedOrientation(isPort() ? ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE :
                        ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
                break;
        }
    }

    @Override
    public void onBackPressed() {
        if (isPort()) {
            super.onBackPressed();
        } else {
            highVisX = cc.getHighestVisibleX();
            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
        }
    }

最後是切換後的處理。

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        setContentView(R.layout.activity_main);
        initView();
        range = newConfig.orientation == Configuration.ORIENTATION_PORTRAIT ? 52 : 86;//豎屏顯示52個 橫屏顯示86個
        configData();
        if (xValues.size() > 0) {
            cc.post(new Runnable() {
                @Override
                public void run() {
                    float x = highVisX - range;
                    cc.moveViewToX(x);
                    bc.moveViewToX(x + barOffset);
                    cc.notifyDataSetChanged();
                    bc.notifyDataSetChanged();
                }
            });
        }
    }

橫屏效果圖:

至此,預定的效果均已實現。 

雖然效果實現了,但還有一個小問題:CombinedChart的高亮線不能繪製在X軸上,即使把線的長度設定的足夠大。如果真有這種需求,也是有辦法解決的。首先是找到造成這種情況的原因。通常,View的繪製語句都是在其onDraw(Canvas canvas)方法中,MPAndroidChart也不例外。檢視CombinedChart父類BarLineChartBase的原始碼,在onDraw()方法中有以下語句:

        // make sure the data cannot be drawn outside the content-rect
        int clipRestoreCount = canvas.save();
        canvas.clipRect(mViewPortHandler.getContentRect());

        mRenderer.drawData(canvas);

        // if highlighting is enabled
        if (valuesToHighlight())
            mRenderer.drawHighlighted(canvas, mIndicesToHighlight);

        // Removes clipping rectangle
        canvas.restoreToCount(clipRestoreCount);

        mRenderer.drawExtras(canvas);

可見在Renderer繪製高亮之前,對畫布canvas進行了裁剪,只保留了中間的內容區域,繪製完高亮之後又恢復了畫布。因此在HighlightCandleRenderer的drawHighlighted()方法中是不可能把高亮繪製在X軸上的。

但是在恢復畫布後,Renderer又呼叫了drawExtras(Canvas c)方法,因此可以在這裡繪製高亮。由於這兩個方法是依次呼叫的,所以可以在drawHighlighted()中把mIndicesToHighlight儲存起來。在drawExtras()中繪製時,把豎線的長度設定成直到圖表底部即可。

        //繪製豎線
        c.drawLine(xp, textXHeight, xp, mChart.getHeight(), mHighlightPaint);

同樣,HighlightLineRenderer中也可以進行相同的操作。