目录
使用MPAndroidChart实现K线图(1)——基本用法
使用MPAndroidChart实现K线图(2)——自定义XY轴
使用MPAndroidChart实现K线图(3)——自定义柱状图
使用MPAndroidChart实现K线图(4)——图表联动、加载更多
使用MPAndroidChart实现K线图(5)——高亮联动、横竖屏切换
首先说一下图表联动和加载更多流程逻辑。图表联动是指,当滑动上部分的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,命名为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(), "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> 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 candleValues = candleSet.getValues();
candleValues.clear();
List ma5Values = lineSet5.getValues();
ma5Values.clear();
List ma10Values = lineSet10.getValues();
ma10Values.clear();
List minValues = lineSetMin.getValues();
minValues.clear();
List barValues = barSet.getValues();
barValues.clear();
for (int i = 0; i < dataList.size(); i++) {
List 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的选中变化,每次选中(以及重复选中时)都重新加载。以下是分时图效果:
不同时间间隔显示图表不同的效果已经实现。
下一步要做的是:
长按触发高亮,以及高亮效果、横竖屏效果。
完整demo地址:https://github.com/ShallowBillow/KlineDemo