Android - ScrollView源碼分析及簡單實現

我的CSDN: ListerCi
我的簡書: 東方未曦

一、ScrollView介紹及源碼分析

ScrollView是Android日常開發中比較常見的一個ViewGroup,它只能有一個子View。用戶在滑動時子View在ScrollView內部滾動,顯示不同的區域。
在開發中如果需要將多個不同類型的視圖垂直排列時,我們一般會使用ScrollView。但是永遠不要將RecyclerView和ListView添加到ScrollView中,這會造成很不好的用戶體驗。並且ScrollView只支持縱向滑動,如果需要橫向滑動,可以考慮使用HorizontalScrollView

雖然ScrollView的功能比較簡單,代碼也不多,但是麻雀雖小,五臟俱全,其中包含了對事件攔截、滑動、多指等一系列的處理。通過分析其源碼,可以對事件處理、View的滾動和Scroller的使用有個基礎的認識。

1.1 預備知識。

1.1.1: 事件分發機制

這裏不準備展開來講這個複雜的話題,我們從宏觀上來理解:由於事件是由父View向子View傳遞的,對於ViewGroup來說,可以通過onInterceptTouchEvent()攔截某次事件來自己處理,也可以將其傳遞給子View。
對ScrollView來說,如果用戶在上下滑動手指,那麼就可以攔截此時的滑動事件並讓子View上下滾動,哪怕你的手指在Button上,也不會觸發它的點擊事件,因爲都被ScrollView消費掉了。

1.1.2: 多指處理

如果用戶同時使用多根手指滑動ScrollView,那麼ScrollView應該聽誰的?Android給出的答案是:誰最後來的就聽誰的。因此需要對每根手指進行編號來對多指觸控進行管理,用於給手指編號的就是pointerId和pointerIndex。
在一次事件流**(從第1根手指落下開始,到最後一根手指離開,之間的所有MotionEvent稱爲一次事件流)**中,每根手指的pointerId都不會改變,但是pointerIndex會向前填充。

舉個例子,這裏從0開始計數:
第0根手指落下時,此時的所有的pointerId和pointerIndex爲[(0: 0)];

直到第3根手指落下時,值爲[(0: 0), (1: 1), (2: 2), (3: 3)];
擡起第2根手指,值爲[(0: 0), (1: 1), (3: 2)],可以發現,pointerId爲2的值不見了,但是pointerId爲3的手指的pointerIndex向前填補變成了2;
擡起pointerId爲1的手指,值變爲[(0: 0), (3: 1)],pointerId爲3的手指的pointerIndex又往前填補了一位變爲了1。

1.2 源碼分析

1.2.1 測量

一般情況下ScrollView的子View高度是大於ScrollView本身高度的,但是按照傳統的measure方式,子View的測量模式如果是AT_MOST或者EXACTLY,那麼測量出的高度不會大於ScrollView,也就無法滑動。
因此ScrollView重寫了measureChild()measureChildWithMargins (),將測量模式改成了UNSPECIFIED,這樣在測量時能夠得到子View完整的高度。

    @Override
    protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        ViewGroup.LayoutParams lp = child.getLayoutParams();

        int childWidthMeasureSpec;
        int childHeightMeasureSpec;

        childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft
                + mPaddingRight, lp.width);
        final int verticalPadding = mPaddingTop + mPaddingBottom;
        childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
                Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - verticalPadding),
                MeasureSpec.UNSPECIFIED);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

    @Override
    protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int usedTotal = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin +
                heightUsed;
        final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
                Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
                MeasureSpec.UNSPECIFIED);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
1.2.2 事件攔截

ScrollView繼承自FrameLayout,重寫其onInterceptTouchEvent()方法可以攔截事件:當該方法返回true時,事件交給ScrollView的onTouchEvent()方法處理。那麼什麼時候需要將事件攔截呢?回憶一下我們使用ScrollView的場景,無非是兩種情況:
一、手指上下移動的範圍大於某個值,此時表示用戶想滑動ScrollView。
二、ScrollView處於慣性滑動中,用戶手指按下,則ScrollView停止滾動並且用戶可以直接滑動。

在代碼中,我們通過一個值mIsBeingDragged來表示當前是否處於滑動狀態,如果是,那麼攔截事件。
精簡後的代碼如下,這裏刪除了和事件攔截無關的代碼並添加了部分註釋。

