使用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的選中變化,每次選中(以及重複選中時)都重新載入。以下是分時圖效果:
不同時間間隔顯示圖表不同的效果已經實現。
下一步要做的是:
長按觸發高亮,以及高亮效果、橫豎屏效果。