的
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,80 @@
|
||||
## 金融类自定义view
|
||||
|
||||
#### FundView.view
|
||||
|
||||
> 仿蚂蚁金服基金收益趋势图
|
||||
|
||||
* 贡献代码
|
||||
|
||||
新建分支
|
||||
|
||||
* v1.0 完成x轴日期提示文字、y轴收益提示文字、x轴虚线、折线图
|
||||
|
||||

|
||||
|
||||
* v1.1 完成长按显示距离按下焦点最近的日期以及对应收益点十字、默认上方提示收益、按下上方显示对应点的时间和累计收益、延迟消失十字效果(仿蚂蚁金融)
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
#### TimeSharingView.view
|
||||
|
||||
> 股票、外汇等交易类的分时图。版本已稳定。
|
||||
|
||||
* 贡献代码
|
||||
|
||||
提交到[timesharing]分支
|
||||
|
||||
* 第一阶段,参数准备,外边框、内虚线、折线图等的绘制
|
||||
|
||||

|
||||
|
||||
* 第二阶段,x、y文字、实时横线和实时数据、下方透明阴影
|
||||
|
||||

|
||||
|
||||
* 第三阶段,实时数据更新分时图
|
||||
|
||||

|
||||
|
||||
* 第四阶段,长按十字线,左右移动分时图
|
||||
|
||||

|
||||
|
||||
* 第五阶段,长按实时显示详细数据、加载更多处理、实时横线优化、滑动优化
|
||||
|
||||

|
||||
|
||||
* 第六阶段,缩放、代码整理、注释
|
||||
|
||||

|
||||
|
||||
|
||||
#### CanldeView.view
|
||||
|
||||
> 股票、外汇等交易类的蜡烛图。版本不稳定,正在完善中,相关代码在'feature_candleview'分支
|
||||
|
||||
* 贡献代码
|
||||
|
||||
提交到[feature_candleview]分支
|
||||
|
||||
* 第一阶段,代码的大量重构,抽出父类,减少子类代码量。代码优化,减少for循环层次。
|
||||
* 第二阶段,完成MA、BOLL相关算法。
|
||||
* 第三阶段,完成蜡烛图。
|
||||
|
||||

