Android View 的滾動(dòng)原理和 Scroller、VelocityTracker 類(lèi)的使用

Android 開(kāi)發(fā)中經(jīng)常涉及 View 的滾動(dòng),例如類(lèi)似于 ScrollView 的滾動(dòng)手勢(shì)和滾動(dòng)動(dòng)畫(huà),例如用 ListView 模仿 iOS 上的左滑刪除 item,例如 ListView 的下拉刷新。這些都是常見(jiàn)的需求,同時(shí)也都涉及 View 滾動(dòng)的相關(guān)知識(shí)。

本文將解析 Android 中 View 的滾動(dòng)原理,并介紹與滾動(dòng)相關(guān)的兩個(gè)輔助類(lèi) ScrollerVelocityTracker,并通過(guò) 3 個(gè)逐漸深入的例子來(lái)加深理解。

注:

  1. 本文沒(méi)有嘗試實(shí)現(xiàn)上述幾種功能,只闡述基本原理和基礎(chǔ)類(lèi)的使用方法。
  2. 文中的例子只是截取了與 View 相關(guān)的代碼,完整的示例代碼請(qǐng)見(jiàn)DEMO
  3. 本文的源碼分析基于 Android API Level 21,并省略掉部分與本文關(guān)系不大的代碼。

View 的滾動(dòng)原理

在了解 View 的滾動(dòng)原理之前,我們先來(lái)想象一個(gè)場(chǎng)景:我們坐在一個(gè)房間里,透過(guò)一扇窗戶看窗外的風(fēng)景。窗戶是有大小限制的,而風(fēng)景是沒(méi)有大小限制的。

把上述的場(chǎng)景對(duì)應(yīng)到 Android 的 View 顯示原理上來(lái):當(dāng)一個(gè) View 顯示在界面上,它的上下左右邊緣就圍成了這個(gè) View 的可視區(qū)域,我們可以稱(chēng)這個(gè)區(qū)域?yàn)椤翱梢暣翱凇?,我們平時(shí)看到的 View 的內(nèi)容,都是透過(guò)這個(gè)可視窗口中看到的“風(fēng)景”。View 的大小內(nèi)容可以無(wú)窮大,不受可視窗口大小的限制。

另外,如果在窗外的風(fēng)景中,有一個(gè)人出現(xiàn)在窗戶右邊很遠(yuǎn)的地方,那么我們?cè)诜块g里就看不到那個(gè)人;如果那個(gè)人站在窗戶正對(duì)著出去的地方,那么我們就可以透過(guò)窗戶看到他。對(duì)應(yīng)到 View 上面來(lái),只有出現(xiàn)在“可視窗口”中的那部分內(nèi)容可以被看到。

View 的 scroll 相關(guān)

在 View 類(lèi)中,有兩個(gè)變量 mScrollXmScrollY,它們記錄的是 View 的內(nèi)容的偏移值。mScrollXmScrollY 的默認(rèn)值都是 0,即默認(rèn)不偏移。另外我們需要知道一點(diǎn),向左滑動(dòng),mScrollX 為正數(shù),反正為負(fù)數(shù)。假設(shè)我們令 mScrollX = 10,那么該 View 的內(nèi)容會(huì)相對(duì)于原來(lái)向左偏移 10px。 看看系統(tǒng)的 View 類(lèi)中的源碼:

// View.java
public class View {

  /**
  * The offset, in pixels, by which the content of this view is scrolled
  * horizontally.
  * {@hide}
  */
  protected int mScrollX;
  
  /**
  * The offset, in pixels, by which the content of this view is scrolled
  * vertically.
  * {@hide}
  */
  protected int mScrollY;
  
  // ...
}

通常我們比較少直接設(shè)置 mScrollXmScrollY,而是通過(guò) View 提供的兩個(gè)方法來(lái)設(shè)置。

