ScrollView源碼分析

本文分析版本: Android API 23

1.簡介

ScrollView是我們在開發中經常使用的控件。當我們需要展示的內容比較多但并不是重復的item時,我們就會使用ScrollView使內容可以在垂直方向滾動顯示防止顯示不全。ScrollView使用起來非常簡單,大多數情況下你甚至都不用寫一行Java代碼就能使用ScrollView了。但是要注意的是ScrollView中只能添加一個子View。今天我們就來看看ScrollView到底是如何實現的。以及最后會教大家一行代碼實現類似IOS上的彈性ScrollView_

2.源碼分析

2.1 繼承關系

extend_relation.png

2.2 主要輔助類

//用來計算滑動位置
private OverScroller mScroller;
//用來繪制邊緣陰影
private EdgeEffect mEdgeGlowTop;
private EdgeEffect mEdgeGlowBottom;
//用于計算滑動時的加速度
private VelocityTracker mVelocityTracker;

2.3 構造方法

ScrollView的構造方法如下:


public ScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);
    initScrollView();

    final TypedArray a = context.obtainStyledAttributes(
            attrs, com.android.internal.R.styleable.ScrollView, defStyleAttr, defStyleRes);
    setFillViewport(a.getBoolean(R.styleable.ScrollView_fillViewport, false));
    a.recycle();
}

在構造方法中分別調用了initScrollView()setFillViewport()方法,代碼如下:

private void initScrollView() {
    //初始化OverScroller
    mScroller = new OverScroller(getContext());
    setFocusable(true);
    setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
    setWillNotDraw(false);
    final ViewConfiguration configuration = ViewConfiguration.get(mContext);
    //被認為是滑動操作的最小距離
    mTouchSlop = configuration.getScaledTouchSlop();
    //最小加速度
    mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
    //最大加速度
    mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
    //用手指拖動超過邊緣的最大距離
    mOverscrollDistance = configuration.getScaledOverscrollDistance();
    //滑動超過邊緣的最大距離
    mOverflingDistance = configuration.getScaledOverflingDistance();
}

可以看到是初始化了一些類與參數,繼續看看setFillViewport()

public void setFillViewport(boolean fillViewport) {
    if (fillViewport != mFillViewport) {
        mFillViewport = fillViewport;
        requestLayout();
    }
}

只是根據布局文件中的fillViewport屬性來給mFillViewport賦值并調用requestLayout()方法。mFillViewport如果為true則表示:將子View的高度延伸到和視圖高度一致,即充滿整個視圖。初始化結束之后,會進入到繪制流程。下面我們按照Measure -> Layout -> Draw的繪制流程來分析ScrollView中的實現。

2.4 Measure、Layout與Draw

2.4.1 onMeasure方法的實現
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    if (!mFillViewport) {
        return;
    }

    final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    if (heightMode == MeasureSpec.UNSPECIFIED) {
        return;
    }

    if (getChildCount() > 0) {
        // 獲取子View
        final View child = getChildAt(0);
        // 獲取ScrollView的高度
        final int height = getMeasuredHeight();
        if (child.getMeasuredHeight() < height) {
            final int widthPadding;
            final int heightPadding;
            final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
            final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
            // 獲取ScrollView的padding
            if (targetSdkVersion >= VERSION_CODES.M) {
                widthPadding = mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin;
                heightPadding = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin;
            } else {
                widthPadding = mPaddingLeft + mPaddingRight;
                heightPadding = mPaddingTop + mPaddingBottom;
            }

            final int childWidthMeasureSpec = getChildMeasureSpec(
                    widthMeasureSpec, widthPadding, lp.width);
            final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                    height - heightPadding, MeasureSpec.EXACTLY);
            //根據新的高度重新measure子View
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
    }
}

從代碼中可以看到首先調用了super.onMeasure(widthMeasureSpec, heightMeasureSpec);即父類FrameLayoutonMeasure()方法。如果我們將mFillViewport設置為false的話將會直接return。當為true時才會繼續執行,會根據子View的高度和ScrollView本身的高度決定是否重新measureView使其充滿ScrollViewScrollViewonMeasure()其實就是處理了mFillViewport

