【原創】也仿薄荷健康的刻度尺效果——帶慣性滑動效果(二)

一、先看效果

scroll-v.gif

二、分析

上一篇博客 我們繪制了薄荷健康的直尺效果,可以說只是簡單的繪制,并沒有交互的操作,例如手勢滑動,數值回調。這一篇我們來完善一下。

首先是手勢滑動,如果還用上一篇的寫法,不好處理,慣性滑動的話我們想到的是 Scroller 這個輔助類以及速度追蹤器。 Scroller 很熟悉,自定義 View 的滑動經常用到,就是計算一系列的數值,然后調用 scrollTo() 這個方法將 View 滾動到確定的位置,寫法都是固定的,參考百度。

重點說說速度追蹤器 VelocityTracker,這個類干嘛的?我也不清楚,找了 一篇博客 觀察一下。用法很詳細,大致了解了一下,但是博客里有幾個重要的參數沒有說明,后面重點提。

上面的 gif 圖中間有一條綠線,這個綠線認為是基準線,代碼里用偏移量表示。

三、代碼

相比較上一篇的代碼,我們需要修改幾個地方,首先是初始化:

    private void init(Context context, AttributeSet attrs) {
        mContext = context;
        centerLinePaint = new Paint();
        centerLinePaint.setAntiAlias(true);
        centerLinePaint.setColor(Color.parseColor("#49BA72"));
        centerLinePaint.setStrokeWidth(5);

        grayLinePaint = new Paint();
        grayLinePaint.setAntiAlias(true);
        grayLinePaint.setColor(Color.parseColor("#66666666"));
        grayLinePaint.setStrokeWidth(5);

        txtPaint = new Paint();
        txtPaint.setAntiAlias(true);
        txtPaint.setColor(Color.parseColor("#333333"));
        txtPaint.setTextSize(50);
        
        // 新增部分
        ViewConfiguration viewConfiguration = ViewConfiguration.get(mContext);
        // 最小響應距離
        touchSlop = viewConfiguration.getScaledTouchSlop();
        mScroller = new Scroller(mContext);
        // 慣性滑動最低速度要求 低于這個速度認為是觸摸
        mMinimumVelocity = viewConfiguration.getScaledMinimumFlingVelocity();
        // 慣性滑動的最大速度  觸摸速度不會超過這個值 
        mMaximumVelocity = viewConfiguration.getScaledMaximumFlingVelocity();
    }

新增的部分標注出來了。然后是觸摸部分 onTouchEvent():

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        obtainVelocityTracker();
        int action = event.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mLastX = event.getX();
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                break;
            case MotionEvent.ACTION_MOVE:
                isFastScroll = false;
                float moveX = event.getX();
                currentOffset = (int) (moveX - mLastX);
                scrollTo(getScrollX() - currentOffset, 0);
                computeAndCallback(getScrollX());
                mLastX = moveX;
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                int initialVelocity = (int) mVelocityTracker.getXVelocity();
                if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
                    isFastScroll = true;
                    flingX(-initialVelocity);
                } else {
                    int x = getScrollX();
                    if (x % space != 0) {
                        x -= x % space;
                    }
                    if (x < -BASELINE_OFFSET) {
                        x = -BASELINE_OFFSET + BASELINE_OFFSET % space;
                    } else if (x > (endValue - startValue) * space * 10 - BASELINE_OFFSET) {
                        x = (endValue - startValue) * space * 10 - BASELINE_OFFSET + BASELINE_OFFSET % space;
                    }
                    scrollTo(x, 0);
                    computeAndCallback(x);
                }
                releaseVelocityTracker();
                break;
        }
        //對每一個Event都需要交給速度追蹤器
        if (mVelocityTracker != null) {
            mVelocityTracker.addMovement(event);
        }
        return true;
    }

很長,一行行分析。

    case MotionEvent.ACTION_DOWN:
         mLastX = event.getX();
         if (!mScroller.isFinished()) {
             mScroller.abortAnimation();
         }
         break;
         

