1. 程式人生 > >使用MPAndroidChart實現K線圖(4)——圖表聯動、載入更多

使用MPAndroidChart實現K線圖(4)——圖表聯動、載入更多

目錄

首先說一下圖表聯動和載入更多流程邏輯。圖表聯動是指,當滑動上部分的K線圖時,成交量圖會跟隨滑動;當滑動成交量圖時,K線圖會跟隨滑動。而顯示和載入的邏輯相對複雜一點,預設情況下,K線左右邊緣的兩個只會顯示一半,資料的時間是從左向右的,右側資料的時間比左側資料的時間更新,也就是從右向左滑可以滑到沒有資料,而從左向右滑可以有足夠多的資料,因此始終使最右端資料顯示完整,最左端不考慮。在設定完資料後,再給Chart的X軸設定最大值即可使最右端顯示完整。

        float xMax = xValues.size() - 0.5F;//預設X軸最大值是 xValues.size() - 1
        cc.getXAxis().setAxisMaximum(xMax);//使最後一個顯示完整
        
        bc.getXAxis().setAxisMaximum(xMax + barOffset);//保持邊緣對齊

接著往下,初次獲取資料後,設定好圖表後,把圖表平移到最右端,顯示最新資料;滑動到邊緣載入更多後,如果載入的是右側的資料,則平移到最右端,如果載入的是左側的資料,則平移到載入之前的位置。載入更多後的圖表繪製,最初的想法是追加到圖表的原有資料上(因為有向左追加的緣故,X的值會取負數且越來越小),但經過試驗,發現向右追加有效,但是向左追加後不會接著繪製(想不明白這裡的原因)。最後的實現方式是,把原始資料存放在dataList中,有新資料就插入進去,每次獲取資料後,不論是初次獲取,還是追加載入,都對圖表進行清空資料並重繪,繪製後平移到對應的位置。

自定義手勢監聽器OnChartGestureListener

聯動滑動時會回撥圖表手勢監聽,因此要自定義OnChartGestureListener,命名為CoupleChartGestureListener

public class CoupleChartGestureListener implements OnChartGestureListener {

    private BarLineChartBase srcChart;
    private Chart[] dstCharts;

    private OnEdgeListener edgeListener;//滑動到邊緣的監聽器
    private boolean isLoadMore;//是否載入更多
    private boolean canLoad;//K線圖手指互動已停止,正在慣性滑動

    public CoupleChartGestureListener(BarLineChartBase srcChart, Chart... dstCharts) {
        this.srcChart = srcChart;
        this.dstCharts = dstCharts;
        isLoadMore = false;
    }

    public CoupleChartGestureListener(OnEdgeListener edgeListener, BarLineChartBase srcChart,
                                      Chart... dstCharts) {
        this.edgeListener = edgeListener;
        this.srcChart = srcChart;
        this.dstCharts = dstCharts;
        isLoadMore = true;
    }

    @Override
    public void onChartGestureStart(MotionEvent me, ChartTouchListener.ChartGesture lastPerformedGesture) {
        canLoad = false;
        syncCharts();
        chartGestureStart(me, lastPerformedGesture);
    }

