產品要做一個支援橫向滾動 中心區域選中 慣性滾動 停止時回滾到中心位置 點選選中的圖表需求 效果圖如下:
最開始的想法時用MPAndroidChart來做,可用這個庫有些細節滿足不了產品的需求 如選中的label標籤要用選中顏色及回滾功能,然後就很沒底,找了很多類似功能的自定義控制元件的類比,做之前也諮詢了一位大佬(在此感謝扔物線大神),覺得薄荷尺子的邏輯和這個需求很類似,就決定用自定義view來實現。自己以前寫過的自定義view都比較簡單,自己剛開始做的時候壓力挺大的,挺擔心自己做不出來影響專案進度的,不過一時也沒有好的辦法,只能逼著自己去做,主要參考之前仿寫薄荷尺子的大神的部落格,做了四天下來,總算有點眉目,把demo拿給產品過目也比較滿意,這個效果的實現也漸漸領略到開源的魅力,看到自己做出來的效果賊開心賊有成就感,週末打算分享出來,希望能對大家有所幫助,專案中有什麼問題請不吝賜教,感激不盡。 目前有些程式碼可能還不夠完善,後續還需要處理巢狀滾動的問題,但主體思路已經比較清晰了話不多說,效果如下:
原始碼地址: github.com/SilenceBurs…參考部落格:
之前仿寫薄荷尺子的大神 很多程式碼甚至註釋都被我毫不留情的copy過來了 ? blog.csdn.net/totond/arti…
scoller相關及多點觸控相關 請看其系列部落格 blog.csdn.net/u012422440/…
根據實現的步驟拆分為如下功能點
- 自定義屬性的設定及使用
- draw 繪製圖表
- 觸控控制並處理多指觸控問題(手指拖動圖表可移動)
- 慣性滾動(根據手指釋放時的速度計算圖表需要滾動的距離)
- 回滾 (up時或者慣性滾動結束 需要回滾到選中位置)
- 點選選中 (根據點選的座標,計算需要選中的下標並選中)
1.自定義屬性的設定及使用
在attr檔案中宣告該控制元件的一些自定義屬性,在構造方法中解析,設定控制元件的屬性即可
2. draw 繪製圖表
繪製圖表其實主要時數學問題,具體座標的計算就不再贅述了
請教扔物線的時候,我問他會不會有效能問題,他就說了一點,螢幕外不要繪製 我們就只需繪製螢幕上使用者看到的內容即可,之前之後的就不用繪製了
但由於如果只繪製螢幕顯示區域的話,左右兩側的點需要計算path連線而且在滾動時文字的顯示會有突然顯示或隱藏的問題,所以把繪製區域加長,左右兩側均多繪製一個label的距離 繪製區域為綠色加紅色 我們根據x軸方向當前已滾動的距離getScrollX()計算第一個顯示的label下標,再加上控制元件寬度和一個label距離(右側多繪製的一個label的距離)計算出最後一個label的下標,只需要繪製兩個下標中間即可,其他的就是數學問題了。多個點的連線使用的貝塞爾曲線,程式碼參考自:www.jianshu.com/p/98088ff77…
3. 觸控控制並處理多指觸控問題(手指拖動圖表可移動)
觸控控制是根據第一個event點移動的距離,呼叫view的scrollBy方法滾動view,主要程式碼如下
//處理滑動 計算現在的event座標和上一個觸控事件的座標來計算偏移量 決定scrollBy的多少
@Override
public boolean onTouchEvent(MotionEvent event) {
...
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
...
//記錄首個觸控點的id
mActivePointerId = event.findPointerIndex(event.getActionIndex());
...
mLastX = event.getX();
...
break;
case MotionEvent.ACTION_MOVE:
if (mActivePointerId == INVALID_ID || event.findPointerIndex(mActivePointerId) == INVALID_ID) {
break;
}
//計算首個觸控點移動後的座標
float moveX = mLastX - event.getX(mActivePointerId);
if (Math.abs(moveX) > IGNORE_MOVE_OFFSET) {
...
mLastX = event.getX(mActivePointerId);
scrollBy((int) moveX, 0);
}
break;
case MotionEvent.ACTION_UP:
mActivePointerId = INVALID_ID;
mLastX = 0;
...
break;
case MotionEvent.ACTION_CANCEL:
mActivePointerId = INVALID_ID;
mLastX = 0;
...
break;
}
return true;
}
複製程式碼
scrollBy方法內部會呼叫scrollTo方法,重寫了scrollTo方法在裡面進行一些選中下標的判斷和最小最大滾動位置的攔截
@Override
public void scrollTo(int x, int y) {
//預設左邊緣為x最小值-半個控制元件的寬度
if (x < mMinPosition) {
x = mMinPosition;
}
//預設右邊緣為x最大值+半個控制元件的寬度
if (x > mMaxPosition) {
x = mMaxPosition;
}
if (x != getScrollX()) {
super.scrollTo(x, y);
}
mSelectIndex = scrollX2Index(x);
}
複製程式碼
注意 在move事件中需要根據第一個觸控點id計算移動距離,直接呼叫event.getX()方法,會有多點觸控問題(復現步驟:一個手指滑動後,按下第二個手指,第一個手指抬起,view會自動滾動) 因為後面會有點選事件的判斷,所以在move時判斷如果移動距離小於IGNORE_MOVE_OFFSET = 2.5時,忽略,這樣當手機滑動比較慢時,會有部分滑動事件被忽略掉的情況,不過2.5這個值自己滑動時覺得體驗還可以,再大的話慢速滑動會有卡頓,太小的話點選事件的判定會過於精確。
4. 慣性滾動(根據手指釋放時的速度計算圖表需要滾動的距離)
慣性滾動的實現需要用到VelocityTracker計算up事件時的速度,OverScroller處理fling事件
主要思路時,當up事件發生時,判斷手指速度,若速度小於最小值,scrollBackToExactPosition()
直接將當前選中下標滾動到中心區域;若速度小於最大值按原速度計算否則按最大速度計算,根據此速度 當前x方向偏移量 可scrollTo的最小、最大值呼叫fling方法,並呼叫invalidate();
方法,invalidate();
內部幾次回撥會呼叫view的draw方法,在view的draw方法中呼叫computeScroll()
方法,若慣性滾動未結束,呼叫scrollTo方法將view滾動到該速度應滾動到的位置,再呼叫postInvalidate();
,幾次回撥又會重新呼叫view的draw方法,迴圈呼叫scrollTo將view再進行滾動 如此實現慣性滾動 直至滾動結束
5. 回滾 (up時或者慣性滾動結束 需要回滾到選中位置)
這個主要也是數學題,需要回滾的距離過大時,使用OverScroller慢速回滾;若過小則立刻回彈
//觸控事件或慣性滾動結束後 應滾動到中心位置
private void scrollBackToExactPosition() {
float rightPosition = mSelectIndex * mParent.getXLabelInterval() - (float) getWidth() / 2;
if (Math.abs(getScrollX() - rightPosition) > IGNORE_OFFSET) {
int dx = Math.round(rightPosition - getScrollX());
if (Math.abs(dx) > MIN_SCROLLER_DP) {
//漸變回彈
mOverScroller.startScroll(getScrollX(), getScrollY(), dx, 0, 500);
invalidate();
} else {
//立刻回彈
scrollBy(dx, 0);
複製程式碼
6. 點選選中 (根據點選的座標,計算需要選中的下標並選中)
點選事件的判定:最開始的想法是,判斷事件如果是down緊接up即為點選,後來發現這種判定比較苛刻,因為有些點選事件會引起略微的move事件,所以在move事件中判斷如果move距離較短,則忽略,這種方法的判定目前沒有發現問題,如果大家有好的想法,歡迎討論。 判定為點選事件後,要根據點選點的座標位置和當前已滾動的距離,計算出點選點所在的下標,改變需要選中的下標,滾動到指定下標
這個控制元件的一點一個功能的實現,過程之中問題不斷,問題解決又是驚喜,希望自己多些信心,多點努力,年後的第一篇部落格,收拾行裝,又上征程,加油,我們都是追夢人