2.4.1 onLayout方法的實現
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    super.onLayout(changed, l, t, r, b);
    mIsLayoutDirty = false;
    // Give a child focus if it needs it
    if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) {
        scrollToChild(mChildToScrollTo);
    }
    mChildToScrollTo = null;
    //是否還未添加過window中去
    if (!isLaidOut()) {
        if (mSavedState != null) {
            mScrollY = mSavedState.scrollPosition;
            mSavedState = null;
        } // mScrollY default value is "0"

        final int childHeight = (getChildCount() > 0) ? getChildAt(0).getMeasuredHeight() : 0;
        final int scrollRange = Math.max(0,
                childHeight - (b - t - mPaddingBottom - mPaddingTop));

        // Don't forget to clamp
        if (mScrollY > scrollRange) {
            mScrollY = scrollRange;
        } else if (mScrollY < 0) {
            mScrollY = 0;
        }
    }

    // Calling this with the present values causes it to re-claim them
    scrollTo(mScrollX, mScrollY);
}

首先也是調用了父類的onLayout方法。接下來處理了是否有需要滾動到的View,以及根據保存的滾動狀態來決定是否需要滾動。如果需要則調用scrollTo()方法。

2.4.1 draw方法的實現
@Override
public void draw(Canvas canvas) {
    super.draw(canvas);
    if (mEdgeGlowTop != null) {
        final int scrollY = mScrollY;
        final boolean clipToPadding = getClipToPadding();
        if (!mEdgeGlowTop.isFinished()) {
            ......
            if (mEdgeGlowTop.draw(canvas)) {
                postInvalidateOnAnimation();
            }
            canvas.restoreToCount(restoreCount);
        }
        if (!mEdgeGlowBottom.isFinished()) {
            ......
            if (mEdgeGlowBottom.draw(canvas)) {
                postInvalidateOnAnimation();
            }
            canvas.restoreToCount(restoreCount);
        }
    }
}

依然是調用了父類的draw方法。之后則是根據是否需要繪制邊緣陰影來繪制陰影。ScrollView的邊緣陰影就是在這里繪制的。值得一提的是包括ListView以及RecycleView的邊緣陰影都是用這種方法來繪制的。以上就是ScrollView的整個繪制流程。可以看出都是調用了父類的對應方法。自身只處理了一些與ScrollView相關的屬性。分析完繪制流程我們就來看看ScrollView中的觸摸事件處理機制,來看看ScrollView中的滑動滾動到底是如何做到的:

2.5 觸摸事件處理

說到觸摸事件的分發與消費機制這算是一個比較基礎的知識。但是要是完全掌握也并不是那么容易的,這里推薦一篇文章Android:View的事件分發與消費機制。對事件處理機制還不了解的同學可以先看看這邊文章。ScrollView因為是繼承自ViewGroup的,所以觸摸事件會依次調用dispatchTouchEvent() -> onInterceptTouchEvent() 若返回true-> onTouchEvent()處理觸摸事件。ScrollView并沒有重寫dispatchTouchEvent()方法,所以我們從onInterceptTouchEvent()方法來看。

