RecyclerView之Scroll和Fling

作者: juexingzhe | 来源:发表于2018-11-11 16:48 被阅读16次

    很久没有写Android控件了,正好最近项目有个自定义控件的需求,是基于RecylerView整理了下,本文先看下ScrollFling的联系和区别

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

    1.Scroll和状态

    Scroll大家都知道,我们可以给RecyclerView添加监听,在RecyclerView滚动的时候会回调。

    RecyclerView.addOnScrollListener(mScrollListener);
    

    就是在RecyclerView滚动的时候会回调

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

    滚动有三种状态:

        /**
         * 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;
    

    这三种回调顺序:

    • SCROLL_STATE_DRAGGING, 先是手指拖拽的状态
    • SCROLL_STATE_SETTLING,再是手指松开但是RecyclerView还在滑动
    • SCROLL_STATE_IDLE, 最后是RecyclerView滚动停止状态。

    2.FlingScroll的关系

    Fling是怎样一种状态,手指在屏幕上滑动RecyclerView然后松手,RecyclerView中的内容会顺着惯性继续往手指滑动的方向继续滚动直到停止,这个过程叫做FlingFling操作从手指离开屏幕瞬间被触发,在滚动停止时结束。其实RecyclerViewFling过程中会把state设置为 SCROLL_STATE_SETTLING。看下源码:
    在Fling触发的时候会回调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中调用snapFromFlingfindTargetSnapPosition会根据滑动速率计算出要滑动到的位置,然后设置给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默认是LinearSmoothScroller,

    public class LinearSmoothScroller extends RecyclerView.SmoothScroller
    

    然后会调用smoothScroller滚动到指定位置, 开始滚动之前会调用start方法, 会设置mTargetView就是我们上面设置的要滚到的位置:

            /**
             * 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滑动是会调用onAnimation,在滚动过程中如果发现getChildPosition(mTargetView) == mTargetPosition,也就是目标位置已经layout出来了,那么就会回调 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);
    }
    

    没有悬念,绕了一大圈终于回到SnapHelperonTargetFound,在onTargetFound中会调用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;
                }
            };
    

    计算到目标位置的坐标然后更新给RecyclerView.Action,然后在上面的onAnimation会调用mRecyclingAction.runIfNecessary(recyclerView);,然后会调用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中就会设置状态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();
            }
    

    上面绕了一圈,总结一下调用栈就是:

    SnapHelper
    onFling ---> snapFromFling 
    

    上面得到最终位置targetPosition,把位置给RecyclerView.SmoothScroller, 然后就开始滑动了:

    RecyclerView.SmoothScroller
    start --> onAnimation
    

    在滑动过程中如果targetPosition对应的targetView已经layout出来了,就会回调SnapHelper,然后计算得到到当前位置到targetView的距离dx,dy

    SnapHelper
    onTargetFound ---> calculateDistanceToFinalSnap
    

    然后把距离dx,dy更新给RecyclerView.Action:

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

    最后调用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.触发fling操作

    接着我们再看看Fling操作在RecyclerView中是什么时候触发的。

    先看下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触发的时候会调用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);
        }
    

    如果有其中一个不为0就会调用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;
        }
    

    这里有个mMinFlingVelocity字段,如果x和y方向的速率小于这个字段,就不会触发fling操作,直接return false,然后就会把RecyclerView的状态设置为SCROLL_STATE_IDLEmMinFlingVelocityRecyclerView构造的时候会从ViewConfiguration中获取

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

    ViewConfiguration构造函数中:

        /**
         * 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;
        }
    

    所以如果滑动速率小于50的话就不会触发fling操作。

    从上面分析知道,fling操作是在手指离开的时候触发然后直到滑动停止这中间的一段操作。在滑动过程中其实state状态是SCROLL_STATE_SETTLING,然后通过RecyclerView.ViewFlinger进行滑动。

    相关文章

      网友评论

        本文标题:RecyclerView之Scroll和Fling

        本文链接:https://www.haomeiwen.com/subject/rfaxfqtx.html