記錄按下的位置,然后如果上一次的動畫還在繼續,立即停止。
再看 MOVE 里面:

    case MotionEvent.ACTION_MOVE:
         isFastScroll = false;
         float moveX = event.getX();
         currentOffset = (int) (moveX - mLastX);
         scrollTo(getScrollX() - currentOffset, 0);
         computeAndCallback(getScrollX());
         mLastX = moveX;
         break;

第一個布爾值是標記是否正在慣性滑動,在后面會用到。為什么在這里置為false?因為觸摸的時候不可能在慣性滑動。然后計算每一次觸摸的偏移,調用 scrollTo() 不斷的讓自己(View 本身)滾動。
后面的 computeAndCallback() 方法暫時可以不看。最后還要記下每一次 MOVE 的坐標,因為是計算每一次的偏移的,不是總的偏移。

最后看 UP 和 CANCEL 事件:

     case MotionEvent.ACTION_CANCEL:
         mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
         int initialVelocity = (int) mVelocityTracker.getXVelocity();
         if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
             isFastScroll = true;
             flingX(-initialVelocity);
         } else {
             int x = getScrollX();
             if (x % space != 0) {
                 x -= x % space;
             }
             if (x < -BASELINE_OFFSET) {
                 x = -BASELINE_OFFSET + BASELINE_OFFSET % space;
             } else if (x > (endValue - startValue) * space * 10 - BASELINE_OFFSET) {
                 x = (endValue - startValue) * space * 10 - BASELINE_OFFSET + BASELINE_OFFSET % space;
             }
             scrollTo(x, 0);
             computeAndCallback(x);
         }
         releaseVelocityTracker();
         break;

第1行 computeCurrentVelocity() 方法是手指離開屏幕的瞬間去計算 View 在手機 x-y 方向的速度值;
第2行 getXVelocity() 方法獲得 X 軸方向的速度值 initialVelocity;
第3行 判斷速度是否大于最低 mMinimumVelocity 要求,滿足的話,認為需要慣性滑動,調用方法 flingX():

    /**
     * 慣性滑動
     *
     * @param velocityX
     */
    public void flingX(int velocityX) {
        mScroller.fling(getScrollX(), getScrollY(), velocityX, 0, -BASELINE_OFFSET, (endValue - startValue) * space * 10 - BASELINE_OFFSET, 0, 0);
        awakenScrollBars(mScroller.getDuration());
        invalidate();
    }
    

上面這個方法就是慣性滑動的重點所在。有了初速度,調用 mScroller.fling() 方法交給 Scroller 處理。這里要注意 fling() 方法的8個參數分別代表什么,12參數代表滾動開始的位置,34參數代表這個方向上的初速度,56參數代表X滾動的范圍,78參數代表Y滾動的范圍。

第6行 也就是 else 不滿足最小滾動速度的時候,認為是觸摸事件的抬起,這個時候我們需要手動的將 View 的刻度線滾動到基準線的位置,因為滾動的時候可能基準線位于兩根刻度線之間,這個時候需要校準:

    int x = getScrollX();
    if (x % space != 0) {
        x -= x % space;
    }
    if (x < -BASELINE_OFFSET) {
        x = -BASELINE_OFFSET + BASELINE_OFFSET % space;
    } else if (x > (endValue - startValue) * space * 10 - BASELINE_OFFSET) {
        x = (endValue - startValue) * space * 10 - BASELINE_OFFSET + BASELINE_OFFSET % space;
    }
    scrollTo(x, 0);
    computeAndCallback(x);

首先獲取滾動的長度,如果對 space 取余有余,說明基準線在兩個刻度之間,需要減去這個余數。得到 space 的整倍數的偏移之后,
還要判斷邊界,如果 x 在基準線右邊,說明滾動過頭了,需要回滾到基準線上。由于基準線是偏移過的,所以 scrollTo 的時候需要補上這個偏移;
如果 x 在基準線左邊,說明向左滾過頭了,也需要回滾到基準線上,同理,后面也要加上偏移的量。最后調用 scrollTo() 就可以回到基準線上。

