一個金融類的自定義View,教大家如何實現股票軟體中的折線圖
北京證監局近日釋出通告,責令樂視網前任董事長賈躍亭於2017年12月31日前回國,切實履行公司實際控制人應盡義務,配合解決公司問題,穩妥處理公司風險,切實保護投資者合法權益。目前距離賈躍亭“回國”還有4天時間,有網友戲稱道:賈躍亭作為失信人,依法不能坐飛機,從美國坐輪船到中國,需要兩個月……
作者簡介本篇來自 _高遠 的投稿,分享了一個金融類的自定義View實現,希望大家會喜歡!
_高遠的部落格地址:
前言http://blog.csdn.net/wgyscsf
有閱讀過第一篇文章的讀者可能會說,這特麼不和第一篇那個一樣嘛。確實,實現思路基本一致,但是複雜度遠大於【基金收益自定義view】,包括互動處理與資料處理。
該【股票分時圖】當時繪製的時候也沒好的分時圖進行模仿,就直接拿自己公司同事之前實現的分時圖進行了模仿。公司分時圖實現是使用了知名的第三方圖形庫MPAndroidChart,地址為:
https://github.com/PhilJay/MPAndroidChart
既然是進行模仿,基本會嚴格按照原分時圖進行實現,讀者可以下載原APP對比體驗。
有些地方實現並沒有嚴格按照自定義view的套路去實現,比如自定義View屬性的抽取、顏色的抽取等(因為真的屬性太多了…),後期會逐步完善抽取。
程式碼我是儘可能的清晰表達,保證在修改使用的時候可以容易閱讀。
本來打算用半年的時候,繪製出自己的金融交易類“K線圖”。現在看來,用不了那麼久了。因為分時圖繪製出來之後,蠟燭圖實現就比較簡單了–只要在目標點繪製矩形圖即可。
程式碼有點多,enjoy it~
效果圖
原【天厚實盤】分時圖
【天厚實盤】分時圖_預設
【天厚實盤】分時圖_長按
【天厚實盤】分時圖_gif
仿分時圖【TimeSharingView.java】
第一階段,引數準備,外邊框、內虛線、折線圖等的繪製
第二階段,x、y文字、實時橫線和實時資料、下方透明陰影
第三階段,實時資料更新分時圖
第四階段,長按十字線,左右移動分時圖
主要運用什麼知識
自定義View基礎知識
運用Paint繪製文字、繪製橫線、繪製矩形
運用Path繪製折線、繪製背景
onTouchEvent(MotionEvent event)各種手勢的監聽與處理
【重點】大量的計算、測量以及定位
執行緒的切換處理(Rxjava,可選)
資料量和位置的確認
對於分時圖有一個問題,就是資料的載入和實時推送。因為,對於分時圖第一次載入的時候,資料量可能是不確定的,可能第一次拉去過來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….(並沒有)
最終的實現思路是這樣的(有線上執行的分時圖是這樣處理的,可以放心使用!):
監聽滑動
手指滑動的時候,記錄下滑動的x軸的距離
根據單位資料的x軸的距離,計算出需要移動幾個單位的資料(注意邊界和方向問題)
根據移動的單位,重新計算起始位置和結束位置(其實最上面的起始位置和結束位置的確認,是在這裡得到啟發的)
重新整理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次重繪,感覺嚴重影響效能。但是,問了一些朋友,貌似都是這樣處理的。。。沒有其它方式嗎?
結語該專案會一直維護,不斷加入新的關於金融類的各種自定義View。最終的目標是繪製出複雜多變的K線圖~
github地址:
https://github.com/scsfwgy/FinancialCustomerView
如果有什麼問題歡迎留言哈,謝謝!
歡迎長按下圖 -> 識別圖中二維碼
或者 掃一掃 關注我的公眾號