RecyclerView之Scroll和Fling

很久沒(méi)有寫(xiě)Android控件了,正好最近項(xiàng)目有個(gè)自定義控件的需求,是基于RecylerView整理了下,本文先看下ScrollFling的聯(lián)系和區(qū)別

本文分析的源碼是基于recyclerview-v7-26.1.0

1.Scroll和狀態(tài)

Scroll大家都知道,我們可以給RecyclerView添加監(jiān)聽(tīng),在RecyclerView滾動(dòng)的時(shí)候會(huì)回調(diào)。

RecyclerView.addOnScrollListener(mScrollListener);

就是在RecyclerView滾動(dòng)的時(shí)候會(huì)回調(diào)

    /**
     * An OnScrollListener can be added to a RecyclerView to receive messages when a scrolling event
     * has occurred on that RecyclerView.
     * <p>
     * @see RecyclerView#addOnScrollListener(OnScrollListener)
     * @see RecyclerView#clearOnChildAttachStateChangeListeners()
     *
     */
    public abstract static class OnScrollListener {
        /**
         * Callback method to be invoked when RecyclerView's scroll state changes.
         *
         * @param recyclerView The RecyclerView whose scroll state has changed.
         * @param newState     The updated scroll state. One of {@link #SCROLL_STATE_IDLE},
         *                     {@link #SCROLL_STATE_DRAGGING} or {@link #SCROLL_STATE_SETTLING}.
         */
        public void onScrollStateChanged(RecyclerView recyclerView, int newState){}

        /**
         * Callback method to be invoked when the RecyclerView has been scrolled. This will be
         * called after the scroll has completed.
         * <p>
         * This callback will also be called if visible item range changes after a layout
         * calculation. In that case, dx and dy will be 0.
         *
         * @param recyclerView The RecyclerView which scrolled.
         * @param dx The amount of horizontal scroll.
         * @param dy The amount of vertical scroll.
         */
        public void onScrolled(RecyclerView recyclerView, int dx, int dy){}
    }

滾動(dòng)有三種狀態(tài):

    /**
     * The RecyclerView is not currently scrolling.
     * @see #getScrollState()
     */
    public static final int SCROLL_STATE_IDLE = 0;

    /**
     * The RecyclerView is currently being dragged by outside input such as user touch input.
     * @see #getScrollState()
     */
    public static final int SCROLL_STATE_DRAGGING = 1;

    /**
     * The RecyclerView is currently animating to a final position while not under
     * outside control.
     * @see #getScrollState()
     */
    public static final int SCROLL_STATE_SETTLING = 2;

這三種回調(diào)順序:

  • SCROLL_STATE_DRAGGING, 先是手指拖拽的狀態(tài)
  • SCROLL_STATE_SETTLING,再是手指松開(kāi)但是RecyclerView還在滑動(dòng)
  • SCROLL_STATE_IDLE, 最后是RecyclerView滾動(dòng)停止?fàn)顟B(tài)。

2.FlingScroll的關(guān)系

Fling是怎樣一種狀態(tài),手指在屏幕上滑動(dòng)RecyclerView然后松手,RecyclerView中的內(nèi)容會(huì)順著慣性繼續(xù)往手指滑動(dòng)的方向繼續(xù)滾動(dòng)直到停止,這個(gè)過(guò)程叫做FlingFling操作從手指離開(kāi)屏幕瞬間被觸發(fā),在滾動(dòng)停止時(shí)結(jié)束。其實(shí)RecyclerViewFling過(guò)程中會(huì)把state設(shè)置為 SCROLL_STATE_SETTLING。看下源碼:
在Fling觸發(fā)的時(shí)候會(huì)回調(diào)SnapHelper中的onFling

    @Override
    public boolean onFling(int velocityX, int velocityY) {
        LayoutManager layoutManager = mRecyclerView.getLayoutManager();
        if (layoutManager == null) {
            return false;
        }
        RecyclerView.Adapter adapter = mRecyclerView.getAdapter();
        if (adapter == null) {
            return false;
        }
        int minFlingVelocity = mRecyclerView.getMinFlingVelocity();
        return (Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity)
                && snapFromFling(layoutManager, velocityX, velocityY);
    }

    private boolean snapFromFling(@NonNull LayoutManager layoutManager, int velocityX,
            int velocityY) {
        ...
        SmoothScroller smoothScroller = createScroller(layoutManager);
        if (smoothScroller == null) {
            return false;
        }

        int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY);
        if (targetPosition == RecyclerView.NO_POSITION) {
            return false;
        }

        smoothScroller.setTargetPosition(targetPosition);
        layoutManager.startSmoothScroll(smoothScroller);
        return true;
    }