// 瞬時(shí)滾動(dòng)到某個(gè)位置
public void scrollTo(int x, int y)
// 瞬時(shí)滾動(dòng)某個(gè)距離
public void scrollBy(int x, int y)

看看兩個(gè)方法的源碼:

// View.java
/**
* Set the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
* @param x the x position to scroll to
* @param y the y position to scroll to
*/
public void scrollTo(int x, int y) {
    if (mScrollX != x || mScrollY != y) {
        int oldX = mScrollX;
        int oldY = mScrollY;
        mScrollX = x;
        mScrollY = y;
        invalidateParentCaches();
        onScrollChanged(mScrollX, mScrollY, oldX, oldY);
        if (!awakenScrollBars()) {
            postInvalidateOnAnimation();
        }
    }
}

/**
* Move the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
* @param x the amount of pixels to scroll by horizontally
* @param y the amount of pixels to scroll by vertically
*/
public void scrollBy(int x, int y) {
    scrollTo(mScrollX + x, mScrollY + y);
}

首先看 scrollTo(int x, int y) 方法,它除了設(shè)置 mScrollXmScrollY 兩個(gè)變量,還會(huì)觸發(fā)自己重新繪制,另外還會(huì)通過(guò) onScrollChanged 觸發(fā)回調(diào)。而 scrollBy 方法其實(shí)也是調(diào)用 scrollTo 方法。

明顯,兩個(gè)方法的區(qū)別在于 scrollTo 方法是滾動(dòng)到特定位置,參數(shù) x、y 代表“絕對(duì)位置”,而 scrollBy 方法是在當(dāng)前位置基礎(chǔ)上滾動(dòng)特定距離,參數(shù) xy 代表“相對(duì)位置”。

另外,View 還提供了 mScrollXmScrollY 的 getter:

// 獲取 mScrollX
public final int getScrollX()
// 獲取 mScrollY
public final int getScrollY()

看看源碼中這兩個(gè)方法的注釋?zhuān)梢愿玫乩斫?scroll 的概念。

// View.java
/**
* Return the scrolled left position of this view. This is the left edge of
* the displayed part of your view. You do not need to draw any pixels
* farther left, since those are outside of the frame of your view on
* screen.
*
* @return The left edge of the displayed part of your view, in pixels.
*/
public final int getScrollX() {
    return mScrollX;
}
/**
* Return the scrolled top position of this view. This is the top edge of
* the displayed part of your view. You do not need to draw any pixels above
* it, since those are outside of the frame of your view on screen.
*
* @return The top edge of the displayed part of your view, in pixels.
*/
public final int getScrollY() {
    return mScrollY;
}

例子1

為了更好地理解 mScrollXmScrollY,也為后續(xù)介紹的知識(shí)做準(zhǔn)備,我們先看一個(gè)例子:

/**
* 示例:自定義 ViewGroup,包含幾個(gè)一字排開(kāi)的子 View,

* 每個(gè)子 View 都與該 ViewGroup 一樣大。
* 調(diào)用 moveToIndex 方法會(huì)調(diào)用 scrollTo 方法,從而瞬時(shí)滾動(dòng)到某一位置
*/
public class Case1ViewGroup extends ViewGroup {

    public static final int CHILD_NUMBER = 6;
    private int mCurrentIndex = 0;

    public Case1ViewGroup(Context context) {
        super(context);
        init();
    }

    public Case1ViewGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public Case1ViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        // 添加幾個(gè)子 View
        for (int i = 0; i < CHILD_NUMBER; i++) {
            TextView child = new TextView(getContext());
            int color;
            switch (i % 3) {
                case 0:
                    color = 0xffcc6666;
                    break;
                case 1:
                    color = 0xffcccc66;
                    break;
                case 2:
                default:
                    color = 0xff6666cc;
                    break;
            }
            child.setBackgroundColor(color);
            child.setGravity(Gravity.CENTER);
            child.setTextSize(TypedValue.COMPLEX_UNIT_SP, 46);
            child.setTextColor(0x80ffffff);
            child.setText(String.valueOf(i));
            addView(child);
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);