    @Override
    public void onChartGestureEnd(MotionEvent me, ChartTouchListener.ChartGesture lastPerformedGesture) {
        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);
    }

    @Override
    public void onChartLongPressed(MotionEvent me) {
        syncCharts();
        chartLongPressed(me);
    }

    @Override
    public void onChartDoubleTapped(MotionEvent me) {
        syncCharts();
        chartDoubleTapped(me);
    }

    @Override
    public void onChartSingleTapped(MotionEvent me) {
        syncCharts();
        chartSingleTapped(me);
    }

    @Override
    public void onChartFling(MotionEvent me1, MotionEvent me2, float velocityX, float velocityY) {
        syncCharts();
    }

    @Override
    public void onChartScale(MotionEvent me, float scaleX, float scaleY) {
        syncCharts();
    }

    /**
     * 由於在外部設定了禁止慣性甩動(因為和Chart的move方法有衝突),
     * if中的語句實際上不會執行(整個手勢互動結束後,最後回撥的方法是onChartGestureEnd,而不是onChartTranslate),
     * 這樣寫是為了統一允許慣性甩動的情況
     */
    @Override
    public void onChartTranslate(MotionEvent me, float dX, float dY) {
        if (canLoad) {
            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);
                }
            }
        }
        syncCharts();
        chartTranslate(me, dX, dY);
    }

    //以下6個方法僅為了:方便在外部根據需要自行重寫
    public void chartGestureStart(MotionEvent me, ChartTouchListener.ChartGesture lastPerformedGesture) {}
    public void chartGestureEnd(MotionEvent me, ChartTouchListener.ChartGesture lastPerformedGesture) {}
    public void chartLongPressed(MotionEvent me) {}
    public void chartDoubleTapped(MotionEvent me) {}
    public void chartSingleTapped(MotionEvent me) {}
    public void chartTranslate(MotionEvent me, float dX, float dY) {}

    private void syncCharts() {
        Matrix srcMatrix;
        float[] srcVals = new float[9];
        Matrix dstMatrix;
        float[] dstVals = new float[9];
        // get src chart translation matrix:
        srcMatrix = srcChart.getViewPortHandler().getMatrixTouch();
        srcMatrix.getValues(srcVals);
        // apply X axis scaling and position to dst charts:
        for (Chart dstChart : dstCharts) {
            dstMatrix = dstChart.getViewPortHandler().getMatrixTouch();
            dstMatrix.getValues(dstVals);

            dstVals[Matrix.MSCALE_X] = srcVals[Matrix.MSCALE_X];
            dstVals[Matrix.MSKEW_X] = srcVals[Matrix.MSKEW_X];
            dstVals[Matrix.MTRANS_X] = srcVals[Matrix.MTRANS_X];
            dstVals[Matrix.MSKEW_Y] = srcVals[Matrix.MSKEW_Y];
            dstVals[Matrix.MSCALE_Y] = srcVals[Matrix.MSCALE_Y];
            dstVals[Matrix.MTRANS_Y] = srcVals[Matrix.MTRANS_Y];
            dstVals[Matrix.MPERSP_0] = srcVals[Matrix.MPERSP_0];
            dstVals[Matrix.MPERSP_1] = srcVals[Matrix.MPERSP_1];
            dstVals[Matrix.MPERSP_2] = srcVals[Matrix.MPERSP_2];

            dstMatrix.setValues(dstVals);
            dstChart.getViewPortHandler().refresh(dstMatrix, dstChart, true);
        }
    }

    public interface OnEdgeListener {
        void edgeLoad(float x, boolean left);
    }
}

給CombinedChart和BarChart設定手勢監聽,並實現CoupleChartGestureListener.OnEdgeListener介面,在回撥時請求資料載入更多。

    private CoupleChartGestureListener ccGesture;
    private CoupleChartGestureListener bcGesture;
    private int[] KL_INTERVAL = {1, 5, 15, 30, 60, 1440};//單位: Min
    private final long M1 = 60 * 1000L;//1 Min的毫秒數

    ccGesture = new CoupleChartGestureListener(this, cc, bc);//設定成全域性變數,後續要用到
    cc.setOnChartGestureListener(ccGesture);//設定手勢聯動監聽
    bcGesture = new CoupleChartGestureListener(this, bc, cc);
    bc.setOnChartGestureListener(bcGesture);

    /**
     * 滑動到邊緣後加載更多
     */
    @Override
    public void edgeLoad(float x, boolean left) {
        int v = (int) x;
        if (!left && !xValues.containsKey(v) && xValues.containsKey(v - 1)) {
            v = v - 1;
        }
        String time = xValues.get(v);
        if (!TextUtils.isEmpty(time)) {
            try {
                long t = sdf.parse(time).getTime();
                if (!left) {//向右獲取資料時判斷時間間隔
                    long interval = KL_INTERVAL[tabLayout.getSelectedTabPosition()] * M1;
                    if (System.currentTimeMillis() - t < interval) {//不會有新資料
                        return;
                    }
                }
                loadingDialog = LoadingDialog.newInstance();
                loadingDialog.show(this);
                toLeft = left;
                getData(t * 1000000L + "");
            } catch (ParseException e) {
                e.printStackTrace();
            }
        }
    }

這樣就實現了滑動聯動,以及滑動邊緣載入更多。以下是帶有時間間隔的效果圖:

 當時間間隔為1m時,K線圖也就成了分時圖,取每分鐘的收盤價來繪製分時圖,此時不再繪製蠟燭圖和均線圖。

在初始化圖表時,初始化分時線的LineDataSet:

    private LineDataSet lineSetMin;//分時線

    //在初始化圖表方法initChart()中新增分時線的初始化
    lineSetMin = new LineDataSet(new ArrayList<Entry>(), "Minutes");
    lineSetMin.setAxisDependency(YAxis.AxisDependency.LEFT);
    lineSetMin.setColor(Color.WHITE);
    lineSetMin.setDrawCircles(false);
    lineSetMin.setDrawValues(false);
    lineSetMin.setDrawFilled(true);
    lineSetMin.setHighlightEnabled(false);
    lineSetMin.setFillColor(gray);
    lineSetMin.setFillAlpha(60);