2.5.1 onInterceptTouchEvent方法的實現
//這個方法只決定我們是否攔截這個手勢,如果返回true,則onMotionEvent會被調用,并處理滑動事件。
//此方法并不處理事件
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {

    //如果是移動手勢并在處于拖拽階段,直接返回true
    final int action = ev.getAction();
    if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
        return true;
    }

    //如果并不能滑動則返回false
    if (getScrollY() == 0 && !canScrollVertically(1)) {
        return false;
    }

    switch (action & MotionEvent.ACTION_MASK) {
        case MotionEvent.ACTION_MOVE: {
            //檢測用戶是否移動了足夠遠的距離。

            final int activePointerId = mActivePointerId;
            if (activePointerId == INVALID_POINTER) {
                // If we don't have a valid id, the touch down wasn't on content.
                break;
            }

            final int pointerIndex = ev.findPointerIndex(activePointerId);
            if (pointerIndex == -1) {
                Log.e(TAG, "Invalid pointerId=" + activePointerId
                        + " in onInterceptTouchEvent");
                break;
            }
            //得到當前觸摸的y左邊
            final int y = (int) ev.getY(pointerIndex);
            //計算移動的插值
            final int yDiff = Math.abs(y - mLastMotionY);
            //如果yDiff大于最小滑動距離,并且是垂直滑動則認為觸發了滑動手勢。
            if (yDiff > mTouchSlop && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
                //標記拖動狀態為true
                mIsBeingDragged = true;
                //賦值mLastMotionY
                mLastMotionY = y;
                //初始化mVelocityTracker并添加
                initVelocityTrackerIfNotExists();
                mVelocityTracker.addMovement(ev);
                mNestedYOffset = 0;
                if (mScrollStrictSpan == null) {
                    mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
                }
                final ViewParent parent = getParent();
                if (parent != null) {
                    //通知父布局不再攔截觸摸事件
                    parent.requestDisallowInterceptTouchEvent(true);
                }
            }
            break;
        }

        case MotionEvent.ACTION_DOWN: {
            final int y = (int) ev.getY();
            //觸摸點不在子View內
            if (!inChild((int) ev.getX(), (int) y)) {
                mIsBeingDragged = false;
                recycleVelocityTracker();
                break;
            }

            //記錄當前位置
            mLastMotionY = y;
            //記錄pointer的ID,ACTION_DOWN總會在index 0
            mActivePointerId = ev.getPointerId(0);
            //初始化mVelocityTracker
            initOrResetVelocityTracker();
            mVelocityTracker.addMovement(ev);
            //如果在滑動過程中則mIsBeingDragged = true
            mIsBeingDragged = !mScroller.isFinished();
            if (mIsBeingDragged && mScrollStrictSpan == null) {
                mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
            }
            //回調NestedScroll相關接口
            startNestedScroll(SCROLL_AXIS_VERTICAL);
            break;
        }

        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP:
            //清除Drag狀態
            mIsBeingDragged = false;
            mActivePointerId = INVALID_POINTER;
            recycleVelocityTracker();
            if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) {
                postInvalidateOnAnimation();
            }
            //回調NestedScroll相關接口
            stopNestedScroll();
            break;
        case MotionEvent.ACTION_POINTER_UP:
            //當多個手指觸摸中有一個手指抬起時,判斷是不是當前active的點,如果是則尋找新的
            //mActivePointerId
            onSecondaryPointerUp(ev);
            break;
    }
    //最終根據是否開始拖拽的狀態返回
    return mIsBeingDragged;
}

以上就是onInterceptTouchEvent()的整體實現。onInterceptTouchEvent()只決定是否攔截觸摸事件并交給onTouchEvent()處理。內部并不處理觸摸邏輯。ScrollView中根據mIsBeingDragged來決定是否攔截事件。當手指按下發生MotionEvent.ACTION_DOWN時,會記錄當前位置并檢測是否在快速滾動過程中如果是則返回true。當手指移動發生MotionEvent.ACTION_MOVE時,會判斷是否是垂直方向上的滑動事件,如果是則返回true。當手指抬起發生MotionEvent.ACTION_UP時,則清除狀態并返回false。在返回true的情況中,onTouchEvent()方法就會被調用來處理觸摸事件。我們繼續來看onTouchEvent()方法的實現。

2.5.2 onTouchEvent方法的實現

在看onTouchEvent()的實現之前,我們知道在ScrollView中手指無論怎么移動,只會有垂直方向上的滑動發生。而觸摸事件的大致流程是:

    ACTION_DOWN -> ACTION_MOVE -> ... -> ACTION_MOVE -> ACTION_UP