        // 每個(gè)子 View 都與自己一樣大
        for (int i = 0, childCount = getChildCount(); i < childCount; i++) {
            View childView = getChildAt(i);
            childView.measure(
                    MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
                    MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
        }

        setMeasuredDimension(width, height);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        // 子 View 一字排開(kāi)
        for (int i = 0, childCount = getChildCount(); i < childCount; i++) {
            View childView = getChildAt(i);
            childView.layout(getWidth() * i, 0, getWidth() * (i + 1), b - t);
        }
    }

    /**
    * 瞬時(shí)滾動(dòng)到第幾個(gè)子 View
    * @param targetIndex 要移動(dòng)到第幾個(gè)子 View
    */
    public void moveToIndex(int targetIndex) {
        if (!canMoveToIndex(targetIndex)) {
            return;
        }
        scrollTo(targetIndex * getWidth(), getScrollY());
        mCurrentIndex = targetIndex;
        invalidate();
    }

    /**
    * 判斷移動(dòng)的子 View 下標(biāo)是否合法
    * @param index 要移動(dòng)到第幾個(gè)子 View
    * @return index 是否合法
    */
    public boolean canMoveToIndex(int index) {
        return index < CHILD_NUMBER && index >= 0;
    }

    public int getCurrentIndex() {
        return mCurrentIndex;
    }
}

將以上這個(gè)自定義的 ViewGroup 放到 Activity 中,調(diào)用它的 moveToIndex(int targetIndex) 就可以實(shí)現(xiàn)瞬時(shí)滾動(dòng)到第 n 個(gè)子 View 了。(完整示例代碼見(jiàn)DEMO

Scroller 類(lèi) —— 計(jì)算滾動(dòng)位置的輔助類(lèi)

到目前為止,我們已經(jīng)能通過(guò) View 提供的方法設(shè)置 mScrollX、mScrollY,來(lái)使 View “滾動(dòng)”。但這種滾動(dòng)都是瞬時(shí)的,換句話說(shuō),這種滾動(dòng)都是無(wú)動(dòng)畫(huà)的。實(shí)際上我們想要做到的滾動(dòng)是平滑的、有動(dòng)畫(huà)的,就像我們不希望窗戶外面的那個(gè)人突然出現(xiàn)在窗戶中間,這樣會(huì)嚇到我們,我們更希望那個(gè)人能有一個(gè)“慢慢走進(jìn)視覺(jué)范圍”的過(guò)程。

Scroller 類(lèi)就是幫助我們實(shí)現(xiàn) View 平滑滾動(dòng)的一個(gè)輔助類(lèi),使用方法通常是在 View 中作為一個(gè)成員變量,用 Scroller 類(lèi)來(lái)記錄/計(jì)算 View 的滾動(dòng)位置,再?gòu)?Scroller 類(lèi)中讀取出計(jì)算結(jié)果,設(shè)置到 View 中。這里注意一點(diǎn):在 Scroller 中設(shè)置和計(jì)算 View 的滾動(dòng)位置并不會(huì)影響 View 的滾動(dòng),只有從 Scroller 中取出計(jì)算結(jié)果并設(shè)置到 View 中時(shí),滾動(dòng)才會(huì)實(shí)際生效。

Scroller 提供了一系列方法來(lái)執(zhí)行滾動(dòng)、計(jì)算滾動(dòng)位置,以下列出幾個(gè)重要方法:

// 開(kāi)始滾動(dòng),并記下當(dāng)前時(shí)間點(diǎn)作為開(kāi)始滾動(dòng)的時(shí)間點(diǎn)
public void startScroll(int startX, int startY, int dx, int dy, int duration)
// 停止?jié)L動(dòng)
public void abortAnimation()
// 計(jì)算當(dāng)前時(shí)間點(diǎn)對(duì)應(yīng)的滾動(dòng)位置,并返回動(dòng)畫(huà)是否還在進(jìn)行
public boolean computeScrollOffset()
// 獲取上一次 computeScrollOffset 執(zhí)行時(shí)的滾動(dòng) x 值
public final int getCurrX()
// 獲取上一次 computeScrollOffset 執(zhí)行時(shí)的滾動(dòng) y 值
public final int getCurrY()
// 根據(jù)當(dāng)前的時(shí)間點(diǎn),判斷動(dòng)畫(huà)是否已結(jié)束
public final boolean isFinished()

有了這幾個(gè)方法,我們?nèi)菀紫氲饺绾螌?shí)現(xiàn) View 的平滑滾動(dòng)動(dòng)畫(huà):

  • 在開(kāi)始動(dòng)畫(huà)時(shí)調(diào)用 startScroll 方法,傳入動(dòng)畫(huà)開(kāi)始位置、移動(dòng)距離、動(dòng)畫(huà)時(shí)長(zhǎng);
  • 每隔一段時(shí)間,調(diào)用 computeScrollOffset 方法,計(jì)算當(dāng)前時(shí)間點(diǎn)對(duì)應(yīng)的滾動(dòng)位置;
  • 如果上一步返回 true,代表動(dòng)畫(huà)仍在進(jìn)行,則調(diào)用 getCurrXgetCurrY 方法獲取當(dāng)前位置,并調(diào)用 View 的 scrollTo 方法使 View 滾動(dòng);
  • 不斷循環(huán)進(jìn)行第 2 步,直到返回 false,代表動(dòng)畫(huà)結(jié)束。

這里提到“每隔一段時(shí)間”,從直覺(jué)上我們可能覺(jué)得應(yīng)該有個(gè)循環(huán),但實(shí)際上我們可以借助 View 的 computeScroll 方法來(lái)實(shí)現(xiàn)。先看看 computeScroll 方法的源碼:

// View.java
/**
* Called by a parent to request that a child update its values for mScrollX
* and mScrollY if necessary. This will typically be done if the child is
* animating a scroll using a {@link android.widget.Scroller Scroller}
* object.
*/
public void computeScroll() {
}

看注釋可知該方法天生就是用來(lái)計(jì)算 View 的 mScrollXmScrollY 值,該方法會(huì)在父 View 調(diào)用該 View 的 draw 方法之前被自動(dòng)調(diào)用,View 類(lèi)中默認(rèn)沒(méi)有實(shí)現(xiàn)任何內(nèi)容,我們需要自己實(shí)現(xiàn)。所以我們只需要在該方法中,用 Scroller 計(jì)算并設(shè)置 mScrollXmScrollY 的值,并判斷如果動(dòng)畫(huà)沒(méi)結(jié)束則讓該 View 失效(調(diào)用 postInvalidate() 方法),觸發(fā)下一次 computeScroll,就可以實(shí)現(xiàn)上述循環(huán)。

例子2

這個(gè)例子的 ViewGroup 繼承自例子 1 的 ViewGroup,擁有同樣的子 View,區(qū)別只在于例子 2 是通過(guò) Scroller 來(lái)滾動(dòng),實(shí)現(xiàn)了滾動(dòng)的動(dòng)畫(huà),而不再是瞬時(shí)滾動(dòng)。

/**
* 示例:自定義一個(gè) ViewGroup,包含幾個(gè)一字排開(kāi)的子 View,

* 每個(gè)子 View 都與該 ViewGroup 一樣大。
* 通過(guò) Scroller 實(shí)現(xiàn)滾動(dòng)。
* 調(diào)用 moveToIndex 方法會(huì)觸發(fā) Scroller 的 startScroller,開(kāi)始動(dòng)畫(huà),并使 View 失效。
* 并在 computeScroll 方法中判斷動(dòng)畫(huà)是否在進(jìn)行,進(jìn)而計(jì)算當(dāng)前滾動(dòng)位置,并觸發(fā)下一次 View 失效。
*/
public class Case2ViewGroup extends Case1ViewGroup {

