*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布
蜡烛图的绘制如下,特别注意边界的处理,当处理可视范围内第一个Quotes时leftRectX、leftLineX必须判断和左边界的关系,取最大的,不然在界面的表现就是蜡烛图会绘制出左边界。同理,可视范围内结束点也需要考虑这个问题,同时,右侧有一个内边距需要考虑进去。
/**
* 绘制蜡烛图
* @param canvas
* @param diverWidth 蜡烛图之间的间距
* @param List index
* @param quotes 目标Quotes
*/
private void drawCandleViewProcess(Canvas canvas, float diverWidth, int i, Quotes quotes) {
//异常拦截以及参数设置
...
//定位蜡烛矩形的四个点
topRectY = (float) (mPaddingTop + mInnerTopBlankPadding +
mPerY * (mCandleMaxY - quotes.o));
bottomRectY = (float) (mPaddingTop + mInnerTopBlankPadding +
mPerY * (mCandleMaxY - quotes.c));
leftRectX = -mPerX / 2 + quotes.floatX + diverWidth / 2;
rightRectX = mPerX / 2 + quotes.floatX - diverWidth / 2;
//定位单个蜡烛中间线的四个点
leftLineX = quotes.floatX;
topLineY = (float) (mPaddingTop + mInnerTopBlankPadding +
mPerY * (mCandleMaxY - quotes.h));
rightLineX = quotes.floatX;
bottomLineY = (float) (mPaddingTop + mInnerTopBlankPadding +
mPerY * (mCandleMaxY - quotes.l));
RectF rectF = new RectF();
//边界处理
if (i == mBeginIndex) {
leftRectX = leftRectX < mPaddingLeft ? mPaddingLeft : leftRectX;
leftLineX = leftLineX < mPaddingLeft ? mPaddingLeft : leftLineX;
} else if (i == (mEndIndex - 1)) {
rightRectX = rightRectX > mWidth - mPaddingRight ? mWidth - mPaddingRight : rightRectX;
rightLineX = rightLineX > mWidth - mPaddingRight ? mWidth - mPaddingRight : rightLineX;
}
rectF.set(leftRectX, topRectY, rightRectX, bottomRectY);
//设置颜色
mCandlePaint.setColor(quotes.c > quotes.o ? mRedCandleColor : mGreenCandleColor);
//绘制蜡烛图
canvas.drawRect(rectF, mCandlePaint);
//开始画low、high线
canvas.drawLine(leftLineX, topLineY, rightLineX, bottomLineY, mCandlePaint);
}
在本项目中MA取最常用的5单位、10单位、20单位,简称MA(5,10,20);BOLL取常用的26单位,简称BOLL(26)。当然,在本项目中到的各种指标算法全部都抽取出来,在FinancialAlgorithm.java中。一般指标算法都是比较繁琐的,要么是多日均值,要么就是加权等等,所以对算法要求尽可能的高效。下面是MA的计算,之前写的是采用双重循环,改后之后只要一层即可,尽可能避免多重循环。
/**
* MA算法,period(周期)的MA的计算:带上今天,向前取period的收盘价之和除以period,即是今日的MA(period)。
* 算法很简洁,但是是对的。
*
* @param period 周期,一般是:5、10、20、30、60
*/
public static void calculateMA(List quotesList, int period) {
if (quotesList == null || quotesList.isEmpty()) return;
if (period < 0 || period > quotesList.size() - 1) return;
double sum1 = 0;
for (int i = 0; i < quotesList.size(); i++) {
Quotes quotes = quotesList.get(i);
sum1 += quotes.c;
//边界
if (i < period - 1) {
continue;
}
if (i > period - 1) {
sum1 -= quotesList.get(i - period).c;//减去之前算过的。
}
if (period == 5) {
quotes.ma5 = sum1 / period;
} else if (period == 10) {
quotes.ma10 = sum1 / period;
} else if (period == 20) {
quotes.ma20 = sum1 / period;
} else {
Log.e(TAG, "calculateMA: 没有该种period,TODO:完善Quotes");
return;
}
}
}
对于主图指标的切换,采用的是单击click,四种类型。对于指标的绘制其实就是绘制Path,只要能确认x轴和y轴对应坐标即可。对于x轴上文已经说了,取单个蜡烛的x方向即可。对于y轴,这里取的是对应Quotes的MA值,当然这里的MA有三种MA5、MA10、MA20,因此画出来也是三条线。有心的同学看上面计算MA的算法有一个判断if (i < period - 1) {continue;}
,对于i < period - 1
直接就continue,意味着就没有对应的MA值。是的,对于数据集合当开始的时候是没有MA值的,有些资料会取一些默认值进行显示,这里的处理方式是不进行计算,在View的效果就是不绘制(效果如下图)。BOLL同理。
/**
* 主图上面的技术指标类型
*/
public enum MasterType {
NONE,//无
MA,//MA5、10、20
BOLL,//BOLL(26)
MA_BOLL//MA5、10、20和BOLL(26)同时展示
}
private void drawMa(Canvas canvas) {
//参数初始化
...
for (int i = mBeginIndex; i < mEndIndex; i++) {
Quotes quotes = mQuotesList.get(i);
//在绘制蜡烛图的时候已经计算了
float floatX = quotes.floatX;
//绘制MA5的逻辑
float floatY = getMasterDetailFloatY(quotes, MasterDetailType.MA5);
//异常,在View的行为就是不显示而已,影响不大。一般都是数据的开头部分。
if (floatY == -1) continue;
if (isFirstMa5) {
isFirstMa5 = false;
path5.moveTo(floatX, floatY);
} else {
path5.lineTo(floatX, floatY);
}
//这里是绘制MA10/20的逻辑,和MA5一样
...
}
canvas.drawPath(path5, mMa5Paint);
canvas.drawPath(path10, mMa10Paint);
canvas.drawPath(path20, mMa20Paint);
}
在开始之前看以下两个截图,对于同一个时刻一个不展示BOLL线,一个展示BOLL线,绘制出来的蜡烛图折线图是“不一样”的。
这并不是绘制错误,而是特意的处理。在绘制分时图的时候,我们对于Y轴的mPerY的计算是根据可视范围内数据集合的收盘价最大值和最小值以及Y轴有效高度计算的。这样的处理可以保证无论分时图浮动多大,可以保证分时图不会绘制出边界。分时图“值”比较单一,只需要考虑一个收盘价即可。可是,对于蜡烛图需要考虑的就比较多了,如果还是仅仅通过收盘价计算最小值和最大值,就会导致如果计算出来的“待显示的点”如果远远大于收盘价,就会导致“越界”!并且,这种情况,确实在开发过程中遇到了。因此,对于蜡烛图最大值和最小值的判断,需要遍历单个Quotes,寻找到最大和最小值。通过观察线上应用【天厚实盘】以及同花顺基本上都是这样处理的:随着主图上指标的变化,蜡烛图显示的高度并不一致,为的就是保证“不越界”。以下是寻找单个Quotes中最大值的过程,特别注意,if判断逻辑不能用else if
,这个错误调试了几个小时才找到!
/**
* 找到单个报价中的最大值
*
* @param quotes
* @param masterType
* @return
*/
public static double getMasterMaxY(Quotes quotes, MasterView.MasterType masterType) {
double max = Integer.MIN_VALUE;
//ma
//只有在存在ma的情况下才计算
if (masterType == MasterView.MasterType.MA || masterType == MasterView.MasterType.MA_BOLL) {
if (quotes.ma5 != 0 && quotes.ma5 > max) {
max = quotes.ma5;
}
if (quotes.ma10 != 0 && quotes.ma10 > max) {
max = quotes.ma10;
}
if (quotes.ma20 != 0 && quotes.ma20 > max) {
max = quotes.ma20;
}
}
//boll
if (masterType == MasterView.MasterType.BOLL || masterType == MasterView.MasterType.MA_BOLL) {
if (quotes.mb != 0 && quotes.mb > max) {
max = quotes.mb;
}
if (quotes.up != 0 && quotes.up > max) {
max = quotes.up;
}
if (quotes.dn != 0 && quotes.dn > max) {
max = quotes.dn;
}
}
//quotes
if (quotes.h != 0 && quotes.h > max) {
max = quotes.h;
}
//没有找到
if (max == Integer.MIN_VALUE) {
max = 0;
}
return max;
}
在处理的时候,把和业务没有关系的View逻辑放到了BaseFinancialView中,比如各种常量的获取、View宽高的测量、以及View的工具类等。而在KView.java中则是处理主图和副图全部都有(或者部分有)的共有属性。对于部分有的特性,比如副图没有x轴的虚线绘制,可以用一个开关在父类中处理是否绘制,开关可以由子类控制。
/**
* 绘制内部x/y轴虚线
* @param canvas
*/
protected void drawInnerXy(Canvas canvas) {
if (isShowInnerX())
drawInnerX(canvas);
if (isShowInnerY())
drawInnerY(canvas);
}
代码继承关系整好之后,一些线基本就OK了,下面是副图的截图,基本没有做任何处理,就可以直接显示了
而一些父类没法实现具体逻辑的,可以直接声明为抽象类,让子类去实现即可。
/**
* 父类:寻找边界和计算单元数据大小。寻找:x轴开始位置数据和结束位置的model、y轴的最大数据和最小数据对应的model;
* 计算x/y轴数据单元大小。这个交给子类去实现,一般情况下寻找边界需要遍历,在父类中遍历没有意义,
* 因为不知道子类还有什么遍历需求。因此改为抽象方法,子类实现。子类必须完成寻找边界的任务。
*/
protected abstract void seekAndCalculateCellData();
-------------------------------------------------------
//MasterView实现具体逻辑。
@Override
protected void seekAndCalculateCellData() {
if (mQuotesList.isEmpty()) return;
//对于蜡烛图,需要计算以下指标。
if (mViewType == ViewType.CANDLE) {
//指标算法
...
}
//寻找边界和计算单元数据大小
...
//计算mPerX、mPerY
...
//重绘
invalidate();
}
通过代码重构的手段,实现了在分时图、蜡烛图以及蜡烛图对应指标、其它各种交互共存的情况下,MasterView的代码行数还是维持在1000多行,MasterView基本没有增加代码量。现在存在的问题是,仍然存在各种各样的属性散列在View中,感觉难以控制。MP作者PhilJay在处理这个问题是将所有属性聚合成一个对象,在使用时可能会有很多层调用,但是在使用者(开发者)层面,看到的只有一个对象,然后控制着View的各种表现。在下一阶段重构的过程中,会考虑属性的处理方式是否也采用这种方式。
https://github.com/scsfwgy/FinancialCustomerView