public boolean onInterceptTouchEvent(MotionEvent ev) {
        /*
         * 考慮最常見的一種情況:此時ScrollView處於滑動狀態,
         * 並且用戶在滑動手指,則可以直接攔截該事件
         */
        final int action = ev.getAction();
        if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
            return true;
        }
        //......

        switch (action & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_MOVE: {
                /*
                 * 當手指移動的距離超過touchSlop時纔開始攔截
                 */
                // 省略部分邊界處理......
                final int y = (int) ev.getY(pointerIndex);
                final int yDiff = Math.abs(y - mLastMotionY);
                if (yDiff > mTouchSlop) { // y上的滑動距離大於某個值
                    mIsBeingDragged = true;
                    mLastMotionY = y;
                    final ViewParent parent = getParent();
                    if (parent != null) {
                        parent.requestDisallowInterceptTouchEvent(true);
                    }
                }
                break;
            }

            case MotionEvent.ACTION_DOWN: {
                final int y = (int) ev.getY();
                /*
                 * 記錄ACTION_DOWN時的座標
                 * 此時的pointerIndex一定爲0
                 */
                mLastMotionY = y;
                mActivePointerId = ev.getPointerId(0);
                /*
                 * 如果在慣性滑動時用戶Down下,則設置爲滑動狀態。
                 * 我們需要先調用computeScrollOffset()之後isFinished()纔會返回正確的值
                 */
                mScroller.computeScrollOffset();
                mIsBeingDragged = !mScroller.isFinished();
                break;
            }

            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                /* 手指擡起,取消滑動狀態 */
                mIsBeingDragged = false;
                mActivePointerId = INVALID_POINTER;
                // 省略回彈
                break;
            case MotionEvent.ACTION_POINTER_UP:
                onSecondaryPointerUp(ev);
                break;
        }
        /*
         * 只有在滑動狀態下ScrollView才需要攔截事件
         */
        return mIsBeingDragged;
    }
1.2.3 手指拖動及慣性滑動

當ScrollView將事件攔截後,就交給自身的onTouchEvent()處理,總的來說onTouchEvent()需要做兩件事情:一是在用戶ACTION_MOVE時使ScrollView中的內容隨着手指拖動;二是在用戶ACTION_UP時通過VelocityTracker計算速度並開啓慣性滑動。
除了上面說的兩個功能,ScrollView的onTouchEvent()方法中還添加了嵌套滑動相關的功能,將這部分刪掉,精簡後的代碼如下所示:

    public boolean onTouchEvent(MotionEvent ev) {
        initVelocityTrackerIfNotExists();
        MotionEvent vtev = MotionEvent.obtain(ev);
        final int actionMasked = ev.getActionMasked();

        switch (actionMasked) {
            case MotionEvent.ACTION_DOWN: {
                if ((mIsBeingDragged = !mScroller.isFinished())) {
                    final ViewParent parent = getParent();
                    if (parent != null) {
                        parent.requestDisallowInterceptTouchEvent(true);
                    }
                }
                /*
                 * isFinished()爲false時還處於fling,也就是慣性滑動中
                 * 如果用戶此時觸摸屏幕,則停止fling
                 */
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                mLastMotionY = (int) ev.getY();
                mActivePointerId = ev.getPointerId(0);
                break;
            }
            case MotionEvent.ACTION_MOVE:
                final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
                // activePointerIndex非法值處理
                if (activePointerIndex == -1) {
                    Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
                    break;
                }

                final int y = (int) ev.getY(activePointerIndex);
                int deltaY = mLastMotionY - y;
                // 省略嵌套滑動dispatchNestedPreScroll部分......
                if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
                    final ViewParent parent = getParent();
                    if (parent != null) {
                        parent.requestDisallowInterceptTouchEvent(true);
                    }
                    mIsBeingDragged = true;
                    // 優化滑動體驗,可以去掉
                    if (deltaY > 0) {
                        deltaY -= mTouchSlop;
                    } else {
                        deltaY += mTouchSlop;
                    }
                }
                if (mIsBeingDragged) {
                    final int oldY = mScrollY;
                    final int range = getScrollRange();
                    final int overscrollMode = getOverScrollMode();
                    boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
                            (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
                    // overScrollBy(...)就是實際進行滾動的方法
                    if (overScrollBy(0, deltaY, 0, mScrollY, 0, range, 0, mOverscrollDistance, true)) {
                        // 當遇到滑動障礙時消除速度
                        mVelocityTracker.clear();
                    }
                    // 省略嵌套滑動dispatchNestedScroll部分......
                    if (canOverscroll) {
                        // 省略滑動到邊緣時繪製的陰影......
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
                if (mIsBeingDragged) {
                    final VelocityTracker velocityTracker = mVelocityTracker;
                    // 計算速度
                    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                    // 得到Y方向上的速度
                    int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
                    if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
                        // 開啓慣性滑動,內部實際調用fling
                        flingWithNestedDispatch(-initialVelocity);
                    }
                    mActivePointerId = INVALID_POINTER;
                    endDrag();
                }
                break;
            case MotionEvent.ACTION_CANCEL:
                if (mIsBeingDragged && getChildCount() > 0) {
                    if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) {
                        postInvalidateOnAnimation();
                    }
                    mActivePointerId = INVALID_POINTER;
                    endDrag();
                }
                break;
            case MotionEvent.ACTION_POINTER_DOWN: {
                final int index = ev.getActionIndex();
                mLastMotionY = (int) ev.getY(index);
                mActivePointerId = ev.getPointerId(index);
                break;
            }
            case MotionEvent.ACTION_POINTER_UP:
                onSecondaryPointerUp(ev);
                mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId));
                break;
        }
        // 將每次的事件添加到mVelocityTracker以便計算速度
        if (mVelocityTracker != null) {
            mVelocityTracker.addMovement(vtev);
        }
        vtev.recycle();
        return true;
    }

