在MPAndroidChart库K线图的基础上画均线
CombinedChart
可以直接使用MPAndroidChart库里面提供的CombinedChart实现组合图形
Demo:CombinedChartDemo
——————分割线(如果想在一个图形上实现,可以参考下面的实现方式)——————
推荐直接使用CombinedChart实现
在GandleStickChart的基础上画均线
之前出过一篇MPAndroidChart的K线图上添加均线,但是在画均线的逻辑上有点问题,画出的均线永远是屏幕上显示的数据的均线,而不是全部数据的均线
在此调整了一下画均线的逻辑,并在上个版本上做了优化,使滑动更流畅,效果图如下:
画K线图的原理
在说画均线之前,先简单说说画K线图的流程
因为公司项目没有开源,现在只是简单聊下思路,贴下简单的代码(都是开源库里有的代码)
如果你已经用过并阅读过MPAndroidChart的代码,那么下面的东西,你一定能看懂。
步骤:
- 首先,库已经给我们封装了一个自定义控件,叫做CandleStickChart,专门是用来画K线的,我们把库引入到工程以后,再自定义一个类继承CandleStickChart控件,然后在我们自定义的控件里随意定义我们自己的K线图,就好了。
- 自定义的内容一般就是描述文字,X轴、Y轴的一些位置和样式,滑动、缩放动画,等等
自定义完我们的控件以后,就要显示数据了,如果你下载过Demo,你应该看过显示数据部分的实现,传递的数据一个是X轴的ArrayList和一个是Y轴的ArrayList,X轴一般是时间维度,Y轴传递的是每个时间维度要显示的数据对象,包括index、最高、最低、开盘、收盘,类似这样:
yVals1.add(new CandleEntry(i, high, low, open, close));
数据传过去以后,就到了画数据的部分了,画数据的核心方法是CandleStickChartRenderer.java 的 drawDataSet(Canvas c, CandleDataSet dataSet)方法,在这个方法里,通过mRenderPaint画K线的每个节点(显示在屏幕上的节点)
- 动画,缩放等等,我们暂时就不操心了。
整个K线从无到有,大概就是这样的一个流程被画出来,那么如何添加均线呢
画均线思路
通过上面步骤的第三步,想要画均线,每个节点只传开盘、收盘、最高、最低,明显是不够的,那么在传递数据的时候,就需要在外面把每个节点的均值,算出来(均线的计算方法可以参考之前的文章),一块传递过去(用于画均线),修改了一下CandleEntry的数据结构,添加了5节点均线值、10节点均线值、30节点均线值的属性,并添加了get、set方法,如果没有值,用-1表示,如下
Code
public class CandleEntry extends Entry { …… private float ma_5 = 0f; private float ma_10 = 0f; private float ma_30 = 0f; public CandleEntry(int xIndex, float shadowH, float shadowL, float open, float close,float ma5,float ma10,float ma30) { super((shadowH + shadowL) / 2f, xIndex); this.mShadowHigh = shadowH; this.mShadowLow = shadowL; this.mOpen = open; this.mClose = close; this.ma_5 = ma5; this.ma_10 = ma10; this.ma_30 = ma30; } public float getMa_5() { return ma_5; } public void setMa_5(float ma_5) { this.ma_5 = ma_5; } public float getMa_10() { return ma_10; } public void setMa_10(float ma_10) { this.ma_10 = ma_10; } public float getMa_30() { return ma_30; } public void setMa_30(float ma_30) { this.ma_30 = ma_30; } …… }
每个节点的均值计算以及数据传递的代码大概是下面这个样子:
Code
for (int i = 0; i < kLineInfo.getData().size(); i++) { float high = kLineInfo.getData().get(i).getHigh(); float low = kLineInfo.getData().get(i).getLow(); float open = kLineInfo.getData().get(i).getOpen(); float close = kLineInfo.getData().get(i).getClose(); float ma5 = -1; if (i >= 4) { ma5 = 0; for (int a = i - 4; a <= i; a++) { ma5 += kLineInfo.getData().get(a).getClose(); } ma5 /= 5; } float ma10 = -1; if (i >= 9) { ma10 = 0; for (int a = i - 9; a <= i; a++) { ma10 += kLineInfo.getData().get(a).getClose(); } ma10 /= 10; } float ma30 = -1; if (i >= 29) { ma30 = 0; for (int a = i - 29; a <= i; a++) { ma30 += kLineInfo.getData().get(a).getClose(); } ma30 /= 30; } yVals1.add(new CandleEntry(i, high, low, open, close, ma5, ma10, ma30)); }
显示
在画均线的之前,需要通过如下代码将传递过来的数据转换成在页面上显示的具体位置,存储到buffer里:
因为我们自己添加了ma5、ma10、ma30的属性,所以在转换的过程我们也要做一定的处理
Code
package ……; import ……; public class CandleBodyBuffer extends AbstractBuffer<CandleEntry> { …… private void addBody(float left, float top, float right, float bottom, float ma5, float ma10, float ma30) { buffer[index++] = left; buffer[index++] = top; buffer[index++] = right; buffer[index++] = bottom; buffer[index++] = ma5; buffer[index++] = ma5; buffer[index++] = ma10; buffer[index++] = ma10; buffer[index++] = ma30; buffer[index++] = ma30; } @Override public void feed(List<CandleEntry> entries) { int size = (int) Math.ceil((mTo - mFrom) * phaseX + mFrom); for (int i = mFrom; i < size; i++) { CandleEntry e = entries.get(i); addBody(e.getXIndex() - 0.5f + mBodySpace, e.getClose() * phaseY, e.getXIndex() + 0.5f - mBodySpace, e.getOpen() * phaseY, e.getMa_5() * phaseY, e.getMa_10() * phaseY, e.getMa_30() * phaseY); } reset(); } }
Code
package ……; import ……; public class CandleShadowBuffer extends AbstractBuffer<CandleEntry> { public CandleShadowBuffer(int size) { super(size); } private void addShadow(float x1, float y1, float x2, float y2, float ma5, float ma10, float ma30) { buffer[index++] = x1; buffer[index++] = y1; buffer[index++] = x2; buffer[index++] = y2; buffer[index++] = ma5; buffer[index++] = ma5; buffer[index++] = ma10; buffer[index++] = ma10; buffer[index++] = ma30; buffer[index++] = ma30; } @Override public void feed(List<CandleEntry> entries) { int size = (int)Math.ceil((mTo - mFrom) * phaseX + mFrom); for (int i = mFrom; i < size; i++) { CandleEntry e = entries.get(i); addShadow(e.getXIndex(), e.getHigh() * phaseY, e.getXIndex(), e.getLow() * phaseY, e.getMa_5() * phaseY, e.getMa_10() * phaseY, e.getMa_30() * phaseY); } reset(); } }
在DataRenderer.java里定义一个画均线的画笔
protected Paint mMAPaint;
public DataRenderer(ChartAnimator animator, ViewPortHandler viewPortHandler) {
super(viewPortHandler);
……
mMAPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mMAPaint.setStrokeWidth(1);
mMAPaint.setColor(0xFFC7238B);
}
画均线
@Override
public void initBuffers() {
CandleData candleData = mChart.getCandleData();
mShadowBuffers = new CandleShadowBuffer[candleData.getDataSetCount()];
mBodyBuffers = new CandleBodyBuffer[candleData.getDataSetCount()];
for (int i = 0; i < mShadowBuffers.length; i++) {
CandleDataSet set = candleData.getDataSetByIndex(i);
mShadowBuffers[i] = new CandleShadowBuffer(set.getValueCount() * 10);
mBodyBuffers[i] = new CandleBodyBuffer(set.getValueCount() * 10);
}
}
protected void drawDataSet(Canvas c, CandleDataSet dataSet) {
……
int range = (maxx - minx) * 10;
……
float tempMA5X = -1;
float tempMA5Y = -1;
float tempMA10X = -1;
float tempMA10Y = -1;
float tempMA30X = -1;
float tempMA30Y = -1;
// draw the body
for (int j = 0; j < range; j += 10) {
……
float leftBody = bodyBuffer.buffer[j];
float open = bodyBuffer.buffer[j + 1];
float rightBody = bodyBuffer.buffer[j + 2];
float close = bodyBuffer.buffer[j + 3];
float ma5 = bodyBuffer.buffer[j + 5];
float ma10 = bodyBuffer.buffer[j + 7];
float ma30 = bodyBuffer.buffer[j + 9];
……
// 画5均线
if (e.getMa_5() != -1) {
if (tempMA5X != -1 && tempMA5Y != -1) {
// 画线
mMAPaint.setColor(0xFFC7238B);
c.drawLine(tempMA5X, tempMA5Y, (leftBody + rightBody) / 2, ma5, mMAPaint);
}
// 赋值
tempMA5X = (leftBody + rightBody) / 2;
tempMA5Y = ma5;
}
// 画10均线
if (e.getMa_10() != -1) {
if (tempMA10X != -1 && tempMA10Y != -1) {
// 画线
mMAPaint.setColor(0xFFDFAA2A);
c.drawLine(tempMA10X, tempMA10Y, (leftBody + rightBody) / 2, ma10, mMAPaint);
}
// 赋值
tempMA10X = (leftBody + rightBody) / 2;
tempMA10Y = ma10;
}
// 画30均线
if (e.getMa_30() != -1) {
if (tempMA30X != -1 && tempMA30Y != -1) {
// 画线
mMAPaint.setColor(0xFF268BC6);
c.drawLine(tempMA30X, tempMA30Y, (leftBody + rightBody) / 2, ma30, mMAPaint);
}
// 赋值
tempMA30X = (leftBody + rightBody) / 2;
tempMA30Y = ma30;
}
}
}
到此,搞定!解决了上一个方案重复绘制均线,导致滑动不是很流畅的问题。
说明
对于上面buffer为什么存了2遍
buffer[index++] = ma5;
buffer[index++] = ma5;
buffer[index++] = ma10;
buffer[index++] = ma10;
buffer[index++] = ma30;
buffer[index++] = ma30;
因为之前,里边只有4个属性,现在加了3个属性以后,他转换的计算有点问题
以前的方法可能是针对这4个属性转换的,我debug观察了下转换后的结果,数组的奇数角标和偶数角标位置的计算逻辑是不一样的
那么为了不改它太深层的东西(怕影响到其他图形,也不想给自己添加麻烦),我干脆就都赋值了两遍
但是我取值的时候,只取我想要的
float ma5 = bodyBuffer.buffer[j + 5];
float ma10 = bodyBuffer.buffer[j + 7];
float ma30 = bodyBuffer.buffer[j + 9];
this all!!