我們根據事件的類型分別來分析:

  • ACTION_DOWN:
    ACTION_DOWN代表手指按下時第一個發生的事件,在onTouchEvent()中實現如下:

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        //初始化VelocityTracker
        initVelocityTrackerIfNotExists();
        //復制當前的MotionEvent賦值給vtev
        MotionEvent vtev = MotionEvent.obtain(ev);

        final int actionMasked = ev.getActionMasked();

        if (actionMasked == MotionEvent.ACTION_DOWN) {
            mNestedYOffset = 0;
        }
        //調整vtev的偏移量
        vtev.offsetLocation(0, mNestedYOffset);

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

                //如果正在fling狀態并且用戶觸摸。則停止fling。
                //當處于fling過程中isFinished為false。
                //fling :即快速滑動。
                if (!mScroller.isFinished()) {
                    //停止
                    mScroller.abortAnimation();
                    if (mFlingStrictSpan != null) {
                        mFlingStrictSpan.finish();
                        mFlingStrictSpan = null;
                    }
                }

                //記錄觸摸事件的初始值
                mLastMotionY = (int) ev.getY();
                mActivePointerId = ev.getPointerId(0);
                startNestedScroll(SCROLL_AXIS_VERTICAL);
                break;
            }
            ......
        }

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

在處理各種事件之前,首先初始化了VelocityTracker。并且復制一個新的MotionEvent對象用于計算加速度。接著開始處理ACTION_DOWN:首先是給mIsBeingDragged賦值,接著檢查是否在fling動畫執行過程中,如果正在執行則停止,這也是為什么我們在ScrollView滑動過程中手指觸摸時會終止ScrollView的滑動。最后記錄了mLastMotionYmActivePointerId

  • ACTION_MOVE:
    當手指移動時,會產生ACTION_MOVE事件:

 @Override
    public boolean onTouchEvent(MotionEvent ev) {
        ......
        switch (actionMasked) {
            ......
            //如果為ACTION_MOVE事件時
            case MotionEvent.ACTION_MOVE:
                final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
                if (activePointerIndex == -1) {
                    Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
                    break;
                }
                //得到當前y值
                final int y = (int) ev.getY(activePointerIndex);
                //計算偏移量deltaY
                int deltaY = mLastMotionY - y;
                //如果dispatchNestedPreScroll返回true,即有NestedScroll存在
                if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
                    deltaY -= mScrollConsumed[1];
                    vtev.offsetLocation(0, mScrollOffset[1]);
                    mNestedYOffset += mScrollOffset[1];
                }
                //如果還未處于drag狀態,并且deltaY大于最小滑動距離,
                //則賦值mIsBeingDragged為true
                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) {
                    //記錄當前的y值
                    mLastMotionY = y - mScrollOffset[1];

                    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)
                            && !hasNestedScrollingParent()) {
                        // Break our velocity if we hit a scroll barrier.
                        mVelocityTracker.clear();
                    }

                    final int scrolledDeltaY = mScrollY - oldY;
                    final int unconsumedY = deltaY - scrolledDeltaY;
                    //處理NestedScroll
                    if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) {
                        mLastMotionY -= mScrollOffset[1];
                        vtev.offsetLocation(0, mScrollOffset[1]);
                        mNestedYOffset += mScrollOffset[1];
                    } else if (canOverscroll) {
                        //如果canOverscroll,即可以越過邊緣滑動。
                        final int pulledToY = oldY + deltaY;
                        //初始化邊緣陰影
                        if (pulledToY < 0) {
                            mEdgeGlowTop.onPull((float) deltaY / getHeight(),
                                    ev.getX(activePointerIndex) / getWidth());
                            if (!mEdgeGlowBottom.isFinished()) {
                                mEdgeGlowBottom.onRelease();
                            }
                        } else if (pulledToY > range) {
                            mEdgeGlowBottom.onPull((float) deltaY / getHeight(),
                                    1.f - ev.getX(activePointerIndex) / getWidth());
                            if (!mEdgeGlowTop.isFinished()) {
                                mEdgeGlowTop.onRelease();
                            }
                        }
                        if (mEdgeGlowTop != null
                                && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) {
                            postInvalidateOnAnimation();
                        }
                    }
                }
                break;
        }
        ......
    }