可以看到ACTION_MOVE時調用了overScrollBy(...)方法來進行滾動,該方法是View類中的方法,看一下它做了什麼。

    /**
     * 爲那些滑動時會超過邊界的View提供滑動處理
     * 調用這個方法的類需要重寫onOverScrolled(int, int, boolean, boolean)方法
     * View可以通過此方法處理任何touch或者fling的滑動
     * @return true 說明滑動到了最大邊界並被限制在了邊界上
     */
    protected boolean overScrollBy(int deltaX, int deltaY,
            int scrollX, int scrollY,// 當前的x、y方向上的滑動值
            int scrollRangeX, int scrollRangeY, // 最大的滑動距離
            int maxOverScrollX, int maxOverScrollY,// 可以超過滑動距離多少
            boolean isTouchEvent) {
        final int overScrollMode = mOverScrollMode;
        final boolean canScrollHorizontal =
                computeHorizontalScrollRange() > computeHorizontalScrollExtent();
        final boolean canScrollVertical =
                computeVerticalScrollRange() > computeVerticalScrollExtent();
        final boolean overScrollHorizontal = overScrollMode == OVER_SCROLL_ALWAYS ||
                (overScrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollHorizontal);
        final boolean overScrollVertical = overScrollMode == OVER_SCROLL_ALWAYS ||
                (overScrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollVertical);

        int newScrollX = scrollX + deltaX;
        if (!overScrollHorizontal) {
            maxOverScrollX = 0;
        }

        int newScrollY = scrollY + deltaY;
        if (!overScrollVertical) {
            maxOverScrollY = 0;
        }

        // Clamp values if at the limits and record
        final int left = -maxOverScrollX;
        final int right = maxOverScrollX + scrollRangeX;
        final int top = -maxOverScrollY;
        final int bottom = maxOverScrollY + scrollRangeY;

        boolean clampedX = false;
        if (newScrollX > right) {
            newScrollX = right;
            clampedX = true;
        } else if (newScrollX < left) {
            newScrollX = left;
            clampedX = true;
        }

        boolean clampedY = false;
        if (newScrollY > bottom) {
            newScrollY = bottom;
            clampedY = true;
        } else if (newScrollY < top) {
            newScrollY = top;
            clampedY = true;
        }
        // 調用重寫的方法
        onOverScrolled(newScrollX, newScrollY, clampedX, clampedY);
        return clampedX || clampedY;
    }

發現overScrollBy(...)方法對滑動的最大距離進行了限制,但實際上並沒有真的滑動View,該方法內部調用了onOverScrolled (...)方法,由於ScrollView重寫了該方法,那麼調用的就是重寫後的方法,代碼如下所示。
如果當前不處於fling狀態,用戶通過手指拖動時觸發ACTION_MOVE事件,此時會運行到else代碼塊,最終通過scrollTo()方法來滾動ScrollView中的內容。

protected void onOverScrolled(int scrollX, int scrollY,
            boolean clampedX, boolean clampedY) {
        if (!mScroller.isFinished()) { // 此時處於fling狀態
            final int oldX = mScrollX;
            final int oldY = mScrollY;
            mScrollX = scrollX;
            mScrollY = scrollY;
            invalidateParentIfNeeded();
            onScrollChanged(mScrollX, mScrollY, oldX, oldY);
            if (clampedY) {
                mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange());
            }
        } else {
            super.scrollTo(scrollX, scrollY);
        }
    }