onFling中調(diào)用snapFromFlingfindTargetSnapPosition會(huì)根據(jù)滑動(dòng)速率計(jì)算出要滑動(dòng)到的位置,然后設(shè)置給smoothScroller:

       public void setTargetPosition(int targetPosition) {
             mTargetPosition = targetPosition;
        }

        /**
         * Returns the adapter position of the target item
         *
         * @return Adapter position of the target item or
         * {@link RecyclerView#NO_POSITION} if no target view is set.
         */
        public int getTargetPosition() {
            return mTargetPosition;
        }

snapHelpersmoothScroller默認(rèn)是LinearSmoothScroller,

public class LinearSmoothScroller extends RecyclerView.SmoothScroller

然后會(huì)調(diào)用smoothScroller滾動(dòng)到指定位置, 開(kāi)始滾動(dòng)之前會(huì)調(diào)用start方法, 會(huì)設(shè)置mTargetView就是我們上面設(shè)置的要滾到的位置:

        /**
         * Starts a smooth scroll for the given target position.
         * <p>In each animation step, {@link RecyclerView} will check
         * for the target view and call either
         * {@link #onTargetFound(android.view.View, RecyclerView.State, SmoothScroller.Action)} or
         * {@link #onSeekTargetStep(int, int, RecyclerView.State, SmoothScroller.Action)} until
         * SmoothScroller is stopped.</p>
         *
         * <p>Note that if RecyclerView finds the target view, it will automatically stop the
         * SmoothScroller. This <b>does not</b> mean that scroll will stop, it only means it will
         * stop calling SmoothScroller in each animation step.</p>
         */
        void start(RecyclerView recyclerView, LayoutManager layoutManager) {
            mRecyclerView = recyclerView;
            mLayoutManager = layoutManager;
            if (mTargetPosition == RecyclerView.NO_POSITION) {
                throw new IllegalArgumentException("Invalid target position");
            }
            mRecyclerView.mState.mTargetPosition = mTargetPosition;
            mRunning = true;
            mPendingInitialRun = true;
            mTargetView = findViewByPosition(getTargetPosition());
            onStart();
            mRecyclerView.mViewFlinger.postOnAnimation();
        }

RecyclerView.SmoothScroller滑動(dòng)是會(huì)調(diào)用onAnimation,在滾動(dòng)過(guò)程中如果發(fā)現(xiàn)getChildPosition(mTargetView) == mTargetPosition,也就是目標(biāo)位置已經(jīng)layout出來(lái)了,那么就會(huì)回調(diào) onTargetFound.


public abstract static class SmoothScroller {
      private void onAnimation(int dx, int dy) {
            final RecyclerView recyclerView = mRecyclerView;
            if (!mRunning || mTargetPosition == RecyclerView.NO_POSITION || recyclerView == null) {
                stop();
            }
            mPendingInitialRun = false;
            if (mTargetView != null) {
                // verify target position
                if (getChildPosition(mTargetView) == mTargetPosition) {
                    onTargetFound(mTargetView, recyclerView.mState, mRecyclingAction);
                    mRecyclingAction.runIfNecessary(recyclerView);
                    stop();
                } else {
                    Log.e(TAG, "Passed over target position while smooth scrolling.");
                    mTargetView = null;
                }
            }
            ...
        }
}

public int getChildPosition(View view) {
        return mRecyclerView.getChildLayoutPosition(view);
}

沒(méi)有懸念,繞了一大圈終于回到SnapHelperonTargetFound,在onTargetFound中會(huì)調(diào)用calculateDistanceToFinalSnap:

        return new LinearSmoothScroller(mRecyclerView.getContext()) {
            @Override
            protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
                int[] snapDistances = calculateDistanceToFinalSnap(mRecyclerView.getLayoutManager(),
                        targetView);
                final int dx = snapDistances[0];
                final int dy = snapDistances[1];
                final int time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy)));
                if (time > 0) {
                    action.update(dx, dy, time, mDecelerateInterpolator);
                }
            }

            @Override
            protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
                return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
            }
        };