    // 滾動(dòng)器
    protected Scroller mScroller;

    public Case2ViewGroup(Context context) {
        super(context);
        initScroller();
    }

    public Case2ViewGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
        initScroller();
    }

    public Case2ViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initScroller();
    }

    private void initScroller() {
        mScroller = new Scroller(getContext());
    }

    /**
    * 通過(guò)動(dòng)畫(huà)滾動(dòng)到第幾個(gè)子 View
    * @param targetIndex 要移動(dòng)到第幾個(gè)子 View
    */
    @Override
    public void moveToIndex(int targetIndex) {
        if (!canMoveToIndex(targetIndex)) {
            return;
        }
        mScroller.startScroll(
                getScrollX(), getScrollY(),
                targetIndex * getWidth() - getScrollX(), getScrollY());
        mCurrentIndex = targetIndex;
        invalidate();
    }

    public void stopMove() {
        if (!mScroller.isFinished()) {
            int currentX = mScroller.getCurrX();
            int targetIndex = (currentX + getWidth() / 2) / getWidth();
            mScroller.abortAnimation();
            this.scrollTo(targetIndex * getWidth(), 0);
            mCurrentIndex = targetIndex;
        }
    }

    /**
    * 在 ViewGroup.dispatchDraw() -> ViewGroup.drawChild() -> View.draw(Canvas,ViewGroup,long) 時(shí)被調(diào)用
    * 任務(wù):計(jì)算 mScrollX & mScrollY 應(yīng)有的值,然后調(diào)用scrollTo/scrollBy
    */
    @Override
    public void computeScroll() {
        boolean isNotFinished = mScroller.computeScrollOffset();
        if (isNotFinished) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate();
        }
    }

}

將以上這個(gè)自定義的 ScrollerViewGroup 放到 Activity 中,調(diào)用它的 moveToIndex(int targetIndex) 就可以實(shí)現(xiàn)滾動(dòng)到第 n 個(gè)子 View 了。(在 Activity 中使用的完整示例代碼見(jiàn)DEMO

VelocityTracker —— 計(jì)算滾動(dòng)速度的輔助類(lèi)

到目前為止,我們已經(jīng)可以實(shí)現(xiàn) View 平滑的滾動(dòng)動(dòng)畫(huà),那么如果我們還想根據(jù)用戶手指在 View 上滑動(dòng)的速度和距離來(lái)控制 View 的滾動(dòng),應(yīng)該怎么做?Android 系統(tǒng)提供了另一個(gè)輔助類(lèi) VelocityTracker 來(lái)實(shí)現(xiàn)類(lèi)似功能。

VelocityTracker 是一個(gè)速度跟蹤器,通過(guò)用戶操作時(shí)(通常在 View 的 onTouchEvent 方法中)傳進(jìn)去一系列的 Event,該類(lèi)就可以計(jì)算出用戶手指滑動(dòng)的速度,開(kāi)發(fā)者可以方便地獲取這些參數(shù)去做其他事情?;蛘呤种富瑒?dòng)超過(guò)一定速度并松手,就觸發(fā)翻頁(yè)。

看看 VelocityTracker 類(lèi)提供的幾個(gè)常用的方法,這些方法分為幾類(lèi):

  • 初始化和銷(xiāo)毀:

    // 由系統(tǒng)分配一個(gè) VelocityTracker 對(duì)象,而不是 new 一個(gè)
    static public VelocityTracker obtain()
    
    - // 使用完畢時(shí)調(diào)用該方法回收 VelocityTracker 對(duì)象
    public void recycle()
    
  • 添加 Event 以供追蹤:

    // 不斷調(diào)用該方法傳入一系列 event,記錄用戶的操作
    public void addMovement(MotionEvent event)
    
  • 計(jì)算速度:

    // 計(jì)算調(diào)用該方法的時(shí)刻對(duì)應(yīng)的速度,傳入的是速度的計(jì)時(shí)單位
    public void computeCurrentVelocity(int units)
    
    // 調(diào)用 computeCurrentVelocity 方法后就可以通過(guò)該方法獲取之前計(jì)算的 x 方向速度
    public float getXVelocity()
    
    // 調(diào)用 computeCurrentVelocity 方法后就可以通過(guò)該方法獲取之前計(jì)算的 y 方向速度
    public float getYVelocity()
    