手指拖動時的邏輯講完,來看一下慣性滑動。在onTouchEvent()中,在ACTION_UP時計算出了縱向的速度,然後調用flingWithNestedDispatch ()方法開始慣性滑動,看名字就知道該方法也加入了嵌套滑動相關的功能,代碼如下所示。

	private void flingWithNestedDispatch(int velocityY) {
        final boolean canFling = (mScrollY > 0 || velocityY > 0) &&
                (mScrollY < getScrollRange() || velocityY < 0);
        if (!dispatchNestedPreFling(0, velocityY)) {
            dispatchNestedFling(0, velocityY, canFling);
            if (canFling) {
                fling(velocityY);
            }
        }
    }

其實本質上就是調用了fling(int velocityY)方法。

    public void fling(int velocityY) {
        if (getChildCount() > 0) {
            int height = getHeight() - mPaddingBottom - mPaddingTop;
            int bottom = getChildAt(0).getHeight();

            mScroller.fling(mScrollX, mScrollY, 0, velocityY, 0, 0, 0,
                    Math.max(0, bottom - height), 0, height/2);
            postInvalidateOnAnimation();
        }
    }

代碼調用了OverScroller的fling(...)方法和postInvalidateOnAnimation ()方法。
OverScroller類只用於計算數值,其功能類似ValueAnimator,在調用fling(...)方法之後,它內部就會計算某一時刻的ScrollY值應該爲多少,我們在每次繪製的時候獲取當前的ScrollY值並設置即可。那麼具體該怎麼做呢?