ACTION_MOVE事件中,首先計算當前的垂直偏移量deltaY。然后判斷是否大于最小滑動距離,并且給mIsBeingDragged賦值。接著如果mIsBeingDraggedtrue。就取得處理滑動需要的各種參數,并調用overScrollBy()方法來處理觸摸事件,overScrollBy()是在View里實現的方法,大致實現如下:

    protected boolean overScrollBy(int deltaX, int deltaY,
            int scrollX, int scrollY,
            int scrollRangeX, int scrollRangeY,
            int maxOverScrollX, int maxOverScrollY,
            boolean isTouchEvent) {
        .....

        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本身是否可以滑動的設定,等等來最終決定了新的newScrollXnewScrollY。接著調用了onOverScrolled()方法來處理滑動,onOverScrolled()方法在View中是空實現,所以再回到ScrollView中可以看到重寫了onOverScrolled()方法:

    @Override
    protected void onOverScrolled(int scrollX, int scrollY,
            boolean clampedX, boolean clampedY) {
        // Treat animating scrolls differently; see #computeScroll() for why.
        if (!mScroller.isFinished()) {
            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);
        }

        awakenScrollBars();
    }

首先是有一句注釋是說:不同的對待滑動動畫,查看computeScroll()方法看原因。說到滑動動畫一定是和Scroller相關了,目前我們還沒涉及到,下面我們再談。回到這里看到根據!mScroller.isFinished()來判斷,根據前面的判斷得知,要么是滑動動畫并不存在,要么就已經被終止,所以在這里!mScroller.isFinished()false。所以會調用super.scrollTo(scrollX, scrollY);最終產生滑動。到這里手指觸摸產生的滑動就分析完了。

  • ACTION_UP:
    ACTION_UP是當我們手指離開時產生的事件,在ScrollView中當我們手指離開時,會根據當前的加速度再滑動一段距離。具體的實現我們來看看是如何實現的:

 @Override
    public boolean onTouchEvent(MotionEvent ev) {
        ......
        switch (actionMasked) {
            ......
            case MotionEvent.ACTION_UP:
                //如果實在drag狀態中
                if (mIsBeingDragged) {
                    //計算加速度
                    final VelocityTracker velocityTracker = mVelocityTracker;
                    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                    int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
                    //如果有有效的加速度
                    if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
                        //處理帶有加速度的滑動事件
                        flingWithNestedDispatch(-initialVelocity);
                    } else if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0,
                            getScrollRange())) {
                        postInvalidateOnAnimation();
                    }

                    mActivePointerId = INVALID_POINTER;
                    //清除drag狀態
                    endDrag();
                }
                break;
        }
        ......
    }

可以看到代碼并不復雜,在計算了加速度后,調用了flingWithNestedDispatch(-initialVelocity);

    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);
            }
        }
    }

代碼如上,我們這里不考慮NestedFling的方式,所以dispatchNestedPreFling(0, velocityY)默認會返回false,所以最終會執行fling(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);

            if (mFlingStrictSpan == null) {
                mFlingStrictSpan = StrictMode.enterCriticalSpan("ScrollView-fling");
            }

            postInvalidateOnAnimation();
        }
    }