再來看下 onDraw() 方法:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        for (int i = startValue * 10; i < endValue * 10 + 1; i++) {
            int lineHeight = 80;
            if (i % 5 == 0) {
                if (i % 10 == 0) {
                    lineHeight = 120;
                    int x = (i - startValue * 10) * space;
                    if (x > 0 || x < width) {
                        canvas.drawText(String.valueOf(i / 10), x, lineHeight + 50, txtPaint);
                    }
                }
            } else {
                lineHeight = 50;
            }
            int startX = (i - startValue * 10) * space;
            if (startX > 0 || startX < width) {
                canvas.drawLine(startX, 0, startX, lineHeight, grayLinePaint);
            }
        }
        int startX = BASELINE_OFFSET + getScrollX() - BASELINE_OFFSET % space;
        canvas.drawLine(startX, 0, startX, 180, centerLinePaint);
    }

這個方法相比較第一個,有所改動,繪制刻度線的時候不需要再加上偏移量了,直接從 View 的起始開始繪制,滾動就交給 scrollTo() 方法處理了。
這里繪制基準線的時候同樣需要注意,除了加上基準偏移,還要扣除余數,否則,基準線對不準刻度線。

滾動還必須要覆蓋的一個方法是 computeScroll():

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            int x = mScroller.getCurrX();
            scrollTo(x, 0);
            computeAndCallback(x);
            postInvalidate();
        } else {
            if (isFastScroll) {
                int x = mScroller.getCurrX() + BASELINE_OFFSET % space;
                if (x % space != 0) {
                    x -= x % space;
                }
                scrollTo(x, 0);
                computeAndCallback(x);
                postInvalidate();
            }
        }
    }

這里的處理也是要注意細節,if 下面的代碼沒的說,但是 else 下面的代碼是只有快速慣性滾動才能去判斷,否則,手指觸摸的時候也會去計算位置,導致移不動,
這個時候上面的 isFastScroll 就有用處了。另外,這里也要加上基準線扣除的余數,同時還要對space取余數。

我們發現只要滾動,后面都會執行 computeAndCallback() 方法:

    /**
     * 計算并回調位置信息
     *
     * @param scrollX
     */
    private void computeAndCallback(int scrollX) {
        if (mListener != null) {
            int finalX = BASELINE_OFFSET + scrollX;
            if (finalX % space != 0) {
                finalX -= finalX % space;
            }
            mListener.onRulerSelected((endValue - startValue) * 10, startValue * 10 + finalX / space);
        }
    }

就是一個回調,返回當前刻度下的值,這個值需要我們計算。
我們首先拿到基準線對準的 finalX 值,這個值確定下來,減去對 space 的取余數,就能得到對應的刻度個數 finalX / space,只要加上 startValue 就好了。
onRulerSelected 方法第一個參數是長度,沒有特別用處。

到這,全部結束了。

附上 Github地址

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,001評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,786評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,986評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,204評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,964評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,354評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,410評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,554評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,106評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,918評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,093評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,648評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,342評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,755評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,009評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,839評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,107評論 2 375

推薦閱讀更多精彩內容

  • 一、先看效果 二、分析 博客 上看到這個動畫效果很不錯,決定自己來寫試試。動畫拆開來看就是Touch的時候不斷的繪...
    刀放下好好說話閱讀 728評論 0 2
  • 內容是博主照著書敲出來的,博主碼字挺辛苦的,轉載請注明出處,后序內容陸續會碼出。 當了解了Android坐標系和觸...
    Blankj閱讀 6,659評論 3 61
  • 預備知識 Android屏幕區域劃分我們先看一副圖來了解一下Android屏幕的區域劃分,如下:Android屏幕...
    小蕓論閱讀 8,972評論 6 44
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,681評論 25 708
  • 好久沒有真正的睡過一場午覺 在白天做的夢也不都是美夢 夢到天色大變 發生不幸的事情后 天神顯威力 雨水驟降,童話小...
    Mioning閱讀 153評論 0 0