計(jì)算到目標(biāo)位置的坐標(biāo)然后更新給RecyclerView.Action,然后在上面的onAnimation會(huì)調(diào)用mRecyclingAction.runIfNecessary(recyclerView);,然后會(huì)調(diào)用smoothScrollBy:

            void runIfNecessary(RecyclerView recyclerView) {
                if (mJumpToPosition >= 0) {
                    final int position = mJumpToPosition;
                    mJumpToPosition = NO_POSITION;
                    recyclerView.jumpToPositionForSmoothScroller(position);
                    mChanged = false;
                    return;
                }
                if (mChanged) {
                    validate();
                    if (mInterpolator == null) {
                        if (mDuration == UNDEFINED_DURATION) {
                            recyclerView.mViewFlinger.smoothScrollBy(mDx, mDy);
                        } else {
                            recyclerView.mViewFlinger.smoothScrollBy(mDx, mDy, mDuration);
                        }
                    } else {
                        recyclerView.mViewFlinger.smoothScrollBy(
                                mDx, mDy, mDuration, mInterpolator);
                    }
                    mConsecutiveUpdates++;
                    if (mConsecutiveUpdates > 10) {
                        // A new action is being set in every animation step. This looks like a bad
                        // implementation. Inform developer.
                        Log.e(TAG, "Smooth Scroll action is being updated too frequently. Make sure"
                                + " you are not changing it unless necessary");
                    }
                    mChanged = false;
                } else {
                    mConsecutiveUpdates = 0;
                }
            }

smoothScrollBy中就會(huì)設(shè)置狀態(tài)SCROLL_STATE_SETTLING:

        public void smoothScrollBy(int dx, int dy, int duration, Interpolator interpolator) {
            if (mInterpolator != interpolator) {
                mInterpolator = interpolator;
                mScroller = new OverScroller(getContext(), interpolator);
            }
            setScrollState(SCROLL_STATE_SETTLING);
            mLastFlingX = mLastFlingY = 0;
            mScroller.startScroll(0, 0, dx, dy, duration);
            if (Build.VERSION.SDK_INT < 23) {
                // b/64931938 before API 23, startScroll() does not reset getCurX()/getCurY()
                // to start values, which causes fillRemainingScrollValues() put in obsolete values
                // for LayoutManager.onLayoutChildren().
                mScroller.computeScrollOffset();
            }
            postOnAnimation();
        }

上面繞了一圈,總結(jié)一下調(diào)用棧就是:

SnapHelper
onFling ---> snapFromFling 

上面得到最終位置targetPosition,把位置給RecyclerView.SmoothScroller, 然后就開(kāi)始滑動(dòng)了:

RecyclerView.SmoothScroller
start --> onAnimation

在滑動(dòng)過(guò)程中如果targetPosition對(duì)應(yīng)的targetView已經(jīng)layout出來(lái)了,就會(huì)回調(diào)SnapHelper,然后計(jì)算得到到當(dāng)前位置到targetView的距離dx,dy

SnapHelper
onTargetFound ---> calculateDistanceToFinalSnap

然后把距離dx,dy更新給RecyclerView.Action:

RecyclerView.Action
update --> runIfNecessary --> recyclerView.mViewFlinger.smoothScrollBy

最后調(diào)用RecyclerView.ViewFlinger, 然后又回到onAnimation

class ViewFlinger implements Runnable

        public void smoothScrollBy(int dx, int dy, int duration, Interpolator interpolator) {
            if (mInterpolator != interpolator) {
                mInterpolator = interpolator;
                mScroller = new OverScroller(getContext(), interpolator);
            }
            setScrollState(SCROLL_STATE_SETTLING);
            mLastFlingX = mLastFlingY = 0;
            mScroller.startScroll(0, 0, dx, dy, duration);
            postOnAnimation();
        }

3.觸發(fā)fling操作

接著我們?cè)倏纯碏ling操作在RecyclerView中是什么時(shí)候觸發(fā)的。

先看下RecyclerViewonTouch方法:

@Override
public boolean onTouchEvent(MotionEvent e) {
            case MotionEvent.ACTION_UP: {
                mVelocityTracker.addMovement(vtev);
                eventAddedToVelocityTracker = true;
                mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
                final float xvel = canScrollHorizontally
                        ? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0;
                final float yvel = canScrollVertically
                        ? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0;
                if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
                    setScrollState(SCROLL_STATE_IDLE);
                }
                resetTouch();
            } break;
}

在up觸發(fā)的時(shí)候會(huì)調(diào)用native方法去獲得X和Y方向的速率:

    /**
     * Compute the current velocity based on the points that have been
     * collected.  Only call this when you actually want to retrieve velocity
     * information, as it is relatively expensive.  You can then retrieve
     * the velocity with {@link #getXVelocity()} and
     * {@link #getYVelocity()}.
     * 
     * @param units The units you would like the velocity in.  A value of 1
     * provides pixels per millisecond, 1000 provides pixels per second, etc.
     * @param maxVelocity The maximum velocity that can be computed by this method.
     * This value must be declared in the same unit as the units parameter. This value
     * must be positive.
     */
    public void computeCurrentVelocity(int units, float maxVelocity) {
        nativeComputeCurrentVelocity(mPtr, units, maxVelocity);
    }

如果有其中一個(gè)不為0就會(huì)調(diào)用fling操作:

    public boolean fling(int velocityX, int velocityY) {
        if (mLayout == null) {
            Log.e(TAG, "Cannot fling without a LayoutManager set. "
                    + "Call setLayoutManager with a non-null argument.");
            return false;
        }
        if (mLayoutFrozen) {
            return false;
        }

        final boolean canScrollHorizontal = mLayout.canScrollHorizontally();
        final boolean canScrollVertical = mLayout.canScrollVertically();

        if (!canScrollHorizontal || Math.abs(velocityX) < mMinFlingVelocity) {
            velocityX = 0;
        }
        if (!canScrollVertical || Math.abs(velocityY) < mMinFlingVelocity) {
            velocityY = 0;
        }
        if (velocityX == 0 && velocityY == 0) {
            // If we don't have any velocity, return false
            return false;
        }

        if (!dispatchNestedPreFling(velocityX, velocityY)) {
            final boolean canScroll = canScrollHorizontal || canScrollVertical;
            dispatchNestedFling(velocityX, velocityY, canScroll);

            if (mOnFlingListener != null && mOnFlingListener.onFling(velocityX, velocityY)) {
                return true;
            }

            if (canScroll) {
                int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
                if (canScrollHorizontal) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
                }
                if (canScrollVertical) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
                }
                startNestedScroll(nestedScrollAxis, TYPE_NON_TOUCH);

                velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
                velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
                mViewFlinger.fling(velocityX, velocityY);
                return true;
            }
        }
        return false;
    }

這里有個(gè)mMinFlingVelocity字段,如果x和y方向的速率小于這個(gè)字段,就不會(huì)觸發(fā)fling操作,直接return false,然后就會(huì)把RecyclerView的狀態(tài)設(shè)置為SCROLL_STATE_IDLEmMinFlingVelocityRecyclerView構(gòu)造的時(shí)候會(huì)從ViewConfiguration中獲取

 final ViewConfiguration vc = ViewConfiguration.get(context);
mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();

ViewConfiguration構(gòu)造函數(shù)中:

    /**
     * Minimum velocity to initiate a fling, as measured in dips per second
     */
    private static final int MINIMUM_FLING_VELOCITY = 50;

    /**
     * Maximum velocity to initiate a fling, as measured in dips per second
     */
    private static final int MAXIMUM_FLING_VELOCITY = 8000;

    public ViewConfiguration() {
        mMinimumFlingVelocity = MINIMUM_FLING_VELOCITY;
        mMaximumFlingVelocity = MAXIMUM_FLING_VELOCITY;
    }

所以如果滑動(dòng)速率小于50的話(huà)就不會(huì)觸發(fā)fling操作。

從上面分析知道,fling操作是在手指離開(kāi)的時(shí)候觸發(fā)然后直到滑動(dòng)停止這中間的一段操作。在滑動(dòng)過(guò)程中其實(shí)state狀態(tài)是SCROLL_STATE_SETTLING,然后通過(guò)RecyclerView.ViewFlinger進(jìn)行滑動(dòng)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容