可以看到是調用了mScrollerfling方法,在上一篇Scroller源碼分析中,我們已經詳細解釋了Scroller的原理,ScrollView中雖然使用的是OverScroller但是使用方法也是類似的。所以在調用了mScrollerfling方法后。我們需要在computeScroll()處理mScroller計算出的值。ScrollView中的computeScroll()方法實現如下:


    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            int oldX = mScrollX;
            int oldY = mScrollY;
            int x = mScroller.getCurrX();
            int y = mScroller.getCurrY();

            if (oldX != x || oldY != y) {
                final int range = getScrollRange();
                final int overscrollMode = getOverScrollMode();
                final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
                        (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);

                overScrollBy(x - oldX, y - oldY, oldX, oldY, 0, range,
                        0, mOverflingDistance, false);
                onScrollChanged(mScrollX, mScrollY, oldX, oldY);

                if (canOverscroll) {
                    if (y < 0 && oldY >= 0) {
                        mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity());
                    } else if (y > range && oldY <= range) {
                        mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity());
                    }
                }
            }

            if (!awakenScrollBars()) {
                // Keep on drawing until the animation has finished.
                postInvalidateOnAnimation();
            }
        } else {
            if (mFlingStrictSpan != null) {
                mFlingStrictSpan.finish();
                mFlingStrictSpan = null;
            }
        }
    }

我省略了一些注釋,意思是說:computeScroll()會在繪制的過程中調用,為了不重復的顯示滾動條。這里重復做了scrollTo()方法中的代碼。但并沒有調用scrollTo(),因為scrollTo()中也有滾動條相關的處理。所以computeScroll()中也調用了overScrollBy()方法處理滑動。所以最終仍然會調用onOverScrolled()方法:

    @Override
    protected void onOverScrolled(int scrollX, int scrollY,
            boolean clampedX, boolean clampedY) {
        // Treat animating scrolls differently; see #computeScroll() for why.
        if (!mScroller.isFinished()) {
            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);
        }

        awakenScrollBars();
    }

這次就會進入到第一個if語句里,可以看到是給mScrollXmScrollY賦值后調用了invalidateParentIfNeeded();方法來完成最終的滑動處理。

  • ACTION_POINTER_DOWN:

 @Override
    public boolean onTouchEvent(MotionEvent ev) {
        ......
        switch (actionMasked) {
            ......
            case MotionEvent.ACTION_POINTER_DOWN: {
                //更新狀態,即新的觸摸手勢決定是否滑動。
                final int index = ev.getActionIndex();
                mLastMotionY = (int) ev.getY(index);
                mActivePointerId = ev.getPointerId(index);
                break;
            }
        }
        ......
    }

ACTION_POINTER_DOWN是指有另外一個手指發生了觸摸。這里的處理是將mActivePointerId賦值給新的點了。所以在ScrollView中當有一個手指按下,我們再按下另一個手指時,第二個按下的手指能決定ScrollView的滑動。

  • ACTION_POINTER_UP:

 @Override
    public boolean onTouchEvent(MotionEvent ev) {
        ......
        switch (actionMasked) {
            ......
            case MotionEvent.ACTION_POINTER_UP:
                onSecondaryPointerUp(ev);
                mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId));
                break;
        }
        ......
    }

ACTION_POINTER_UP是指多個手指中的一個手指離開屏幕。所以這里會檢測是否是當前active的手指離開了,并做相應的處理,具體邏輯在onSecondaryPointerUp(ev);方法中,我們就不多解釋了。至此整個ScrollView我們應該有了一個清晰完整的理解了。最后再分享一個小trick。一行代碼實現仿ios的彈性ScrollView

3.一行代碼實現彈性ScrollView

我們都知道ios上的彈性滑動做的相當順滑。我們Android系統一直都沒有。看完ScrollView的代碼發現,其實通過變量mOverflingDistance就能決定彈性滑動的最大值。但是ScrollView并沒有暴露出方法給我們設置。但是我們只需要通過反射來設定mOverflingDistance的值即可。。JOOR對反射做了封裝,可以使我們非常簡潔的來寫反射,所以這里我們只需要一行代碼即可:

Reflect.on(scrollView).set("mOverflingDistance", 100);

這樣就實現了彈性ScrollView。以上demo的代碼在SkyScrollViewDemo

我每周會寫一篇源代碼分析的文章,以后也可能會有其他主題.
如果你喜歡我寫的文章的話,歡迎關注我的新浪微博@達達達達sky
地址: http://weibo.com/u/2030683111
每周我會第一時間在微博分享我寫的文章,也會積極轉發更多有用的知識給大家.

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

推薦閱讀更多精彩內容