android 中心區域選中圖表 WheelChart

SilenceBurst發表於2019-04-27

產品要做一個支援橫向滾動 中心區域選中 慣性滾動 停止時回滾到中心位置 點選選中的圖表需求 效果圖如下:

在這裡插入圖片描述
最開始的想法時用MPAndroidChart來做,可用這個庫有些細節滿足不了產品的需求 如選中的label標籤要用選中顏色及回滾功能,然後就很沒底,找了很多類似功能的自定義控制元件的類比,做之前也諮詢了一位大佬(在此感謝扔物線大神),覺得薄荷尺子的邏輯和這個需求很類似,就決定用自定義view來實現。自己以前寫過的自定義view都比較簡單,自己剛開始做的時候壓力挺大的,挺擔心自己做不出來影響專案進度的,不過一時也沒有好的辦法,只能逼著自己去做,主要參考之前仿寫薄荷尺子的大神的部落格,做了四天下來,總算有點眉目,把demo拿給產品過目也比較滿意,這個效果的實現也漸漸領略到開源的魅力,看到自己做出來的效果賊開心賊有成就感,週末打算分享出來,希望能對大家有所幫助,專案中有什麼問題請不吝賜教,感激不盡。 目前有些程式碼可能還不夠完善,後續還需要處理巢狀滾動的問題,但主體思路已經比較清晰了

話不多說,效果如下:

在這裡插入圖片描述
原始碼地址: github.com/SilenceBurs…

參考部落格:

之前仿寫薄荷尺子的大神 很多程式碼甚至註釋都被我毫不留情的copy過來了 ? blog.csdn.net/totond/arti…

scoller相關及多點觸控相關 請看其系列部落格 blog.csdn.net/u012422440/…

根據實現的步驟拆分為如下功能點

  1. 自定義屬性的設定及使用
  2. draw 繪製圖表
  3. 觸控控制並處理多指觸控問題(手指拖動圖表可移動)
  4. 慣性滾動(根據手指釋放時的速度計算圖表需要滾動的距離)
  5. 回滾 (up時或者慣性滾動結束 需要回滾到選中位置)
  6. 點選選中 (根據點選的座標,計算需要選中的下標並選中)

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距離較短,則忽略,這種方法的判定目前沒有發現問題,如果大家有好的想法,歡迎討論。 判定為點選事件後,要根據點選點的座標位置和當前已滾動的距離,計算出點選點所在的下標,改變需要選中的下標,滾動到指定下標

這個控制元件的一點一個功能的實現,過程之中問題不斷,問題解決又是驚喜,希望自己多些信心,多點努力,年後的第一篇部落格,收拾行裝,又上征程,加油,我們都是追夢人

相關文章