其中的關鍵就是View類中的computeScroll()方法,這個方法會在View每次繪製的時候被調用,當你調用invalidate()等重繪方法時,computeScroll()也會被調用。
看下源碼,發現是空實現,很明顯是讓我們重寫它。註釋說該方法一般被用於ViewGroup更新其子View的mScrollX和mScrollY值,並且基本是與Scroller配合使用的。

    /**
     * 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() {
    }

因此ScrollView的fling()方法中的postInvalidateOnAnimation ()方法最終還會觸發ScrollView重寫的computeScroll()方法:

    public void computeScroll() {
        if (mScroller.computeScrollOffset()) { // mScroller的動畫還沒結束
            int oldX = mScrollX;
            int oldY = mScrollY;
            int x = mScroller.getCurrX();
            int y = mScroller.getCurrY();

            if (oldX != x || oldY != y) {
                final int range = getScrollRange();
                overScrollBy(x - oldX, y - oldY, oldX, oldY, 0, range,
                        0, mOverflingDistance, false);
                onScrollChanged(mScrollX, mScrollY, oldX, oldY);
            }

            if (!awakenScrollBars()) {
                // 在動畫結束前繼續調用postInvalidateOnAnimation觸發重繪
                postInvalidateOnAnimation();
            }
        }
    }

可以看到computeScroll()內部調用了overScrollBy()方法進行了移動,而在動畫沒有完全結束前,又會再次調用postInvalidateOnAnimation()方法在下次繪製時繼續移動。

二、簡單實現

經過上面的分析可以發現ScrollView的基礎功能並不難實現,下面是一個簡單的版本,有基礎的滑動和overScroll,不包含進度條、邊緣的陰影和嵌套滑動,用法和ScrollView一致。

public class MyScrollView extends FrameLayout {

    private int mTouchSlop;
    private boolean mIsBeingDragged;
    private int mActivePointerId = MotionEvent.INVALID_POINTER_ID;
    private int mLastMotionY;

    private OverScroller mScroller;
    private VelocityTracker mVelocityTracker;
    private int mMinimumVelocity;
    private int mMaximumVelocity;
    private int mOverscrollDistance;
    private int mOverflingDistance;

    public MyScrollView(@NonNull Context context) {
        this(context, null);
    }

    public MyScrollView(@NonNull Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyScrollView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        mScroller = new OverScroller(getContext());
        setFocusable(true);
        setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
        setWillNotDraw(false);
        setOverScrollMode(OVER_SCROLL_ALWAYS);
        final ViewConfiguration configuration = ViewConfiguration.get(getContext());
        mTouchSlop = configuration.getScaledTouchSlop();
        mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
        mOverscrollDistance = configuration.getScaledOverscrollDistance();
        mOverflingDistance = configuration.getScaledOverflingDistance();
    }

    private void initOrResetVelocityTracker() {
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        } else {
            mVelocityTracker.clear();
        }
    }

    private void initVelocityTrackerIfNotExists() {
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
    }

    private void recycleVelocityTracker() {
        if (mVelocityTracker != null) {
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        final int action = ev.getAction();
        if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
            return true;
        }

        if (super.onInterceptTouchEvent(ev)) {
            return true;
        }
        if (getScrollY() == 0 && !canScrollVertically(1)) {
            return false;
        }

        switch (action & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_MOVE: {
                final int activePointerId = mActivePointerId;
                if (activePointerId == MotionEvent.INVALID_POINTER_ID) {
                    break;
                }

                final int pointerIndex = ev.findPointerIndex(activePointerId);
                if (pointerIndex == -1) {
                    Log.e("TAG", "Invalid pointerId=" + activePointerId
                            + " in onInterceptTouchEvent");
                    break;
                }

                final int y = (int) ev.getY(pointerIndex);
                final int yDiff = Math.abs(y - mLastMotionY);
                if (yDiff > mTouchSlop) {
                    mIsBeingDragged = true;
                    mLastMotionY = y;
                    initVelocityTrackerIfNotExists();
                    mVelocityTracker.addMovement(ev);
                    final ViewParent parent = getParent();
                    if (parent != null) {
                        parent.requestDisallowInterceptTouchEvent(true);
                    }
                }
                break;
            }

            case MotionEvent.ACTION_DOWN: {
                final int y = (int) ev.getY();
                if (!inChild((int) ev.getX(), (int) y)) {
                    mIsBeingDragged = false;
                    recycleVelocityTracker();
                    break;
                }

                mLastMotionY = y;
                mActivePointerId = ev.getPointerId(0);

                initOrResetVelocityTracker();
                mVelocityTracker.addMovement(ev);
                mScroller.computeScrollOffset();
                mIsBeingDragged = !mScroller.isFinished();
                break;
            }

            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                mIsBeingDragged = false;
                mActivePointerId = MotionEvent.INVALID_POINTER_ID;
                recycleVelocityTracker();
                if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0,
                        0, getScrollRange())) {
                    postInvalidateOnAnimation();
                }
                break;
            case MotionEvent.ACTION_POINTER_UP:
                onSecondaryPointerUp(ev);
                break;
        }
        return mIsBeingDragged;
    }

    private boolean inChild(int x, int y) {
        if (getChildCount() > 0) {
            final int scrollY = getScrollY();
            final View child = getChildAt(0);
            return !(y < child.getTop() - scrollY
                    || y >= child.getBottom() - scrollY
                    || x < child.getLeft()
                    || x >= child.getRight());
        }
        return false;
    }

    @Override
    protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
        ViewGroup.LayoutParams lp = child.getLayoutParams();
        int childWidthMeasureSpec;
        int childHeightMeasureSpec;
        childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, getPaddingLeft()
                + getPaddingRight(), lp.width);
        final int verticalPadding = getPaddingTop() + getPaddingBottom();
        childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - verticalPadding),
                MeasureSpec.UNSPECIFIED);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

    @Override
    protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int usedTotal = getPaddingTop() + getPaddingBottom() + lp.topMargin + lp.bottomMargin +
                heightUsed;
        final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
                MeasureSpec.UNSPECIFIED);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

    private int getScrollRange() {
        int scrollRange = 0;
        if (getChildCount() > 0) {
            View child = getChildAt(0);
            scrollRange = Math.max(0,
                    child.getHeight() - (getHeight() - getPaddingBottom() - getPaddingTop()));
        }
        return scrollRange;
    }

    private void onSecondaryPointerUp(MotionEvent ev) {
        final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
                MotionEvent.ACTION_POINTER_INDEX_SHIFT;
        final int pointerId = ev.getPointerId(pointerIndex);
        if (pointerId == mActivePointerId) {
            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
            mLastMotionY = (int) ev.getY(newPointerIndex);
            mActivePointerId = ev.getPointerId(newPointerIndex);
            if (mVelocityTracker != null) {
                mVelocityTracker.clear();
            }
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        initVelocityTrackerIfNotExists();
        final int actionMasked = ev.getActionMasked();

        switch (actionMasked) {
            case MotionEvent.ACTION_DOWN: {
                if (getChildCount() == 0) {
                    return false;
                }
                if ((mIsBeingDragged = !mScroller.isFinished())) {
                    final ViewParent parent = getParent();
                    if (parent != null) {
                        parent.requestDisallowInterceptTouchEvent(true);
                    }
                }

                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }

                mLastMotionY = (int) ev.getY();
                mActivePointerId = ev.getPointerId(0);
                startNestedScroll(SCROLL_AXIS_VERTICAL);
                break;
            }
            case MotionEvent.ACTION_MOVE:
                final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
                if (activePointerIndex == -1) {
                    Log.e("TAG", "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
                    break;
                }

                final int y = (int) ev.getY(activePointerIndex);
                int deltaY = mLastMotionY - y;
                if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
                    final ViewParent parent = getParent();
                    if (parent != null) {
                        parent.requestDisallowInterceptTouchEvent(true);
                    }
                    mIsBeingDragged = true;
                    if (deltaY > 0) {
                        deltaY -= mTouchSlop;
                    } else {
                        deltaY += mTouchSlop;
                    }
                }
                if (mIsBeingDragged) {
                    mLastMotionY = y;
                    if (overScrollBy(0, deltaY, 0, getScrollY(), 0, getScrollRange(),
                            0, mOverscrollDistance, true)) {
                        // Break our velocity if we hit a scroll barrier.
                        mVelocityTracker.clear();
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
                if (mIsBeingDragged) {
                    final VelocityTracker velocityTracker = mVelocityTracker;
                    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                    int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);

                    if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
                        fling(-initialVelocity);
                    } else if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0,
                            getScrollRange())) {
                        postInvalidateOnAnimation();
                    }

                    mActivePointerId = MotionEvent.INVALID_POINTER_ID;
                    endDrag();
                }
                break;
            case MotionEvent.ACTION_CANCEL:
                if (mIsBeingDragged && getChildCount() > 0) {
                    if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0,
                            0, getScrollRange())) {
                        postInvalidateOnAnimation();
                    }
                    mActivePointerId = MotionEvent.INVALID_POINTER_ID;
                    endDrag();
                }
                break;
            case MotionEvent.ACTION_POINTER_DOWN: {
                final int index = ev.getActionIndex();
                mLastMotionY = (int) ev.getY(index);
                mActivePointerId = ev.getPointerId(index);
                break;
            }
            case MotionEvent.ACTION_POINTER_UP:
                onSecondaryPointerUp(ev);
                mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId));
                break;
        }

        if (mVelocityTracker != null) {
            mVelocityTracker.addMovement(ev);
        }
        return true;
    }

    public void fling(int velocityY) {
        if (getChildCount() > 0) {
            int height = getHeight() - getPaddingBottom() - getPaddingTop();
            int bottom = getChildAt(0).getHeight();
            mScroller.fling(getScrollX(), getScrollY(), 0, velocityY, 0, 0, 0,
                    Math.max(0, bottom - height), 0, height/2);
            postInvalidateOnAnimation();
        }
    }

    private void endDrag() {
        mIsBeingDragged = false;
        recycleVelocityTracker();
    }

    @Override
    protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
        if (!mScroller.isFinished()) {
            final int oldX = getScrollX();
            final int oldY = getScrollY();
            setScrollX(scrollX);
            setScrollY(scrollY);
            onScrollChanged(getScrollX(), getScrollY(), oldX, oldY);
            if (clampedY) {
                mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange());
            }
        } else {
            super.scrollTo(scrollX, scrollY);
        }
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            int oldX = getScrollX();
            int oldY = getScrollY();
            int x = mScroller.getCurrX();
            int y = mScroller.getCurrY();
            if (oldX != x || oldY != y) {
                final int range = getScrollRange();
                overScrollBy(x - oldX, y - oldY, oldX, oldY, 0, range,
                        0, mOverflingDistance, false);
                onScrollChanged(getScrollX(), getScrollY(), oldX, oldY);
            }
            if (!awakenScrollBars()) {
                // Keep on drawing until the animation has finished.
                postInvalidateOnAnimation();
            }
        }
    }
}

三、補充&總結

  1. ScrollView有一個屬性叫fillViewPort,如果該屬性設置爲true,那麼即使子View的高度小於ScrollView,它也會撐滿ScrollView。具體可見ScrollView的measure()方法。
  2. ScrollView的嵌套滑動功能類似一個連接父Layout和子View的橋樑,本身沒有實現具體的功能。
    我的理解就是這樣,如果有不同的意見和想法,歡迎在評論區討論。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章