配置資料方法也修改如下:

    /**
     * size是指追加資料之前,已有的資料個數
     */
    private void handleData(List<List<String>> lists, int size) {
        if (toLeft) {
            dataList.addAll(0, lists);//新增到左側
        } else {
            dataList.addAll(lists);
        }

        configData();
        if (xValues.size() > 0) {
            int x = xValues.size() - (toLeft ? size : 0);
            //如果設定了慣性甩動 move方法將會無效
            if (!toLeft && size > 0) {
                cc.moveViewToAnimated(x, 0, YAxis.AxisDependency.LEFT, 200);
                bc.moveViewToAnimated(x + barOffset, 0, YAxis.AxisDependency.LEFT, 200);
            } else {
                cc.moveViewToX(x);
                bc.moveViewToX(x + barOffset);
            }
            cc.notifyDataSetChanged();
            bc.notifyDataSetChanged();
        }
    }

    private void configData() {
        if (dataList.size() == 0) {
            cc.setNoDataText("暫無相關資料");
            cc.clear();
            bc.setNoDataText("暫無相關資料");
            bc.clear();
        } else {
            if (combinedData == null) {
                combinedData = new CombinedData();
            }
            xValues.clear();
            List<CandleEntry> candleValues = candleSet.getValues();
            candleValues.clear();
            List<Entry> ma5Values = lineSet5.getValues();
            ma5Values.clear();
            List<Entry> ma10Values = lineSet10.getValues();
            ma10Values.clear();
            List<Entry> minValues = lineSetMin.getValues();
            minValues.clear();
            List<BarEntry> barValues = barSet.getValues();
            barValues.clear();
            for (int i = 0; i < dataList.size(); i++) {
                List<String> k = dataList.get(i);
                Date d = new Date(Long.parseLong(k.get(6)) * 1000);//毫秒
                String x = sdf.format(d);//顯示日期
                if (xValues.containsValue(x)) {//x重複
                    dataList.remove(i);
                    i--;
                } else {
                    xValues.put(i, x);
                    float open = Float.parseFloat(k.get(4));
                    float close = Float.parseFloat(k.get(1));
                    candleValues.add(new CandleEntry(i, Float.parseFloat(k.get(2)),
                            Float.parseFloat(k.get(3)), open, close));
                    minValues.add(new Entry(i, close));
                    barValues.add(new BarEntry(i, Float.parseFloat(k.get(8)), close >= open ? 0 : 1));
                    if (i >=4) {
                        ma5Values.add(new Entry(i, getMA(i, 5)));
                        if (i >= 9) {
                            ma10Values.add(new Entry(i, getMA(i, 10)));
                        }
                    }
                }
            }
            candleSet.setValues(candleValues);
            lineSet5.setValues(ma5Values);
            lineSet10.setValues(ma10Values);
            lineSetMin.setValues(minValues);
            if (tabLayout.getSelectedTabPosition() == 0) {
                combinedData.removeDataSet(candleSet);//分時圖時移除蠟燭圖
                combinedData.setData(new LineData(lineSetMin));
            } else {
                combinedData.setData(new CandleData(candleSet));
                combinedData.setData(new LineData(lineSet5, lineSet10));
            }

            cc.setData(combinedData);
            float xMax = xValues.size() - 0.5F;//預設X軸最大值是 xValues.size() - 1
            cc.getXAxis().setAxisMaximum(xMax);//使最後一個顯示完整

            barSet.setValues(barValues);
            BarData barData = new BarData(barSet);
            barData.setBarWidth(1 - candleSet.getBarSpace() * 2);//使Candle和Bar寬度一致
            bc.setData(barData);
            bc.getXAxis().setAxisMaximum(xMax + barOffset);//保持邊緣對齊

            cc.setVisibleXRange(range, range);//設定顯示X軸個數的上下限,豎屏固定52個
            bc.setVisibleXRange(range, range);
        }
    }

配置資料之後,執行開頭提到的顯示邏輯,即向右載入更多時,圖表移動到最右端,向左載入更多時,圖表移動到載入之前的位置。因為每次載入都是重新給圖表設定資料,載入之前的位置(即最左端)是0,而載入之後左端被填充了(xValues.size() -  size),因此需要從0(每次設定資料後都是在0)移動到(xValues.size() -  size)。圖表移動後呼叫notifyDataSetChanged()方法,可以避免圖表閃動的問題。

最後,監聽TabLayout的選中變化,每次選中(以及重複選中時)都重新載入。以下是分時圖效果:

 不同時間間隔顯示圖表不同的效果已經實現。

下一步要做的是:

長按觸發高亮,以及高亮效果、橫豎屏效果。