|
||||
|
||||
|
||||
* 第四阶段,MA指标图、BOLL指标图、蜡烛图长按处理
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
#### KView.view
|
||||
|
||||
> 主图和副图的组合图已经开发完毕,可以下载demo apk体验:[https://github.com/scsfwgy/FinancialCustomerView/tree/master/apk](https://github.com/scsfwgy/FinancialCustomerView/tree/master/apk)
|
||||
效果视频:[https://github.com/scsfwgy/FinancialCustomerView/blob/master/img/%E4%B8%BB%E5%9B%BE%E5%89%AF%E5%9B%BE%E7%BB%84%E5%90%88%E5%9B%BEdemo%E8%A7%86%E9%A2%91.mp4](https://github.com/scsfwgy/FinancialCustomerView/blob/master/img/%E4%B8%BB%E5%9B%BE%E5%89%AF%E5%9B%BE%E7%BB%84%E5%90%88%E5%9B%BEdemo%E8%A7%86%E9%A2%91.mp4)
|
||||
@@ -0,0 +1,155 @@
|
||||
# 整理算法
|
||||
## 简称和命名
|
||||
### Quotes
|
||||
* 最原始的Quote对象包含五个字段:t,o,c,l,h
|
||||
* t 时间,该报价单元生成的时间戳
|
||||
* o 开盘价,该报价单元的开始报价
|
||||
* c 闭盘价,该报价单元的结束报价
|
||||
* l 最低价,该报价单元的最低价
|
||||
* h 最高价,该报价单元的最高价
|
||||
|
||||
## 算法和示例代码
|
||||
### MA
|
||||
> 移动平均线,Moving Average,简称MA,MA是用统计分析的方法,将一定时期内的证券价格(指数)加以平均,并把不同时间的平均值连接起来,形成一根MA,用以观察证券价格变动趋势的一种技术指标。[[百度百科:移动平均线](https://baike.baidu.com/item/%E7%A7%BB%E5%8A%A8%E5%B9%B3%E5%9D%87%E7%BA%BF/217887?fr=aladdin&fromid=1511750&fromtitle=MA)]
|
||||
|
||||
#### MA算法
|
||||
* MA5,指的是5单位的移动平均线,其它同理。MA(5,10,20)指的是5单位、10单位、20单位三条移动平均线。
|
||||
* 很多资料都把MAn,称为n天的移动平均线,感觉这样是不合理的,因为折线图有很多类型,比如1分钟的、1小时的、1周的等等。所以,这里"n"不应该称为“天”,而是应该是对应的类型,所以这里直接称为“单位”。
|
||||
* 计算公式:MA =(C1+C2+C3+C4+C5+...+Cn)/n,其中C为收盘价n为移动平均周期数。例如现货黄金的5日移动平均价格计算方法为:MA5=(前四天收盘价+前三天收盘价+前天收盘价+昨天收盘价+今天收盘价)/5。
|
||||
* 指定点的MAn值的计算,就是从该点开始(包括该点)起向历史数据取n个数据单元的收盘价之和,然后取均值。
|
||||
* 特殊的,假如数据集合中最开始的n个数据,是没法计算MAn的。这里的处理方式有两种
|
||||
1. 对于最开始的n个点,直接用当日的收盘价当作对应MA值即可。
|
||||
2. 不计算,绘制时直接不绘制对应MA即可。本项目采用这种方式。
|
||||
|
||||
#### MA示例代码
|
||||
* 本项目中处理MA只计算MA5,MA10,MA20。并且直接在Quotes中定义该三个字段,计算好之后直接赋值。
|
||||
* 示例代码
|
||||
|
||||
```java
|
||||
/**
|
||||
* 计算公式:MA =(C1+C2+C3+C4+C5+...+Cn)/n,其中C为收盘价n为移动平均周期数。
|
||||
* 例如现货黄金的5日移动平均价格计算方法为:MA5=(前四天收盘价+前三天收盘价+前天收盘价+昨天收盘价+今天收盘价)/5。
|
||||
* 特殊的,假如数据集合中最开始的n个数据,是没法计算MAn的。这里的处理方式是不计算,绘制时直接不绘制对应MA即可。
|
||||
* @param quotesList 数据集合
|
||||
* @param period MAn中的n
|
||||
*/
|
||||
public static void calculateMA(List<Quotes> quotesList, int period) {
|
||||
|
||||
if (quotesList == null || quotesList.isEmpty()) return;
|
||||
|
||||
if (period < 0 || period > quotesList.size() - 1) return;
|
||||
|
||||
double sum = 0;
|
||||
for (int i = 0; i < quotesList.size(); i++) {
|
||||
//计算和
|
||||
Quotes quotes = quotesList.get(i);
|
||||
sum += quotes.c;
|
||||
if (i > period - 1) {
|
||||
sum -= quotesList.get(i - period).c;
|
||||
}
|
||||
|
||||
//边界
|
||||
if (i < period - 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (period == 5) {
|
||||
quotes.ma5 = sum / period;
|
||||
} else if (period == 10) {
|
||||
quotes.ma10 = sum / period;
|
||||
} else if (period == 20) {
|
||||
quotes.ma20 = sum / period;
|
||||
} else {
|
||||
Log.e(TAG, "calculateMA: 没有该种period,TODO:完善Quotes");
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### BOLL
|
||||
> 布林线,Boll,是股市技术分析的常用工具之一,通过计算股价的“标准差”,再求股价的“信赖区间”。该指标在图形上画出三条线,其中上下两条线可以分别看成是股价的压力线和支撑线,而在两条线之间还有一条股价平均线,布林线指标的参数最好设为20。一般来说,股价会运行在压力线和支撑线所形成的通道中。[[百度百科:布林线](https://baike.baidu.com/item/%E5%B8%83%E6%9E%97%E7%BA%BF/3424486?fr=aladdin)]
|
||||
|
||||
#### BOLL算法
|
||||
* BOLL(n),指的是n单位的BOLL线。
|
||||
* BOLL(n)包含三条线,中轨线(MB)、上轨线(UP)和下轨线(DN)。
|
||||
* BOLL(n)计算公式:
|
||||
* MA=n日内的收盘价之和÷n。
|
||||
* MD=(n-1)日的平方根(C-MA)的两次方之和除以n
|
||||
* MB=(n-1)日的MA
|
||||
* UP=MB+k×MD
|
||||
* DN=MB-k×MD
|
||||
* K为参数,可根据股票的特性来做相应的调整,一般默认为2
|
||||
* 特殊的,假如数据集合中最开始的n个数据,是没法计算BOLL(n)的。这里的处理方式有两种
|
||||
1. 对于最开始的n个点,直接用当日的收盘价当作当日的MA值即可。
|
||||
2. 不计算,绘制时直接不绘制对应BOLL即可。本项目采用这种方式。
|
||||
|
||||
#### BOLL示例代码
|
||||
* 本项目中处理MA只计算BOLL(26),k=2。并且直接在Quotes中定义mb、up、dn,计算好之后直接赋值。
|
||||
* 示例代码
|
||||
|
||||
```java
|
||||
/**
|
||||
* BOLL(n)计算公式:
|
||||
* MA=n日内的收盘价之和÷n。
|
||||
* MD=(n-1)日的平方根(C-MA)的两次方之和除以n
|
||||
* MB=(n-1)日的MA
|
||||
* UP=MB+k×MD
|
||||
* DN=MB-k×MD
|
||||
* K为参数,可根据股票的特性来做相应的调整,一般默认为2
|
||||
*
|
||||
* @param quotesList 数据集合
|
||||
* @param period 周期,一般为26
|
||||
* @param k 参数,可根据股票的特性来做相应的调整,一般默认为2
|
||||
*/
|
||||
public static void calculateBOLL(List<Quotes> quotesList, int period, int k) {
|
||||
if (quotesList == null || quotesList.isEmpty()) return;
|
||||
if (period < 0 || period > quotesList.size() - 1) return;
|
||||
|
||||
double mb;//上轨线
|
||||
double up;//中轨线
|
||||
double dn;//下轨线
|
||||
|
||||
//n日
|
||||
double sum = 0;
|
||||
//n-1日
|
||||
double sum2=0;
|
||||
for (int i = 0; i < quotesList.size(); i++) {
|
||||
Quotes quotes = quotesList.get(i);
|
||||
sum += quotes.c;
|
||||
sum2+=quotes.c;
|
||||
if (i > period - 1)
|
||||
sum -= quotesList.get(i - period).c;
|
||||
if(i>period-2)
|
||||
sum2 -= quotesList.get(i - period+1).c;
|
||||
|
||||
//这个范围不计算,在View上的反应就是不显示这个范围的boll线
|
||||
if (i < period - 1)
|
||||
continue;
|
||||
|
||||
//n日MA
|
||||
double ma = sum / period;
|
||||
//n-1日MA
|
||||
double ma2=sum2/(period-1);
|
||||
double md = 0;
|
||||
for (int j = i + 1 - period; j <= i; j++) {
|
||||
//n-1日
|
||||
md += Math.pow(quotesList.get(j).c - ma, 2);
|
||||
}
|
||||
md = Math.sqrt(md / period);
|
||||
//(n-1)日的MA
|
||||
mb = ma2;
|
||||
up = mb + k * md;
|
||||
dn = mb - k * md;
|
||||
|
||||
quotes.mb = mb;
|
||||
quotes.up = up;
|
||||
quotes.dn = dn;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
## 金融类自定义View(一)--仿蚂蚁财富基金收益曲线图
|
||||
> 该自定义View模仿了蚂蚁金服的基金收益曲线图走势。大致包含以下功能:x轴绘制等分时间坐标;y轴绘制等分收益值;中间绘制等分虚线;收益变化折线图;上方默认显示最后收益值;长按显示当前距离最近按下点的时间坐标以及对应收益值的十字线,同时上方显示对应时间与收益。看似简单的一个收益曲线图,包含很多细节技术点需要处理。虽然该自定义View比较简单,但是和金融类复杂的K线图、蜡烛图等以及相应复杂的交互本质上是一样的,会提供很好的解决思路。本文不会讲解自定义View入门知识。enjoy it~
|
||||
|
||||
#### 效果图
|
||||
* 原蚂蚁金服收益View操作GIF
|
||||
|
||||

|
||||
|
||||
* 仿操作GIF
|
||||
|
||||

|
||||
|
||||
* 截图
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
#### 中间虚线的绘制
|
||||
* 要绘制横轴那五条虚线,我们首先需要知道该控件的大小和位置,由调用者在使用时设置,控件在onMeasure()的时候获取,特别注意处理一下`AT_MOST`的情况。另外,我们需要设置上下左右的起始位置,也就是给该控件设置好Padding值,如下:
|
||||
|
||||
//上下左右padding
|
||||
float mPaddingTop = 100;
|
||||
float mPaddingBottom = 70;
|
||||
float mPaddingLeft = 50;
|
||||
float mPaddingRight = 50;
|
||||
|
||||
* 知道了绘制的起始和结束位置之后,我们需要初始化一个绘制虚线的画笔,用来在Canves上绘制虚线
|
||||
|
||||
//初始化绘制虚线的画笔
|
||||
private void initInnerXPaint() {
|
||||
mInnerXPaint = new Paint();
|
||||
mInnerXPaint.setColor(getColor(R.color.color_fundView_xLineColor));
|
||||
mInnerXPaint.setStrokeWidth(convertDp2Px(mInnerXStrokeWidth));
|
||||
mInnerXPaint.setStyle(Paint.Style.STROKE);
|
||||
setLayerType(LAYER_TYPE_SOFTWARE, null);//禁用硬件加速
|
||||
PathEffect effects = new DashPathEffect(new float[]{5, 5, 5, 5}, 1);
|
||||
mInnerXPaint.setPathEffect(effects);
|
||||
}
|
||||
|
||||
* 其实知道位置和初始化画笔之后,就可以直接绘制了。我们很容易知道最上面的那一条的起始点和结束点是`(mPaddingLeft, mPaddingTop)`,`(mWidth - mPaddingRight, mPaddingTop)`,就是左右上边界减去左右和上的Padding值。同理,下方的位置也很好确定。至于中间那三条线的确定,因为是上下平移,所以主要是确定y周的坐标。我们可以根据最下面线的y周坐标减去最上面的y轴值取均分值即可:
|
||||
|
||||
private void drawInnerXPaint(Canvas canvas) {
|
||||
//画5条横轴的虚线
|
||||
//首先确定最大值和最小值的位置
|
||||
float perHight = (mHeight - mPaddingBottom - mPaddingTop) / 4;
|
||||
|
||||
canvas.drawLine(0 + mPaddingLeft, mPaddingTop,
|
||||
mWidth - mPaddingRight, mPaddingTop, mInnerXPaint);//最上面的那一条
|
||||
|
||||
canvas.drawLine(0 + mPaddingLeft, mPaddingTop + perHight * 1,
|
||||
mWidth - mPaddingRight, mPaddingTop + perHight * 1, mInnerXPaint);//2
|
||||
|
||||
canvas.drawLine(0 + mPaddingLeft, mPaddingTop + perHight * 2,
|
||||
mWidth - mPaddingRight, mPaddingTop + perHight * 2, mInnerXPaint);//3
|
||||
|
||||
canvas.drawLine(0 + mPaddingLeft, mPaddingTop + perHight * 3,
|
||||
mWidth - mPaddingRight, mPaddingTop + perHight * 3, mInnerXPaint);//4
|
||||
|
||||
canvas.drawLine(0 + mPaddingLeft, mHeight - mPaddingBottom,
|
||||
mWidth - mPaddingRight, mHeight - mPaddingBottom, mInnerXPaint);//最下面的那一条
|
||||
|
||||
}
|
||||
|
||||
* 按照上述步骤执行完毕之后,`invalidate()`刷新界面即可。其实虚线的绘制还是比较简单的,主要是定位、定分y周轴。
|
||||
|
||||
#### x轴时间文字和y轴收益文字的定位和绘制
|
||||
* 首先定义绘制x\y轴文字的画笔、字体大小颜色等属性。当然,如果要细分x轴和y轴可以定义两套Paint,这里简单处理,只用了一个Paint:
|
||||
|
||||
//外围X、Y轴线文字
|
||||
Paint mXYPaint;
|
||||
//x、y轴指示文字字体的大小
|
||||
final float mXYTextSize = 14;
|
||||
//左侧文字距离左边线线的距离
|
||||
final float mLeftTxtPadding = 16;
|
||||
//底部文字距离底部线的距离
|
||||
final float mBottomTxtPadding = 20;
|
||||
|
||||
* 对于x轴的处理,我们只需要获取列表中第一个Model的时间值、结束位置的时间值和中间值寄存下来,然后在固定位置显示即可。对于第一个时间的位置,和绘制最下面一条虚线定位一样。中间时间文字的显示,x轴的坐标是:总宽度减去左右pading值,然后取半再加上左边pdding值。当然,你如果仔细观察,这个时候x并不是居中显示的,因为是从**中间点**开始显示的,所有会错一个一半的时间文字宽度的值,这个时候应该再减去txt.lnegth()/2。同理,最后一个时间点的x轴开始位置也是需要减去文字的宽度的,不然就会绘制出范围。特别注意,为了好看,我们让文字向下偏移了一定的距离:`float hight = mHeight - mPaddingBottom + mBottomTxtPadding;`。
|
||||
|
||||
//找到最大时间、最小时间和中间时间显示即可
|
||||
private void drawXPaint(Canvas canvas) {
|
||||
long beginTime = mFundModeList.get(0).datetime;
|
||||
long midTime = mFundModeList.get((mFundModeList.size() - 1) / 2).datetime;
|
||||
long endTime = mFundModeList.get(mFundModeList.size() - 1).datetime;
|
||||
String bengin = processDateTime(beginTime);
|
||||
String mid = processDateTime(midTime);
|
||||
String end = processDateTime(endTime);
|
||||
|
||||
//x轴文字的高度
|
||||
float hight = mHeight - mPaddingBottom + mBottomTxtPadding;
|
||||
|
||||
canvas.drawText(bengin,
|
||||
mPaddingLeft,
|
||||
hight, mXYPaint);
|
||||
|
||||
canvas.drawText(mid,
|
||||
mPaddingLeft + (mWidth - mPaddingLeft - mPaddingRight) / 2,
|
||||
hight, mXYPaint);
|
||||
|
||||
canvas.drawText(end,
|
||||
mWidth - mPaddingRight - mXYPaint.measureText(end),
|
||||
hight, mXYPaint);//特别注意x轴的处理:- mXYPaint.measureText(end)
|
||||
|
||||
}
|
||||
|
||||
|
||||
* 对于y轴文字的处理,我们并不知道最小值和最大值,所以我们需要遍历记录下最小值和最大值。然后均分取间距,在左侧绘制即可。也需要特别注意文字绘制起始点的问题。
|
||||
|
||||
#### 折线的绘制
|
||||
* 对于折线的处理就比较麻烦了。按照上述思路,x轴可以直接取有效总宽度除以数据总个数,计算出每一个点的宽度即可。对于y轴,我们可以考虑一下,y轴的收益可能小到1分,或者大到100元,怎么确定每一点的y轴位置,如何保证绘制的曲线不出有效范围?我们可以用y轴有效的总高度除以收益的最大最小区间,获取每个单位的高度。当绘制时,根据对应时间点的收益乘以单位收益,即可以计算出需要的高度:
|
||||
|
||||
//获取单个数据X/y轴的大小
|
||||
mPerX = (mWidth - mPaddingLeft - mPaddingRight) / mFundModeList.size();
|
||||
mPerY = ((mHeight - mPaddingTop - mPaddingBottom) / (mMaxFundMode.dataY - mMinFundMode.dataY));
|
||||
Log.e(TAG, "setDataList: " + mMinFundMode + "," + mMaxFundMode + "..." + mPerX + "," + mPerY);
|
||||
|
||||
* 根据上述计算出来单位高度,绘制y轴文字信息
|
||||
|
||||
private void drawYPaint(Canvas canvas) {
|
||||
//现将最小值、最大值画好
|
||||
//draw min
|
||||
float txtWigth = mXYPaint.measureText(mMinFundMode.originDataY) + mLeftTxtPadding;
|
||||
canvas.drawText(mMinFundMode.originDataY + "",
|
||||
mPaddingLeft - txtWigth,
|
||||
mHeight - mPaddingBottom, mXYPaint);
|
||||
//draw max
|
||||
canvas.drawText(mMaxFundMode.dataY + "",
|
||||
mPaddingLeft - txtWigth,
|
||||
mPaddingTop, mXYPaint);
|
||||
//因为横线是均分的,所以只要取到最大值最小值的差值,均分即可。
|
||||
float perYValues = (mMaxFundMode.dataY - mMinFundMode.dataY) / 4;
|
||||
float perYWidth = (mHeight - mPaddingBottom - mPaddingTop) / 4;
|
||||
//从下到上依次画
|
||||
for (int i = 1; i <= 3; i++) {
|
||||
canvas.drawText(mMinFundMode.dataY + perYValues * i + "",
|
||||
mPaddingLeft - txtWigth,
|
||||
mHeight - mPaddingBottom - perYWidth * i, mXYPaint);
|
||||
}
|
||||
}
|
||||
|
||||
#### 不长按上面最终收益文字的绘制
|
||||
* 我们观察可以发现,默认文字包括:绿色的小圆点、提示文字、收益金额。看似简单的几个字,也需要一个一个处理,准备不同的画笔(当然也可以共用一个画笔)。代码不再给出。
|
||||
|
||||
#### 长按上面最终收益文字的绘制
|
||||
* 这一块因为需要监测用户的长按状态,会稍微复杂一点。首先需要在`onTouchEvent(MotionEvent event)`获取长按监听:
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
mPressTime = event.getDownTime();
|
||||
break;
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
if (event.getEventTime() - mPressTime > DEF_LONGPRESS_LENGTH) {
|
||||
Log.e(TAG, "onTouchEvent: 长按了。。。");
|
||||
mPressX = event.getX();
|
||||
mPressY = event.getY();
|
||||
//处理长按后的逻辑
|
||||
showLongPressView();
|
||||
}
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
//处理松手后的逻辑
|
||||
hiddenLongPressView();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
* 至于长按后的处理,其实并不是在`showLongPressView();`中处理,这里只是去通知onCanves()方法去刷新界面。同时用一个标记位处理是否绘制十字:
|
||||
|
||||
private void showLongPressView() {
|
||||
mDrawLongPressPaint = true;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
|
||||
* 这里有一个问题,仔细观看GIF图,十字的中间位置并不一定是用户手指长按的位置,而是距离按下最近的有效时间点和对应的收益坐标的位置。这样描述可能比较难以理解,这么说吧,比如你点击的是(4,5),但是这一点并没有刚好对应的那一天(因为一天是一个区间,而坐标是一个点),而最近的一天的坐标是(4.1,5.1)。不仅仅x轴不能根据按下的位置确定,并且y轴也不能确认!需要确认到距离按下的最近的有效位置。这里的处理思路是:获取按下的x轴的位置坐标,然后遍历集合中的x轴的时间点所对应的坐标,找到间距最小的有效点,该点的x轴即有效的x轴点。为了简单处理y轴的值,这里保存的是对应点的Model,不用再去处理y轴的有效点。至于绘制十字就可以根据该有效点的x、y轴的坐标去处理:
|
||||
|
||||
/**
|
||||
* 这里处理画十字的逻辑:这里的十字不是手指按下的位置,这样没有意义。
|
||||
* 而是当前按下的距离x轴最近的时间(注意:并不一定按下对应的x轴就是有时间的,如果没有取最近的)。
|
||||
* 当取到x轴的值,之后算出来对应的y轴的值,这个才是十字对应的位置坐标。
|
||||
* 如何获取x轴最近的时间?我们可以在FundMode中定义x\y的位置参数,遍历对比找到最小即可。
|
||||
* (see: drawBrokenPaint(canvas);)
|
||||
*
|
||||
* @param canvas
|
||||
*/
|
||||
private void drawLongPress(Canvas canvas) {
|
||||
if (!mDrawLongPressPaint) return;
|
||||
|
||||
//获取距离最近按下的位置的model
|
||||
float pressX = mPressX;
|
||||
//循环遍历,找到距离最短的x轴的mode
|
||||
FundMode finalFundMode = mFundModeList.get(0);
|
||||
float minXLen = Integer.MAX_VALUE;
|
||||
for (int i = 0; i < mFundModeList.size(); i++) {
|
||||
FundMode currFunMode = mFundModeList.get(i);
|
||||
float abs = Math.abs(pressX - currFunMode.floatX);
|
||||
if (abs < minXLen) {
|
||||
finalFundMode = currFunMode;
|
||||
minXLen = abs;
|
||||
}
|
||||
}
|
||||
|
||||
//x
|
||||
canvas.drawLine(mPaddingLeft, finalFundMode.floatY, mWidth - mPaddingRight, finalFundMode.floatY, mLongPressPaint);
|
||||
//y
|
||||
canvas.drawLine(finalFundMode.floatX, mPaddingTop, finalFundMode.floatX, mWidth - mPaddingBottom, mLongPressPaint);
|
||||
|
||||
//开始处理按下之后top的文字信息
|
||||
//先画背景
|
||||
float hight = mPaddingTop - 30;
|
||||
Paint bgColor = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
bgColor.setColor(getColor(R.color.color_fundView_pressIncomeTxtBg));
|
||||
canvas.drawRect(0, 0, mWidth, hight, bgColor);
|
||||
|
||||
//开始画按下之后左边的日期文字
|
||||
Paint timePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
timePaint.setTextSize(mLongPressTextSize);
|
||||
timePaint.setColor(getColor(R.color.color_fundView_xyTxtColor));
|
||||
canvas.drawText(processDateTime(finalFundMode.datetime) + "",
|
||||
10, hight / 2 + getFontHeight(mLoadingTextSize, timePaint) / 2, timePaint);
|
||||
|
||||
//右边红色收益文字
|
||||
canvas.drawText(finalFundMode.dataY + "",
|
||||
mWidth - mPaddingRight - mLongPressPaint.measureText(finalFundMode.dataY + ""),
|
||||
hight / 2 + getFontHeight(mLoadingTextSize, timePaint) / 2, mLongPressPaint);
|
||||
|
||||
//右边的左边的提示文字
|
||||
Paint hintPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
hintPaint.setTextSize(mLongPressTextSize);
|
||||
hintPaint.setColor(getColor(R.color.color_fundView_xyTxtColor));
|
||||
canvas.drawText(getString(R.string.string_fundView_pressHintTxt),
|
||||
mWidth - mPaddingRight - mLongPressPaint.measureText(finalFundMode.dataY + "")
|
||||
- hintPaint.measureText(getString(R.string.string_fundView_pressHintTxt)),
|
||||
hight / 2 + getFontHeight(mLoadingTextSize, timePaint) / 2, hintPaint);
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
* 如何实现蚂蚁金服基金收益的长按后十字延迟消失?这里延迟消失,其实是很好的体验,省的按下的位置不能及时看不到。其实只需要设置一个标记位,当标记为为false时,不执行绘制十字的操作即可:`if (!mDrawLongPressPaint) return;`
|
||||
|
||||
private void hiddenLongPressView() {
|
||||
//实现蚂蚁金服延迟消失十字线
|
||||
postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mDrawLongPressPaint = false;
|
||||
invalidate();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
#### 正在加载中文字处理
|
||||
* 这里只需要在数据过来之前居中显示一个Text即可:`canvas.drawText(mLoadingText, mWidth / 2 - mLoadingPaint.measureText(mLoadingText) / 2, mHeight / 2, mLoadingPaint);`
|
||||
* 那么如何在数据来之后取消这个显示呢?其实所谓取消,直接不执行这个绘制即可(因为重新刷新了界面)。其实和上面十字消失是一样的道理,加一个标记位。
|
||||
|
||||
#### code
|
||||
* [`https://github.com/scsfwgy/FinancialCustomerView`](https://github.com/scsfwgy/FinancialCustomerView "https://github.com/scsfwgy/FinancialCustomerView")
|
||||
* 注:该项目会一直维护,不断加入新的关于金融类的各种自定义View。最终的目标是绘制出复杂多变的K线图~
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
# 金融类自定义View(三)--股票分时图(关于细节和实现思路)
|
||||
|
||||
## 前言
|
||||
* 本篇文章首先会介绍上一篇文章[上一篇文章](http://blog.csdn.net/wgyscsf/article/details/78632594)遗留下来的问题:长按回调、缩放问题、加载更多等。
|
||||
* 剩下的会介绍如何绘制这个分时图,怎么的思路,流程怎样。
|
||||
* 建议,先看第二篇文章和对代码进行大致的浏览,不然阅读起来可能会有点吃力。
|
||||
|
||||
## 效果图
|
||||
|
||||
#### 原【天厚实盘】分时图
|
||||
|
||||
* 【天厚实盘】分时图_默认
|
||||
|
||||

|
||||
|
||||
* 【天厚实盘】分时图_长按
|
||||
|
||||

|
||||
|
||||
* 【天厚实盘】分时图_gif
|
||||
|
||||

|
||||
|
||||
#### 仿分时图【TimeSharingView.java】
|
||||
|
||||
* 第一阶段,参数准备,外边框、内虚线、折线图等的绘制
|
||||
|
||||

|
||||
|
||||
* 第二阶段,x、y文字、实时横线和实时数据、下方透明阴影
|
||||
|
||||

|
||||
|
||||
* 第三阶段,实时数据更新分时图
|
||||
|
||||

|
||||
|
||||
* 第四阶段,长按十字线,左右移动分时图
|
||||
|
||||

|
||||
|
||||
* 第五阶段,长按实时显示详细数据、加载更多处理、实时横线优化、滑动优化
|
||||
|
||||

|
||||
|
||||
* 第六阶段,缩放、代码整理、注释
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
## 长按回调、缩放问题、加载更多
|
||||
#### 长按回调
|
||||
* 在上一节,我们已经绘制出了长按的十字,也可以左右滑动。现在的需求是仿照【天厚实盘】长按在View上面绘制出按下点(十字中央)对应点的报价详情以及涨跌幅情况。这个地方的处理,有两种:直接在View上绘制;在布局中设置布局,然后把数据回调给使用者,再设置上对应数据。很明显,第二种方式更简单,也更灵活,这里采用第二种。特别注意需要回调除了当前点,还有上一个点,便于计算涨跌幅。套路:定义接口,设置回调,设置布局。
|
||||
|
||||
//长按的绘制逻辑以及回调
|
||||
protected void drawLongPress(Canvas canvas) {
|
||||
if (!mDrawLongPressPaint) return;
|
||||
|
||||
//长按的逻辑
|
||||
....
|
||||
|
||||
//在这里回调数据信息
|
||||
if (mTimeSharingListener != null) {
|
||||
int size = mQuotesList.size();
|
||||
if ((0 <= finalIndex && finalIndex < size) &&
|
||||
(0 <= finalIndex - 1 && finalIndex - 1 < size))
|
||||
//回调,需要两个数据,便于计算涨跌百分比
|
||||
mTimeSharingListener.onLongTouch(mQuotesList.get(finalIndex - 1),
|
||||
mQuotesList.get(finalIndex));
|
||||
}
|
||||
}
|
||||
|
||||
#### 长按回调
|
||||
* 对于缩放问题的处理,真是操碎了心,思考了好久好久。所谓缩放,我们要知道两手指缩放的距离占View有效宽度的百分比,然后根据百分比计算新的有效可视个数,然后重绘。真的有这么简单吗?我们一直绘制的依据是确定起始位置和结束位置。当两个手指缩小视图时,真正的中心点在两指中间,因此起始位置要变,结束位置也要变。最后采用的方案是采用系统的`ScaleGestureDetector`监听手指,根据`detector.getScaleFactor()`确定缩放因子。缩放思路:所谓缩放,也是计算新的起始位置和结束位置。这里根据缩放因子detector.getScaleFactor()计算新的可见个数(x缩放因子即可)。当放大时,可见的数据集合的个数(A)应该减少。detector.getScaleFactor()(B的范围[1,2)),这个时候可以新的可见数据集合(C)可以考虑采用C=A-A*(B-1);当然这样计算是否准确,还需要商榷。思路简单,但是这里细节比较多,具体可以参考代码。
|
||||
|
||||
//缩放手势监听
|
||||
ScaleGestureDetector.OnScaleGestureListener mOnScaleGestureListener =
|
||||
new ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
||||
@Override
|
||||
public boolean onScale(ScaleGestureDetector detector) {
|
||||
//没有缩放
|
||||
if (detector.getScaleFactor() == 1) return true;
|
||||
|
||||
//是放大还是缩小
|
||||
boolean isBigger = detector.getScaleFactor() > 1;
|
||||
|
||||
//变化的个数(缩小或者放大),必须向上取整,不然当mShownMaxCount过小时容易取到0。
|
||||
int changeNum = (int) Math.ceil(mShownMaxCount * Math.abs(detector.getScaleFactor() - 1));
|
||||
|
||||
//容错处理,省略
|
||||
...
|
||||
|
||||
//计算新的开始位置。这个地方比较难以理解:拉伸了起始点变大,并且是拉伸数量的一半,结束点变小,也是原来的一半。
|
||||
// 收缩,相反。可以自己画一个图看看
|
||||
mBeginIndex = isBigger ? mBeginIndex + helfChangeNum : mBeginIndex - helfChangeNum;
|
||||
if (mBeginIndex < 0) {
|
||||
mBeginIndex = 0;
|
||||
} else if ((mBeginIndex + mShownMaxCount) > mQuotesList.size()) {
|
||||
mBeginIndex = mQuotesList.size() - mShownMaxCount;
|
||||
}
|
||||
|
||||
mEndIndex = mBeginIndex + mShownMaxCount;
|
||||
|
||||
//只要找好起始点和结束点就可以交给处理重绘的方法就好啦~
|
||||
seekAndCalculateCellData();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onScaleBegin(ScaleGestureDetector detector) {
|
||||
Log.e(TAG, "onScaleBegin: " + detector.getFocusX());
|
||||
//指头数量,过滤无用手势
|
||||
if (mFingerPressedCount != 2) return true;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
#### 加载更多
|
||||
* 本身,加载更多是很简单的,只要判断移动的时候到最右端就去加载就好。可是,这里牵扯出来另外几个问题
|
||||
* 滑动到最右边的时候,需要显示右侧的内间距和绘制小圆点(其它情况不需要显示)。
|
||||
* 不再最右侧的时候,滑动时需要隐藏右侧间距;同时,来新数据后,不应该实时绘制View。
|
||||
* 滑动到最左边时,需要加载更多。
|
||||
* 加载更多触发可能在最左侧,但是加载过程中(加载过程可以左右滑动)可能又在最右侧。状态不确定。
|
||||
|
||||
* 这里的处理是定义滑动枚举类型,确认实时状态
|
||||
|
||||
enum PullType {
|
||||
PULL_RIGHT,//向右滑动
|
||||
PULL_LEFT,//向左滑动
|
||||
PULL_RIGHT_STOP,//滑动到最右边
|
||||
PULL_LEFT_STOP,//滑动到最左边
|
||||
}
|
||||
|
||||
* 实时监听并记录状态,并且在触发加载更多时(最好设置一定阀值,比如剩余10个数据时就触发加载更多)
|
||||
|
||||
/**
|
||||
* 移动K线图计算移动的单位和重新计算起始位置和结束位置
|
||||
*
|
||||
* @param moveLen
|
||||
*/
|
||||
protected void moveKView(float moveLen) {
|
||||
//移动之前将右侧的内间距值为0
|
||||
mInnerRightBlankPadding = 0;
|
||||
|
||||
mPullRight = moveLen > 0;
|
||||
int moveCount = (int) Math.ceil(Math.abs(moveLen) / mPerX);
|
||||
if (mPullRight) {
|
||||
int len = mBeginIndex - moveCount;
|
||||
//阀值
|
||||
if (len < DEF_MINLEN_LOADMORE) {
|
||||
//加载更多
|
||||
if (mTimeSharingListener != null && mCanLoadMore) {
|
||||
loadMoreIng();
|
||||
mTimeSharingListener.needLoadMore();
|
||||
}
|
||||
}
|
||||
//向右拉逻辑
|
||||
...
|
||||
} else {
|
||||
//向左拉逻辑
|
||||
...
|
||||
}
|
||||
//确认结束位置
|
||||
mEndIndex = mBeginIndex + mShownMaxCount;
|
||||
//开始位置和结束位置确认好,就可以重绘啦~
|
||||
//Log.e(TAG, "moveKView: mPullRight:" + mPullRight + ",mBeginIndex:" + mBeginIndex + ",mEndIndex:" + mEndIndex);
|
||||
seekAndCalculateCellData();
|
||||
}
|
||||
|
||||
|
||||
|
||||
## 分时图绘制思路
|
||||
* 整个分时图到这里基本全部完成了,包括如下功能:基本的边框、内部虚线、x/y周文字标示、走势折现、加载更多、左右滑动、滑动数据回调、缩放、实时横线价格展示等。
|
||||
* 总代码行数1000+行,完全继承系统View,不依赖任何第三方。虽然也不是很多,但是第一次看肯定是懵逼的。下面大致简述思路。
|
||||
* 首先是数据的模拟,为了更加符合真实的使用场景,我们把拿到的数据进行了“转换处理”。可以想象,真实使用场景,不可能你直接从服务端拿到的数据就可以刚好符合View的数据类型。至于获取数据、模拟网络环境、切线程、模拟实时Socket推数据等采用了Rx进行了处理(多说一句,Rx在线程切换真是好用到爆)。
|
||||
* 基本点,我们会把拿到数据集合(包括推过来的实时数据)全部存到全局的List<Quotes>中,保证单一数据集合。定义可视范围的起始点mBeginIndex和结束点mEndIndex。有大致了解代码的同学,会看到整个View的大部分操作过程中,主要是计算起始点和结束点,然后重绘View。是的,实时加载数据、左右滑动、缩放、加载更多这些核心功能,其实都是为了计算新的起始位置和结束位置。
|
||||
* 核心方法,核心方法全部都在`onDraw(Canvas canvas)`中完成,但是为了逻辑清晰,我们会单独绘制每一个业务功能,保证业务逻辑的清晰,方便修改和扩展。其它的,手势监控在`onTouchEvent(MotionEvent event)`和`ScaleGestureDetector mScaleGestureDetector`中完成。加载数据直接由使用者传递。
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
//Log.e("TimeSharingView", "onDraw: ");
|
||||
//默认加载loading界面
|
||||
showLoadingPaint(canvas);
|
||||
if (mQuotesList == null || mQuotesList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
drawOuterLine(canvas);
|
||||
drawInnerXy(canvas);
|
||||
drawXyTxt(canvas);
|
||||
drawBrokenLine(canvas);
|
||||
//长按处理
|
||||
drawLongPress(canvas);
|
||||
//长按情况下的时间和数据框
|
||||
drawLongPressTxt(canvas);
|
||||
}
|
||||
|
||||
|
||||
* 关于绘图,绘图全部相对于View的左上角开始,在View全局中定义了宽度和高度以及内边距等,基本上在View中绘制任何线和图全部会相对这几个值进行操作,更多的是对位置的准确把握以及对边界的准确控制。
|
||||
|
||||
//控件宽高,会在onMeasure中进行测量。
|
||||
int mWidth;
|
||||
int mHeight;
|
||||
//上下左右padding,这里不再采用系统属性padding,因为用户容易忘记设置padding,直接在这里更改即可。
|
||||
float mPaddingTop = 20;
|
||||
float mPaddingBottom = 50;
|
||||
float mPaddingLeft = 8;
|
||||
float mPaddingRight = 90;
|
||||
//默认情况下结束点距离右边边距
|
||||
float mInnerRightBlankPadding = DEF_INNER_RIGHT_BLANK_PADDING;
|
||||
//为了美观,容器内(边框内部的折线图距离外边框线的上下距离)上面有一定间距,下面也有一定的间距。
|
||||
float mInnerTopBlankPadding = 60;
|
||||
float mInnerBottomBlankPadding = 60;
|
||||
|
||||
* 关于y轴最大最小值的确认,这个比较重要。如何保证分时图准确绘制不绘制出边界?如果数据的范围本来是[1,100],突然来了一个10000的数据怎么办?处理的手段是,每次拿到数据之后遍历,找到分时图**可视范围**内的最大值和最小值和View有效高度,算出来单位高度,然后根据每个点的值和最小值(最大值也可以)的差值计算应有的高度坐标即可。如果出现上述中的特别特别大的数据怎么办?那可能就绘制出一条直线咯(生产环境中有遇到)。
|
||||
|
||||
* 关于错误和调试,有阅读代码的同学可以在代码中看到大量的Log日志。写的过程中遇到了很多问题,由于有大量的数据还实时推数据并且实时刷新绘制,这个时候可能debug就很难发现问题,直接打Log是有效的手段,可以实时观察到数据的异常。当然,大部分问题直接卡断点就能定位到问题。
|
||||
|
||||
|
||||
#### code
|
||||
* [`https://github.com/scsfwgy/FinancialCustomerView`](https://github.com/scsfwgy/FinancialCustomerView "https://github.com/scsfwgy/FinancialCustomerView")
|
||||
* 注:该项目会一直维护
|
||||
* 绘制各种金融类的自定义View。
|
||||
* 提供金融类自定义View的实现思路。
|
||||
* 收集整理相关算法、文档以及专业资料。
|
||||
* 另,蜡烛图(包括主图指标)大部分功能已经绘制出来啦,代码也进行了大量的重构。在分支:feature_candleview
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
## 金融类自定义View(二)--股票分时图
|
||||
> 本节主要介绍股票分时图的详细实现思路与遇到的各种问题。该分时图主要包括以下功能:数据的适配、x/y背景轴的绘制、x/轴文字的绘制、长按十字的处理、实时折线的处理、分时折线的左右滑动等。主要会陈述的问题:如何实现最新数据横线的实现、实时数据导致折线的变化的实现思路、折线图左右滑动的处理。(参考阅读:[百度百科--分时图](https://baike.baidu.com/item/%E5%88%86%E6%97%B6%E5%9B%BE/11004740?fr=aladdin "https://baike.baidu.com/item/%E5%88%86%E6%97%B6%E5%9B%BE/11004740?fr=aladdin"))
|
||||
|
||||
#### 前言
|
||||
* 有阅读过第一篇文章的读者可能会说,这特么不和第一篇那个一样嘛。确实,实现思路基本一致,但是复杂度远大于【基金收益自定义view】,包括交互处理与数据处理。
|
||||
* 该【股票分时图】当时绘制的时候也没好的分时图进行模仿,就直接拿自己公司同事之前实现的分时图进行了模仿。公司分时图实现是使用了知名的第三方图形库`MPAndroidChart`[https://github.com/PhilJay/MPAndroidChart](https://github.com/PhilJay/MPAndroidChart)。
|
||||
* 既然是进行模仿,基本会严格按照原分时图进行实现,读者可以下载原APP对比体验。
|
||||
* 有些地方实现并没有严格按照自定义view的套路去实现,比如自定义View属性的抽取、颜色的抽取等(因为真的属性太多了...),后期会逐步完善抽取。
|
||||
* 代码我是尽可能的清晰表达,保证在修改使用的时候可以容易阅读。
|
||||
* 本来打算用半年的时候,绘制出自己的金融交易类“K线图”。现在看来,用不了那么久了。因为分时图绘制出来之后,蜡烛图实现就比较简单了--只要在目标点绘制矩形图即可。
|
||||
* 代码有点多,enjoy it~
|
||||
|
||||
#### 效果图
|
||||
###### 原【天厚实盘】分时图
|
||||
|
||||
* 【天厚实盘】分时图_默认
|
||||
|
||||

|
||||
|
||||
* 【天厚实盘】分时图_长按
|
||||
|
||||

|
||||
|
||||
* 【天厚实盘】分时图_gif
|
||||
|
||||

|
||||
|
||||
###### 仿分时图【TimeSharingView.java】
|
||||
|
||||
* 第一阶段,参数准备,外边框、内虚线、折线图等的绘制
|
||||
|
||||

|
||||
|
||||
* 第二阶段,x、y文字、实时横线和实时数据、下方透明阴影
|
||||
|
||||

|
||||
|
||||
* 第三阶段,实时数据更新分时图
|
||||
|
||||

|
||||
|
||||
* 第四阶段,长按十字线,左右移动分时图
|
||||
|
||||

|
||||
|
||||
#### 主要运用什么知识
|
||||
* 自定义View基础知识
|
||||
* 运用Paint绘制文字、绘制横线、绘制矩形
|
||||
* 运用Path绘制折线、绘制背景
|
||||
* `onTouchEvent(MotionEvent event)`各种手势的监听与处理
|
||||
* 【重点】大量的计算、测量以及定位
|
||||
* 线程的切换处理(Rxjava,可选)
|
||||
|
||||
#### 参考
|
||||
* 加载中文字处理、x/y轴曲线的绘制、x轴时间文字和y轴收益文字的定位和绘制、折线的绘制
|
||||
、长按十字的处理请参考第一篇文章:[金融类自定义View(一)--仿蚂蚁财富基金收益曲线图](http://blog.csdn.net/wgyscsf/article/details/78446572 "http://blog.csdn.net/wgyscsf/article/details/78446572")
|
||||
|
||||
|
||||
#### 数据量和位置的确认
|
||||
* 对于分时图有一个问题,就是数据的加载和实时推送。因为,对于分时图第一次加载的时候,数据量可能是不确定的,可能第一次拉去过来500条,也可能是100条。同时,分时图中显示的条数也是可变的。比如,分时图中可以显示40条,也可以显示100条。对于自定义分时图来说,两边的数据都不确认。这里的处理思路是,定义全局的数据起始点和结束点,每次刷新页面都会重新计算数据的起始位置和结束位置,同时刚加载的时候和滑动的时候数据的起始位置和结束位置计算方式是不一样的。我们多思考一点,其实所谓的K线图的移动,就是重新寻找数据的起始位置和结束位置,计算好新的开始和结束位置,可以刷新界面了,完成移动操作。
|
||||
|
||||
/**
|
||||
* 来最新数据或者刚加载的时候,计算开始位置和结束位置。特别注意,最新的数据在最后面,所以数据范围应该是[(size-mShownMaxCount)~size)
|
||||
*/
|
||||
private void counterBeginAndEndByNewer() {
|
||||
int size = mQuotesList.size();
|
||||
if (size >= mShownMaxCount) {
|
||||
mBeginIndex = size - mShownMaxCount;
|
||||
mEndIndex = mBeginIndex + mShownMaxCount;
|
||||
} else {
|
||||
mBeginIndex = 0;
|
||||
mEndIndex = mBeginIndex + mQuotesList.size();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 移动K线图计算移动的单位和重新计算起始位置和结束位置
|
||||
* @param moveLen
|
||||
*/
|
||||
private void moveKView(float moveLen) {
|
||||
mPullRight = moveLen > 0;
|
||||
mPullType = moveLen > 0 ? PullType.PULL_RIGHT : PullType.PULL_LEFT;
|
||||
int moveCount = (int) Math.ceil(Math.abs(moveLen) / mPerX);
|
||||
if (mPullRight) {
|
||||
int len = mBeginIndex - moveCount;
|
||||
if (len < 0) {
|
||||
mBeginIndex = 0;
|
||||
} else {
|
||||
mBeginIndex = len;
|
||||
}
|
||||
} else {
|
||||
int len = mBeginIndex + moveCount;
|
||||
if (len + mShownMaxCount > mQuotesList.size()) {
|
||||
mBeginIndex = mQuotesList.size() - mShownMaxCount;
|
||||
} else {
|
||||
mBeginIndex = len;
|
||||
mPullType = PullType.PULL_NONE;//到最左边啦
|
||||
}
|
||||
}
|
||||
mEndIndex = mBeginIndex + mShownMaxCount;
|
||||
//开始位置和结束位置确认好,就可以重绘啦~
|
||||
Log.e(TAG, "moveKView: mPullRight:" + mPullRight + ",mBeginIndex:" + mBeginIndex + ",mEndIndex:" + mEndIndex);
|
||||
processData();
|
||||
}
|
||||
|
||||
#### 分时图的实时变化
|
||||
* 所谓分时图,就是要根据服务推送过来的数据,实时更新折线图。这里的实现采用随机时间模拟服务端推送过来的数据。所谓的折线图实时变化,实现思路其实关键点还是在于数据集合的起始位置和结束位置的确认。确认好起始和结束位置后,重新绘制新的折线即可。因此,该分时图View提供了一个添加单个数据的方法:
|
||||
|
||||
/**
|
||||
* 实时推送过来的数据,实时更新
|
||||
*
|
||||
* @param quotes
|
||||
*/
|
||||
public void addTimeSharingData(Quotes quotes) {
|
||||
if (quotes == null) {
|
||||
Toast.makeText(mContext, "数据异常", Toast.LENGTH_SHORT).show();
|
||||
Log.e(TAG, "setTimeSharingData: 数据异常");
|
||||
return;
|
||||
}
|
||||
mQuotesList.add(quotes);
|
||||
//如果实在左右移动,则不去实时更新K线图,但是要把数据加进去
|
||||
if (mPullType == PullType.PULL_NONE) {
|
||||
Log.e(TAG, "addTimeSharingData: 处理实时更新操作...");
|
||||
counterBeginAndEndByNewer();
|
||||
processData();
|
||||
}
|
||||
}
|
||||
|
||||
#### 折线图的绘制和折线图阴影的处理
|
||||
* 由上面两点可以看出,只要有起始位置和结束位置,不仅可以绘制好折线还可以实时更新,那么折线图的绘制就要求很高了。同时,我们可以注意一下,折线图下面的阴影,这个阴影怎么处理?查找了很多资料,并没有Path绘制折线并且绘制不同颜色的背景的处理;并且,折线并不是完整的闭环,而是只有上方一部分。这个地方思考了很久,最终的解决方案是,采用两个Path,第一个Path只绘制用户看到的折线图,同时设置画笔属性`mBrokenLinePaint.setStyle(Paint.Style.STROKE);`,只绘制折线,不填充内容。另外提供一个Path,只绘制内容,设置画笔属性:`mBrokenLineBgPaint.setStyle(Paint.Style.FILL);`,另外提供透明效果:`mBrokenLineBgPaint.setAlpha(mAlpha);`,这样就实现了这种效果:
|
||||
|
||||

|
||||
|
||||
//折线图绘制核心代码
|
||||
private void drawBrokenLine(Canvas canvas) {
|
||||
//先画第一个点
|
||||
Quotes quotes = mQuotesList.get(mBeginIndex);
|
||||
Path path = new Path();
|
||||
Path path2 = new Path();
|
||||
//这里需要说明一下,x轴的起始点,其实需要加上mPerX,但是加上之后不是从起始位置开始,不好看。
|
||||
// 同理,for循环内x轴其实需要(i+1)。现在这样处理,最后会留一点空隙,其实挺好看的。
|
||||
float floatY = (float) (mHeight - mPaddingBottom - mInnerBottomBlankPadding - mPerY * (quotes.c - mMinQuotes.c));
|
||||
//在自定义view:FundView中的位置坐标
|
||||
//记录下位置信息
|
||||
quotes.floatX = mPaddingLeft;
|
||||
quotes.floatY = floatY;
|
||||
path.moveTo(mPaddingLeft, floatY);
|
||||
path2.moveTo(mPaddingLeft, floatY);
|
||||
for (int i = mBeginIndex + 1; i < mEndIndex; i++) {
|
||||
Quotes q = mQuotesList.get(i);
|
||||
float floatX2 = mPaddingLeft + mPerX * (i - mBeginIndex);//注意这个 mPerX * (i-mBeginIndex),而不是mPerX * (i)
|
||||
float floatY2 = (float) (mHeight - mPaddingBottom - mInnerBottomBlankPadding - mPerY * (q.c - mMinQuotes.c));
|
||||
//记录下位置信息
|
||||
q.floatX = floatX2;
|
||||
q.floatY = floatY2;
|
||||
path.lineTo(floatX2, floatY2);
|
||||
path2.lineTo(floatX2, floatY2);
|
||||
//最后一个点,画一个小圆点;实时横线;横线的右侧数据与背景;折线下方阴影
|
||||
if (i == mEndIndex - 1) {
|
||||
//绘制小圆点
|
||||
canvas.drawCircle(floatX2, floatY2, mDotRadius, mDotPaint);
|
||||
|
||||
//接着画实时横线
|
||||
canvas.drawLine(mPaddingLeft, floatY2, mWidth - mPaddingRight, floatY2, mTimingLinePaint);
|
||||
|
||||
//接着绘制实时横线的右侧数据与背景
|
||||
//文字高度
|
||||
float txtHight = getFontHeight(mTimingTxtWidth, mTimingTxtBgPaint);
|
||||
//绘制背景
|
||||
canvas.drawRect(mWidth - mPaddingRight, floatY2 - txtHight / 2, mWidth, floatY2 + txtHight / 2, mTimingTxtBgPaint);
|
||||
|
||||
//绘制实时数据
|
||||
//距离左边的距离
|
||||
float leftDis = 8;
|
||||
canvas.drawText(FormatUtil.numFormat(q.c, mDigits), mWidth - mPaddingRight + leftDis, floatY2 + txtHight / 4, mTimingTxtPaint);
|
||||
|
||||
//在这里把path圈起来,添加阴影。特别注意,这里处理下方阴影和折线边框。采用两个画笔和两个Path处理的,貌似没有一个Paint可以同时绘制边框和填充色
|
||||
path2.lineTo(floatX2, mHeight - mPaddingBottom);
|
||||
path2.lineTo(mPaddingLeft, mHeight - mPaddingBottom);
|
||||
path2.close();
|
||||
}
|
||||
}
|
||||
canvas.drawPath(path, mBrokenLinePaint);
|
||||
canvas.drawPath(path2, mBrokenLineBgPaint);
|
||||
}
|
||||
|
||||
#### 折线图的左右滑动处理
|
||||
* 这个功能的实现考虑了很久,不知道怎么实现。当时还在考虑安卓的画布是否会提供类似于ScrollView的功能,可以绘制很多View,然后可以左右滑动View;或者类似于ListView....(并没有)
|
||||
* 最终的实现思路是这样的(有线上运行的分时图是这样处理的,可以放心使用!):
|
||||
0. 监听滑动
|
||||
1. 手指滑动的时候,记录下滑动的x轴的距离
|
||||
2. 根据单位数据的x轴的距离,计算出需要移动几个单位的数据(注意边界和方向问题)
|
||||
3. 根据移动的单位,重新计算起始位置和结束位置(其实最上面的起始位置和结束位置的确认,是在这里得到启发的)
|
||||
4. 刷新View
|
||||
|
||||
|
||||
//监听判断手指移动
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
mPressedX = event.getX();
|
||||
mPressTime = event.getDownTime();
|
||||
//按下的手指个数
|
||||
mFingerPressedCount = event.getPointerCount();
|
||||
break;
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
|
||||
...
|
||||
|
||||
//判断是否是手指移动
|
||||
float currentPressedX = event.getX();
|
||||
float moveLen = currentPressedX - mPressedX;
|
||||
//重置当前按下的位置【不要忘了】
|
||||
mPressedX = currentPressedX;
|
||||
if (Math.abs(moveLen) > DEF_PULL_LENGTH && mFingerPressedCount == 1) {
|
||||
Log.e(TAG, "onTouchEvent: 正在移动分时图");
|
||||
//移动k线图
|
||||
moveKView(moveLen);
|
||||
}
|
||||
break;
|
||||
...
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 移动K线图计算移动的单位和重新计算起始位置和结束位置
|
||||
* @param moveLen
|
||||
*/
|
||||
private void moveKView(float moveLen) {
|
||||
mPullRight = moveLen > 0;
|
||||
mPullType = moveLen > 0 ? PullType.PULL_RIGHT : PullType.PULL_LEFT;
|
||||
int moveCount = (int) Math.ceil(Math.abs(moveLen) / mPerX);
|
||||
if (mPullRight) {
|
||||
int len = mBeginIndex - moveCount;
|
||||
if (len < 0) {
|
||||
mBeginIndex = 0;
|
||||
} else {
|
||||
mBeginIndex = len;
|
||||
}
|
||||
} else {
|
||||
int len = mBeginIndex + moveCount;
|
||||
if (len + mShownMaxCount > mQuotesList.size()) {
|
||||
mBeginIndex = mQuotesList.size() - mShownMaxCount;
|
||||
} else {
|
||||
mBeginIndex = len;
|
||||
mPullType = PullType.PULL_NONE;//到最左边啦
|
||||
}
|
||||
}
|
||||
mEndIndex = mBeginIndex + mShownMaxCount;
|
||||
//开始位置和结束位置确认好,就可以重绘啦~
|
||||
Log.e(TAG, "moveKView: mPullRight:" + mPullRight + ",mBeginIndex:" + mBeginIndex + ",mEndIndex:" + mEndIndex);
|
||||
//这里就是处理和刷新数据
|
||||
processData();
|
||||
}
|
||||
|
||||
|
||||
#### 待处理的问题
|
||||
* 长按十字,上方应该显示:开盘价、收盘价、最高价、最低价、时间等信息;这里不再将这一块在View中绘制,而是在xml中进行绘制即可。该View其实很简单,只需要提供一个接口,把点击长按的数据回调出去就好~
|
||||
* 双指View缩放的处理,这里当时考虑很难处理的。但是,现在已经有思路了:改变显示的View的数据量,同时找到中间位置,以该位置左右加减指定的数据量,刷新界面即可!
|
||||
* 滑动到最左边,加载更多数据....
|
||||
* 手指滑动、更新数据都会导致整个View的重新绘制,可能短时间内会导致N次重绘,感觉严重影响性能。但是,问了一些朋友,貌似都是这样处理的。。。没有其它方式吗?
|
||||
* 打个广告,公司一直在**招聘**,java、DBA、H5、Devlops等,坐标:上海,欢迎来投:[天厚投资 Tophold](https://www.lagou.com/gongsi/j3720.html)
|
||||
|
||||
#### code
|
||||
* [`https://github.com/scsfwgy/FinancialCustomerView`](https://github.com/scsfwgy/FinancialCustomerView "https://github.com/scsfwgy/FinancialCustomerView")
|
||||
* 注:该项目会一直维护,不断加入新的关于金融类的各种自定义View。最终的目标是绘制出复杂多变的K线图~
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
# 金融类自定义View(四)--股票蜡烛图(包含MA、BOLL指标)以及代码重构
|
||||
|
||||
## 前言
|
||||
* 本文只描述蜡烛图单独的绘制逻辑,至于和分时图相同的逻辑不再阐述,感兴趣的可以查看之前的文章,会有详细的阐述。
|
||||
* 首先会介绍蜡烛图的绘制思路、MA/BOLL指标的绘制、指标y轴越界问题、x轴绘制的细节处理。
|
||||
* 最后会介绍一下代码的重构处理以及对后期副图部分的绘制影响。
|
||||
|
||||
## 效果图
|
||||
|
||||
#### 蜡烛图【MasterView.java】
|
||||
|
||||
* 默认蜡烛图
|
||||
|
||||

|
||||
|
||||
* MA指标(BOLL类似)
|
||||
|
||||

|
||||
|
||||
* 蜡烛图长按
|
||||
|
||||

|
||||
|
||||
* gif
|
||||
|
||||

|
||||
|
||||
|
||||
## 蜡烛图绘制
|
||||
#### 命名
|
||||
* 主流的交易曲线图会包含两个大部分,上面一大块是走势图,包含分时图、5分图、15分图、日k、周k、月k等;下面一小块是指标,包含成交量、MACD、KDJ、RSI等。如下图,是同花顺的交易曲线图,基本也是这样构成的:
|
||||
|
||||
|
||||

|
||||
|
||||
* 因此,在本项目命名如下,整个交易曲线图称为KView,上面的部分称为MasterView(主图,包含分时图和蜡烛图);下面的部分称为MinorView(副图,包含各种副图指标)。其中蜡烛图又包含MA(5,10,20)、BOLL(26)、MA(5,10,20)\BOLL(26)三种指标,对于副图部分,计划只绘制以下三种指标,和【天厚实盘】APP保持同步:MACD(12,26,9)、RSI(6,12,24)、KDJ(9,3,3)。
|
||||
|
||||
#### 蜡烛图的绘制
|
||||
* 仅仅是蜡烛图的绘制很简单,之前绘制好分时图就说过这个问题。蜡烛图的矩形Y轴是根据开盘价格和闭市价格绘制对应的高度,至于哪个在上哪个在下不确定。X轴的绘制是在原来分时图目标点向左向右分别取1/2.0个单元大小。对于蜡烛间间距的处理,这里采用倍率的处理方式,间距是单个蜡烛宽度的0.1f(为什么不采用固定大小?当缩放蜡烛图时如果间距固定大小会非常丑)。至于蜡烛图Y轴中间的横线则是根据最高价和最低价对应的高度进行绘制。
|
||||
* 蜡烛图的绘制如下,特别注意边界的处理,当处理可视范围内第一个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、BOLL的绘制
|
||||
* MA、BOLL是在主图的指标表现,可以辅助用户在交易中做出策略指导。链接:[百度百科:移动平均线(MA)](https://baike.baidu.com/item/%E7%A7%BB%E5%8A%A8%E5%B9%B3%E5%9D%87%E7%BA%BF/217887?fr=aladdin&fromid=1511750&fromtitle=MA),[百度百科:布林线(BOLL)](https://baike.baidu.com/item/%E5%B8%83%E6%9E%97%E7%BA%BF/3424486?fr=aladdin)
|
||||
* 在本项目中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<Quotes> 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功能、交互的的增加,代码量也大量激增,在分时图功能完成时整个View的代码行数已经过1000行了,感觉多的都没法维护了=。=,因此考虑对代码进行重构。
|
||||
* 在开始绘制绘制蜡烛图之前,对代码进行了大量的重构,将一些公共的逻辑抽离到了父类,尽可能减少子类中的业务逻辑。什么是公共的逻辑?比如边距、单机长按阀值、背景、边框等等很多属性其实主图和副图都是一样的,也有部分一样的,可以分开对待,比如主图需要绘制x/y内虚线,而副图只需要绘制y轴的虚线即可。当然,还有一些父类没法实现的,可以考虑将方法抽象化,让子类去实现业务逻辑。
|
||||
* 整个继承关系如下
|
||||
|
||||

|
||||
|
||||
* 在处理的时候,把和业务没有关系的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的各种表现。在下一阶段重构的过程中,会考虑属性的处理方式是否也采用这种方式。
|
||||
|
||||
|
||||
#### code
|
||||
* [`https://github.com/scsfwgy/FinancialCustomerView`](https://github.com/scsfwgy/FinancialCustomerView "https://github.com/scsfwgy/FinancialCustomerView")
|
||||
* 注:该项目会一直维护
|
||||
* 绘制各种金融类的自定义View。
|
||||
* 提供金融类自定义View的实现思路。
|
||||
* 收集整理相关算法、文档以及专业资料。
|
||||
* 代码进行了大量重构。
|
||||
* 蜡烛图绘制完毕。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user