例子3

下面通過(guò)一個(gè)例子來(lái)看看 VelocityTracker 的用法。該例子的 ViewGroup 繼承自例子 2 的 ViewGroup,擁有同樣的子 View,區(qū)別在于除了可以用動(dòng)畫(huà)來(lái)滾動(dòng),還可以用手勢(shì)來(lái)拖動(dòng)滾動(dòng)。重點(diǎn)看該 ViewGroup 的 onTouchEvent 方法:

/**
* 示例:自定義一個(gè) ViewGroup,包含幾個(gè)一字排開(kāi)的子 View,

* 每個(gè)子 View 都與該 ViewGroup 一樣大。
* 通過(guò) VelocityTracker 監(jiān)控手指滑動(dòng)速度。
*/
public class Case3ViewGroup extends Case2ViewGroup {

    // 速度監(jiān)控器
    private VelocityTracker mVelocityTracker;

    public Case3ViewGroup(Context context) {
        super(context);
    }

    public Case3ViewGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public Case3ViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    // 非滑動(dòng)狀態(tài)
    private static final int TOUCH_STATE_REST = 0;
    // 滑動(dòng)狀態(tài)
    private static final int TOUCH_STATE_SCROLLING = 1;
    // 表示當(dāng)前狀態(tài)
    private int mTouchState = TOUCH_STATE_REST;

    // 上一次事件的位置
    private float mLastMotionX;
    // 觸發(fā)滾動(dòng)的最小滑動(dòng)距離,手指滑動(dòng)超過(guò)該距離才認(rèn)為是要拖動(dòng),防止手抖
    private int mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
    // 最小滑動(dòng)速率,手指滑動(dòng)超過(guò)該速度時(shí)才會(huì)觸發(fā)翻頁(yè)
    private static final int VELOCITY_MIN = 600;

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        final int action = ev.getAction();

        //表示已經(jīng)開(kāi)始滑動(dòng)了,不需要走該 ACTION_MOVE 方法了。
        if ((action == MotionEvent.ACTION_MOVE) && (mTouchState != TOUCH_STATE_REST)) {
            return true;
        }

        final float x = ev.getX();

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mLastMotionX = x;
                mTouchState = mScroller.isFinished() ? TOUCH_STATE_REST : TOUCH_STATE_SCROLLING;
                break;

            case MotionEvent.ACTION_MOVE:
                final int xDiff = (int) Math.abs(mLastMotionX - x);
                //超過(guò)了最小滑動(dòng)距離,就可以認(rèn)為開(kāi)始滑動(dòng)了
                if (xDiff > mTouchSlop) {
                    mTouchState = TOUCH_STATE_SCROLLING;
                }
                break;

            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                mTouchState = TOUCH_STATE_REST;
                break;
        }
        return mTouchState != TOUCH_STATE_REST;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);

        // 速度監(jiān)控器,監(jiān)控每一個(gè) event
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(event);

        // 觸摸點(diǎn)
        final float eventX = event.getX();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:

                // 如果滾動(dòng)未結(jié)束時(shí)按下,則停止?jié)L動(dòng)
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                // 記錄按下位置
                mLastMotionX = eventX;
                break;
            case MotionEvent.ACTION_MOVE:
                // 手指移動(dòng)的位移
                int deltaX = (int)(eventX - mLastMotionX);
                // 滾動(dòng)內(nèi)容,前提是不超出邊界
                int targetScrollX = getScrollX() - deltaX;
                if (targetScrollX >= 0 &&
                        targetScrollX <= getWidth() * (CHILD_NUMBER - 1)) {
                    scrollTo(targetScrollX, 0);
                }
                // 記下手指的新位置
                mLastMotionX = eventX;
                break;
            case MotionEvent.ACTION_UP:
                // 計(jì)算速度
                mVelocityTracker.computeCurrentVelocity(1000);
                float velocityX = mVelocityTracker.getXVelocity();
                if (velocityX > VELOCITY_MIN && canMoveToIndex(getCurrentIndex() - 1)) {
                    // 自動(dòng)向右邊繼續(xù)滑動(dòng)
                    moveToIndex(getCurrentIndex() - 1);
                } else if (velocityX < -VELOCITY_MIN && canMoveToIndex(getCurrentIndex() + 1)) {
                    // 自動(dòng)向左邊繼續(xù)滑動(dòng)
                    moveToIndex(getCurrentIndex() + 1);
                } else {
                    // 手指速度不夠或不允許再滑

                    int targetIndex = (getScrollX() + getWidth() / 2) / getWidth();
                    moveToIndex(targetIndex);
                }
                // 回收速度監(jiān)控器
                if (mVelocityTracker != null) {
                    mVelocityTracker.recycle();
                    mVelocityTracker = null;
                }
                //修正 mTouchState 值
                mTouchState = TOUCH_STATE_REST;
                break;
            case MotionEvent.ACTION_CANCEL:
                mTouchState = TOUCH_STATE_REST;
                break;
        }

        return true;
    }
}

在該例子中,在 View 的 onTouchEvent 方法中,在 ACTION_MOVE 手指移動(dòng)中不斷調(diào)用 scrollTo 方法,實(shí)現(xiàn) View 跟隨手指移動(dòng);同時(shí),將 Event 不斷地添加到 mVelocityTracker 速度監(jiān)控器中,并在 ACTION_UP 手指抬起時(shí)從速度監(jiān)控器中獲取速度,當(dāng)速度達(dá)到某一閾值時(shí)自動(dòng)滾動(dòng)到上一頁(yè)或下一頁(yè)。

總結(jié)

至此,我們已經(jīng)了解了 View 的滾動(dòng)原理,并兩個(gè)輔助類(lèi)來(lái)幫助控制 View 的滾動(dòng)位置和滾動(dòng)速度??偨Y(jié)一下:

  • View 的顯示可以理解為透過(guò)“視覺(jué)窗口”來(lái)看內(nèi)容,內(nèi)容可以無(wú)限大,改變 View 的 mScrollXmScrollY 可以看到不同的內(nèi)容,實(shí)現(xiàn)瞬時(shí)滾動(dòng)。
  • 調(diào)用 View 的 scrollToscrollBy 方法可以瞬時(shí)滾動(dòng) View。
  • Scroller 輔助類(lèi)可以協(xié)助實(shí)現(xiàn) View 的滾動(dòng)動(dòng)畫(huà),實(shí)現(xiàn)方法是:調(diào)用 startScroll 方法開(kāi)始滾動(dòng),并在 View 的 computeScroll 方法中不斷改變 mScrollXmScrollY 來(lái)滾動(dòng) View。
  • VelocityTracker 輔助類(lèi)可以協(xié)助追蹤 View 的滾動(dòng)速度,通常是在 View 的 onTouchEvent 方法中將 Event 傳進(jìn)該類(lèi)中來(lái)追蹤。調(diào)用該類(lèi)的 computeCurrentVelocity 方法之后,就可以調(diào)用 getXVelocitygetYVelocity 方法分別獲取 x 方向和 y 方向的速度。

有了上述的知識(shí)和工具后,我們就能實(shí)現(xiàn)很多與滾動(dòng)相關(guān)的效果。

以上,感謝閱讀。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容