1. 1. scrollTo 和 scrollBy
    1. 1.1. scrollByInternal 介绍
    2. 1.2. LayoutManager.scrollHorizontallyBy 介绍
    3. 1.3. View.offsetLeftAndRight 介绍
    4. 1.4. 小结
  2. 2. scrollToPosition
    1. 2.1. 小结
  3. 3. smoothScrollBy
    1. 3.1. 小结
  4. 4. smoothScrollToPosition
    1. 4.1. 小结
    2. 4.2. 补充滚到到一个屏幕外的位置
    3. 4.3. 补充 scrollToPositionWithOffset
  5. 5. 手势滑动
    1. 5.1. 小结
  6. 6. 总结
  7. 7. REF

本文总结一下RecyclerView提供的滑动相关的api,并探究一下为什么有的滑动方法不会回调监听 onScrollStateChanged(int state)

RecyclerView scroll 相关API有:

  • scrollTo(int x, int y)
  • scrollBy(int x, int y)
  • scrollToPosition(int position)
  • smoothScrollBy(@Px int dx, @Px int dy) 及其重载方法
  • smoothScrollToPosition(int position)
  • 当然,还有手势滑动
  • RecyclerView 通过 addOnScrollListener(OnScrollListener) 监听滑动事件

    RecyclerView 调用这个方法通知layoutmanager,scroll状态已经改变

    @Override
    public void onScrollStateChanged(int state) {
    super.onScrollStateChanged(recyclerView, newState);
    if (state == SCROLL_STATE_IDLE) {
    //滑动停止
    }
    }

    @Override
    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
    super.onScrolled(recyclerView, dx, dy);
    //...
    }

    但是并不是所有的滑动行为都会回调到 onScrollStateChanged ,下面来一一分析下每个滑动场景

    scrollTo 和 scrollBy

    scrollTo 和 scrollBy 是 View 相关方法,实现的是 View 内容 的滑动,效果是 View 控件本身并没有滑动,而是控件上绘制的内容在控件范围内发生滑动。
    看下在 RecyclerView 里面的实现:

    // RecyclerView
    @Override
    public void scrollTo(int x, int y) {
    // RecyclerView 不支持滚动到绝对位置,尝试使用 scrollToPosition 替换
    Log.w(TAG, "RecyclerView does not support scrolling to an absolute position. "
    + "Use scrollToPosition instead");
    }

    @Override
    public void scrollBy(int x, int y) {
    // 这个方法可以配合实现 NestedScrollingChild 的控件联动
    if (mLayout == null) {
    Log.e(TAG, "Cannot scroll without a LayoutManager set. "
    + "Call setLayoutManager with a non-null argument.");
    return;
    }
    if (mLayoutSuppressed) {
    return;
    }
    final boolean canScrollHorizontal = mLayout.canScrollHorizontally();
    final boolean canScrollVertical = mLayout.canScrollVertically();
    if (canScrollHorizontal || canScrollVertical) {
    scrollByInternal(canScrollHorizontal ? x : 0, canScrollVertical ? y : 0, null);
    }
    }

    scrollByInternal 介绍

    scrollByInternal后面在处理手势拖动的时候也会用到,继续往下看:

    // RecyclerView
    boolean scrollByInternal(int x, int y, MotionEvent ev) {
    int unconsumedX = 0;
    int unconsumedY = 0;
    int consumedX = 0;
    int consumedY = 0;

    consumePendingUpdateOperations();
    if (mAdapter != null) {
    mReusableIntPair[0] = 0;
    mReusableIntPair[1] = 0;
    // 这里处理滑动
    scrollStep(x, y, mReusableIntPair);
    consumedX = mReusableIntPair[0];
    consumedY = mReusableIntPair[1];
    unconsumedX = x - consumedX;
    unconsumedY = y - consumedY;
    }
    // ... 分发 nestedscroll 结果
    dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,
    TYPE_TOUCH, mReusableIntPair);
    // ... 通知 onScrolled 事件
    if (consumedX != 0 || consumedY != 0) {
    dispatchOnScrolled(consumedX, consumedY);
    }
    // ...
    }

    dispatchOnScrolled 会通知 onScrolled 的监听者

    scrollStep方法,如下:

    // RecyclerView
    void scrollStep(int dx, int dy, @Nullable int[] consumed) {
    // ...
    int consumedX = 0;
    int consumedY = 0;
    if (dx != 0) { // 调用LayoutManager处理滑动
    consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
    }
    if (dy != 0) {
    consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
    }
    // ...
    }

    LayoutManager.scrollHorizontallyBy 介绍

    本文以下都以 LinearLayoutManager 为例,继续看scrollHorizontallyBy 方法:

    // LinearLayoutManager
    int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
    if (getChildCount() == 0 || delta == 0) {
    return 0;
    }
    ensureLayoutState();
    mLayoutState.mRecycle = true;
    final int layoutDirection = delta > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
    final int absDelta = Math.abs(delta);
    updateLayoutState(layoutDirection, absDelta, true, state);
    final int consumed = mLayoutState.mScrollingOffset
    + fill(recycler, mLayoutState, state, false);
    // ...
    final int scrolled = absDelta > consumed ? layoutDirection * consumed : delta;
    mOrientationHelper.offsetChildren(-scrolled);

    mLayoutState.mLastScrollDelta = scrolled;
    return scrolled;
    }

    mOrientationHelper是LinearLayoutManager 类的一个全局变量,初始化是在:

    // LinearLayoutManager
    public void setOrientation(@RecyclerView.Orientation int orientation) {
    if (orientation != HORIZONTAL && orientation != VERTICAL) {
    throw new IllegalArgumentException("invalid orientation:" + orientation);
    }

    assertNotInLayoutOrScroll(null);

    if (orientation != mOrientation || mOrientationHelper == null) {
    mOrientationHelper =
    OrientationHelper.createOrientationHelper(this, orientation);
    mAnchorInfo.mOrientationHelper = mOrientationHelper;
    mOrientation = orientation;
    requestLayout();
    }
    }

    继续往下看:

    // abstract class OrientationHelper
    public static OrientationHelper createOrientationHelper(
    RecyclerView.LayoutManager layoutManager, @RecyclerView.Orientation int orientation) {
    switch (orientation) {
    case HORIZONTAL:
    return createHorizontalHelper(layoutManager);
    case VERTICAL:
    return createVerticalHelper(layoutManager);
    }
    throw new IllegalArgumentException("invalid orientation");
    }

    以HORIZONTAL 方向为例:

    public static OrientationHelper createHorizontalHelper(
    RecyclerView.LayoutManager layoutManager) {
    return new OrientationHelper(layoutManager) {
    @Override
    public int getEndAfterPadding() {
    return mLayoutManager.getWidth() - mLayoutManager.getPaddingRight();
    }

    @Override
    public int getEnd() {
    return mLayoutManager.getWidth();
    }

    @Override
    public void offsetChildren(int amount) {
    // 最终还是调用的全局变量 mLayoutManager 的 offsetChildrenHorizontal 方法
    mLayoutManager.offsetChildrenHorizontal(amount);
    // 垂直方向调用的就是下面的方法:
    // mLayoutManager.offsetChildrenVertical(amount);
    }
    //...
    }
    }

    private OrientationHelper(RecyclerView.LayoutManager layoutManager) {
    mLayoutManager = layoutManager;
    }

    mLayoutManager 作为 OrientationHelper 构造方法的唯一参数。
    点击mLayoutManager.offsetChildrenHorizontal(amount) 方法,跳转到的是RecyclerView.LayoutManger的方法:

    // RecyclerView.LayoutManger 
    public void offsetChildrenHorizontal(@Px int dx) {
    if (mRecyclerView != null) {
    mRecyclerView.offsetChildrenHorizontal(dx);
    }
    }

    回到RecyclerView的这个方法:

    public void offsetChildrenHorizontal(@Px int dx) {
    final int childCount = mChildHelper.getChildCount();
    for (int i = 0; i < childCount; i++) {
    // getChildAt返回的是每个child view
    mChildHelper.getChildAt(i).offsetLeftAndRight(dx);
    }
    }

    View.offsetLeftAndRight 介绍

    经过上面的跳转步骤,最终发现会调用到列表里每个View的 offsetLeftAndRight 方法:

    // View
    public void offsetLeftAndRight(int offset) {
    if (offset != 0) {
    final boolean matrixIsIdentity = hasIdentityMatrix();
    if (matrixIsIdentity) {
    if (isHardwareAccelerated()) {
    invalidateViewProperty(false, false);
    } else {
    final ViewParent p = mParent;
    if (p != null && mAttachInfo != null) {
    final Rect r = mAttachInfo.mTmpInvalRect;
    int minLeft;
    int maxRight;
    if (offset < 0) {
    minLeft = mLeft + offset;
    maxRight = mRight;
    } else {
    minLeft = mLeft;
    maxRight = mRight + offset;
    }
    r.set(0, 0, maxRight - minLeft, mBottom - mTop);
    p.invalidateChild(this, r);
    }
    }
    } else {
    invalidateViewProperty(false, false);
    }

    mLeft += offset;
    mRight += offset;
    mRenderNode.offsetLeftAndRight(offset);
    if (isHardwareAccelerated()) {
    invalidateViewProperty(false, false);
    invalidateParentIfNeededAndWasQuickRejected();
    } else {
    if (!matrixIsIdentity) {
    invalidateViewProperty(false, true);
    }
    invalidateParentIfNeeded();
    }
    notifySubtreeAccessibilityStateChangedIfNeeded();
    }
    }

    这里RecyclerView的每个子View都是通过改变子 View 的 mLeft、mTop 等坐标,将初始位置值及偏移量传入,即需要滑动到的位置的坐标完成滑动。
    这里的 mLeft、mTop是指基于 父控件 视图坐标系 中的坐标

    Android 中有两种坐标系:

  • 一种是视图坐标系,以当前控件左上角为坐标原点,向右为 X 轴正方向,向下为 Y 轴正方向,MotionEvent 的 getX()、getY() 方法获取的是点击位置在视图坐标系中的坐标。
  • 另一种是 Android 坐标系,以屏幕左上角为坐标原点,向右为 X 轴正方向,向下为 Y 轴正方向,MotionEvent 的 getRawX()、getRawY() 方法获取的是点击位置在 Android 坐标系中的坐标
  • 小结

    小结一下,scorllBy 方法通知了onScrolled 回调,但是没有通知 onScrollStateChanged 回调

    额外补充一点RecyclerView的布局过程,看上面scrollBy 里调用的 fill 方法

    // LinearLayoutManager
    // fill填充方法, 返回的是填充ItemView需要的像素,以便拿去做滚动
    int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
    RecyclerView.State state, boolean stopOnFocusable) {
    // 填充起始位置
    final int start = layoutState.mAvailable;
    if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
    //如果有滚动就执行一次回收
    recycleByLayoutState(recycler, layoutState);
    }
    // 计算剩余可用的填充空间
    int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
    // 用于记录每一次while循环的填充结果
    LayoutChunkResult layoutChunkResult = mLayoutChunkResult;

    // ================== 核心while循环 ====================

    while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {

    // ====== 填充itemView核心填充方法 ====== 屏幕还有剩余可用空间并且还有数据就继续执行
    layoutChunk(recycler, state, layoutState, layoutChunkResult);
    // ...
    }
    // 填充完成后修改起始位置
    return start - layoutState.mAvailable;
    }

    这个方法是用来填充内容的,更多布局过程可以参考这篇文章: 《图文详解LinearLayoutManager填充、测量、布局过程》

    scrollToPosition

    再来看下 scrollToPosition,这个方法会使 RecyclerView 滚动最小的距离,以使目标位置可见,如果目标位置的view没有创建,则滑动不会发生

    // RecyclerView.java
    public void scrollToPosition(int position) {
    if (mLayoutSuppressed) {
    return;
    }
    stopScroll();
    if (mLayout == null) {
    Log.e(TAG, "Cannot scroll to position a LayoutManager set. "
    + "Call setLayoutManager with a non-null argument.");
    return;
    }
    mLayout.scrollToPosition(position); // 实际调用的是LayoutManager的对应方法
    awakenScrollBars();
    }

    这个方法调用了LayoutManager的同名方法:

    // LinearLayoutManager.java
    @Override
    public void scrollToPosition(int position) {
    // 用 mPendingScrollPosition 变量保存方法传入的位置
    mPendingScrollPosition = position;
    mPendingScrollPositionOffset = INVALID_OFFSET;
    if (mPendingSavedState != null) {
    mPendingSavedState.invalidateAnchor();
    }
    requestLayout(); // 请求刷新布局
    }

    requestLayout(),会重走onMeasure, onLayout过程,在 RecyclerView 的 dispatchLayoutStep1() 中 会调用onLayoutChildren()方法。
    同时 LinearLayoutManager 重写了onLayoutChildren方法

    // LinearLayoutManager.java
    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    // layout algorithm: 布局算法
    // 1) by checking children and other variables, find an anchor coordinate and an anchor item position.
    // 通过检查孩子和其他变量,找到锚坐标和锚点项目位置 mAnchor为布局锚点 理解为不具有的起点.
    // mAnchor包含了子控件在Y轴上起始绘制偏移量(coordinate),ItemView在Adapter中的索引位置(position)和布局方向(mLayoutFromEnd)
    // 2) fill towards start, stacking from bottom 开始填充, 从底部堆叠
    // 3) fill towards end, stacking from top 结束填充,从顶部堆叠
    // 4) scroll to fulfill requirements like stack from bottom. 滚动以满足堆栈从底部的要求

    // resolve layout direction 设置布局方向(VERTICAL/HORIZONTAL)
    resolveShouldLayoutReverse();

    // ...
    final View focused = getFocusedChild();
    if (!mAnchorInfo.mValid || mPendingScrollPosition != RecyclerView.NO_POSITION
    || mPendingSavedState != null) {
    // mPendingScrollPosition 值已更新,会进到这里
    mAnchorInfo.reset();
    // mStackFromEnd需要我们开发者主动调用,不然一直未false
    // VERTICAL方向为mLayoutFromEnd为false HORIZONTAL方向是为true
    mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
    // 计算更新保存绘制锚点信息
    updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
    mAnchorInfo.mValid = true;
    } else if (focused != null && (mOrientationHelper.getDecoratedStart(focused)
    >= mOrientationHelper.getEndAfterPadding()
    || mOrientationHelper.getDecoratedEnd(focused)
    <= mOrientationHelper.getStartAfterPadding())) {
    mAnchorInfo.assignFromViewAndKeepVisibleRect(focused, getPosition(focused));
    }

    // ... 如果 mPendingScrollPosition 有效,则会在 prelayout 阶段在用到这个值
    if (state.isPreLayout() && mPendingScrollPosition != RecyclerView.NO_POSITION
    && mPendingScrollPositionOffset != INVALID_OFFSET) {
    final View existing = findViewByPosition(mPendingScrollPosition);
    if (existing != null) {
    final int current;
    final int upcomingOffset;
    if (mShouldReverseLayout) {
    current = mOrientationHelper.getEndAfterPadding()
    - mOrientationHelper.getDecoratedEnd(existing);
    upcomingOffset = current - mPendingScrollPositionOffset;
    } else {
    current = mOrientationHelper.getDecoratedStart(existing)
    - mOrientationHelper.getStartAfterPadding();
    upcomingOffset = mPendingScrollPositionOffset - current;
    }
    if (upcomingOffset > 0) {
    extraForStart += upcomingOffset;
    } else {
    extraForEnd -= upcomingOffset;
    }
    }
    }

    // ...省略根据布局方向调用 fill 方法填充
    }

    // 更新 anchorInfo
    private boolean updateAnchorFromPendingData(RecyclerView.State state, AnchorInfo anchorInfo) {
    //...
    // 简单理解,如果位置大于itemCount,不会更新 anchorInfo,并且清除 mPendingScrollPosition 的值
    if (mPendingScrollPosition < 0 || mPendingScrollPosition >= state.getItemCount()) {
    mPendingScrollPosition = RecyclerView.NO_POSITION;
    mPendingScrollPositionOffset = INVALID_OFFSET;
    if (DEBUG) {
    Log.e(TAG, "ignoring invalid scroll position " + mPendingScrollPosition);
    }
    return false;
    }
    // ...
    }

    小结

    上面代码可以看出,如果scrollToPosition(position) 的参数 position 所在位置还没有view被layout,则滑动不会被处理。

    同时也可以看出 scrollToPosition 是 LayoutManager 通过 requestLayout 方式来刷新 ReycclerView,所以并不会通知 onScrollStateChanged

    这里额外介绍一下根据 mAnchor 和布局方向填充view的逻辑

    圆形红点就是我们布局算法在第一步updateAnchorInfoForLayout方法中计算出来的填充锚点位置。

  • 第一种情况是屏幕显示的位置在RecyclerView的最底部,那么就只有一种填充方向为formEnd
  • 第二种情况是屏幕显示的位置在RecyclerView的顶部,那么也只有一种填充方向为formStart
  • 第三种情况应该是最常见的,屏幕显示的位置在RecyclerView的中间,那么填充方向就有formEnd和formStart两种情况,这就是 fill 方法调用两次的原因。
  • 上面是RecyclerView的方向为VERTICAL的情况,当为HORIZONTAL方向的时候填充算法是不变的。

    smoothScrollBy

    smoothScrollBy有很多重载方法,直接看最终的:

    // RecyclerView.java
    void smoothScrollBy(@Px int dx, @Px int dy, @Nullable Interpolator interpolator,
    int duration, boolean withNestedScrolling) {
    // ... 前面都是是否能滑动判断
    if (dx != 0 || dy != 0) {
    boolean durationSuggestsAnimation = duration == UNDEFINED_DURATION || duration > 0;
    if (durationSuggestsAnimation) {
    if (withNestedScrolling) {
    int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
    if (dx != 0) {
    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
    }
    if (dy != 0) {
    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
    }
    startNestedScroll(nestedScrollAxis, TYPE_NON_TOUCH);
    }
    mViewFlinger.smoothScrollBy(dx, dy, duration, interpolator);
    } else {
    scrollBy(dx, dy);
    }
    }
    }

    最终调用了mViewFlinger.smoothScrollBy 方法:

    // ViewFlinger.smoothScrollBy
    public void smoothScrollBy(int dx, int dy, int duration,
    @Nullable Interpolator interpolator) {

    // Handle cases where parameter values aren't defined.
    if (duration == UNDEFINED_DURATION) {
    duration = computeScrollDuration(dx, dy, 0, 0);
    }
    if (interpolator == null) {
    interpolator = sQuinticInterpolator;
    }

    // If the Interpolator has changed, create a new OverScroller with the new
    // interpolator.
    if (mInterpolator != interpolator) {
    mInterpolator = interpolator;
    mOverScroller = new OverScroller(getContext(), interpolator);
    }

    // Reset the last fling information.
    mLastFlingX = mLastFlingY = 0;

    // 调用RecyclerView的setScrollState方法,通知滑动事件开始
    setScrollState(SCROLL_STATE_SETTLING);
    mOverScroller.startScroll(0, 0, dx, dy, duration);

    if (Build.VERSION.SDK_INT < 23) {
    mOverScroller.computeScrollOffset();
    }

    postOnAnimation();
    }

    小结

    smoothScrollBy 调用了 RecyclerView.setScrollState方法,最终会通知 onScrollStateChanged 的监听

    smoothScrollToPosition

    再来看下 smoothScrollToPosition

    // RecyclerView.java
    public void smoothScrollToPosition(int position) {
    if (mLayoutSuppressed) {
    return;
    }
    if (mLayout == null) {
    Log.e(TAG, "Cannot smooth scroll without a LayoutManager set. "
    + "Call setLayoutManager with a non-null argument.");
    return;
    }
    mLayout.smoothScrollToPosition(this, mState, position);
    }

    上面直接调用了LayoutManager的同名方法

    // LinearLayoutManager.java
    @Override
    public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
    int position) {
    LinearSmoothScroller linearSmoothScroller =
    new LinearSmoothScroller(recyclerView.getContext());
    linearSmoothScroller.setTargetPosition(position);
    startSmoothScroll(linearSmoothScroller);
    }

    可以看到每次调用方法,都会创建一个LinearSmoothScroller,LinearSmoothScroller的父类是RecyclerView.SmoothScroller

    最终调用的是 mRecyclerView.mViewFlinger.postOnAnimation();
    ViewFlinger其实一个Runnable,在postOnAnimation()内部又将该Runnable发送出去了
    那么我们在看下ViewFlinger的run()方法就行了。

    // RecyclerView.ViewFlinger
    @Override
    public void run() {
    // ...
    final OverScroller scroller = mScroller;
    //获得layoutManger中的SmoothScroller
    final SmoothScroller smoothScroller = mLayout.mSmoothScroller;
    if (scroller.computeScrollOffset()) {//如果是第一次走,会返回false
    // ...省略部分代码
    if (mAdapter != null) {
    mReusableIntPair[0] = 0;
    mReusableIntPair[1] = 0;
    scrollStep(unconsumedX, unconsumedY, mReusableIntPair);
    consumedX = mReusableIntPair[0];
    consumedY = mReusableIntPair[1];
    // ...
    }

    if (consumedX != 0 || consumedY != 0) {
    dispatchOnScrolled(consumedX, consumedY);
    }
    }
    if (smoothScroller != null) {
    if (smoothScroller.isPendingInitialRun()) {
    smoothScroller.onAnimation(0, 0);
    }
    if (!mReSchedulePostAnimationCallback) {
    smoothScroller.stop(); //stop if it does not trigger any scroll
    }
    }
    // ...
    }

    小结

    可以看出在ViewFlinger 的 run()方法中,调用了dispatchOnScrolled(consumedX, consumedY),通知了onScrolled()

    真正产生滑动距离consumedX、consumedY 的方法是 scrollStep() 。这个方法前面分析 scrollBy()方法的时候已经分析过了,最终会调用到每个view的offsetLeftAndRight()方法。

    补充滚到到一个屏幕外的位置

    scrollToPosition 和 smoothScrollToPosition 只能保证指定位置的item滑动到屏幕可见,如果指定的item本来就已在屏幕可见范围,则不会滑动,并且屏幕外的item滑到可见范围后,还需手动置顶,手动置顶可以调用 LinearLayoutManager.scrollToPositionWithOffset(position, 0)

    recyclerView.scrollToPosition(pos);
    linearLayoutManager.scrollToPositionWithOffset(pos, 0)

    还可以使用另一种方式:
    可以继承 LinearSmoothScroller,重写getVerticalSnapPreference()或getHorizontalSnapPreference()getVerticalSnapPreference()

    // LinearSmoothScroller
    protected int getHorizontalSnapPreference() {
    return mTargetVector == null || mTargetVector.x == 0 ? SNAP_TO_ANY :
    mTargetVector.x > 0 ? SNAP_TO_END : SNAP_TO_START;
    }

    @Override
    protected int getVerticalSnapPreference() {
    // 子view与RecyclerView垂直方向顶部对齐
    return SNAP_TO_START;
    }

    之后这样调用就可以:

    final TopSmoothScroller mTopScroller = new TopSmoothScroller(this);
    mTopScroller.setTargetPosition(position);
    mRecyclerView.getLayoutManager.startSmoothScroll(mTopScroller);

    补充 scrollToPositionWithOffset

    这个方式是LinearLayoutManager才有的方法,RecyclerView没有。
    RecyclerView可以调用的是LinearLayoutManger.scrollToPosition(position)

    这两个方法区别是区别是RecyclerView.scrollToPosition(position) 的 mPendingScrollPositionOffset 值为 INVALID_OFFSET = Integer.MIN_VALUE

    public void scrollToPositionWithOffset(int position, int offset) {
    mPendingScrollPosition = position;
    mPendingScrollPositionOffset = offset;
    if (mPendingSavedState != null) {
    mPendingSavedState.invalidateAnchor();
    }
    requestLayout();
    }

    前面也提到过了,这个方法第二个参数传0,可以让 position 位置对应的view置顶

    同样这个方法是 LayoutManager 通过 requestLayout 方式来刷新 ReycclerView,所以并不会通知 onScrollStateChanged。

    手势滑动

    手势处理肯定是在RecyclerView的onTouchEvent方法中了:

    @Override
    public boolean onTouchEvent(MotionEvent e) {
    switch (action) {
    case MotionEvent.ACTION_MOVE: {
    // ...判断手势索引,避免多指引起冲突
    final int index = e.findPointerIndex(mScrollPointerId);

    final int x = (int) (e.getX(index) + 0.5f);
    final int y = (int) (e.getY(index) + 0.5f);
    int dx = mLastTouchX - x;
    int dy = mLastTouchY - y;

    if (mScrollState != SCROLL_STATE_DRAGGING) {
    boolean startScroll = false;
    if (canScrollHorizontally) {
    if (dx > 0) {
    dx = Math.max(0, dx - mTouchSlop);
    } else {
    dx = Math.min(0, dx + mTouchSlop);
    }
    if (dx != 0) {
    startScroll = true;
    }
    }
    if (canScrollVertically) {
    if (dy > 0) {
    dy = Math.max(0, dy - mTouchSlop);
    } else {
    dy = Math.min(0, dy + mTouchSlop);
    }
    if (dy != 0) {
    startScroll = true;
    }
    }
    if (startScroll) {
    // 可以滑动。设置状态
    setScrollState(SCROLL_STATE_DRAGGING);
    }
    }

    if (mScrollState == SCROLL_STATE_DRAGGING) {
    mReusableIntPair[0] = 0;
    mReusableIntPair[1] = 0;
    if (dispatchNestedPreScroll( // 处理 nested 联动
    canScrollHorizontally ? dx : 0,
    canScrollVertically ? dy : 0,
    mReusableIntPair, mScrollOffset, TYPE_TOUCH
    )) {
    dx -= mReusableIntPair[0];
    dy -= mReusableIntPair[1];
    // Updated the nested offsets
    mNestedOffsets[0] += mScrollOffset[0];
    mNestedOffsets[1] += mScrollOffset[1];
    // Scroll has initiated, prevent parents from intercepting
    getParent().requestDisallowInterceptTouchEvent(true);
    }

    mLastTouchX = x - mScrollOffset[0];
    mLastTouchY = y - mScrollOffset[1];

    if (scrollByInternal( // 调用 scrollByInternal() 让每个view自己滑动
    canScrollHorizontally ? dx : 0,
    canScrollVertically ? dy : 0,
    e)) {
    getParent().requestDisallowInterceptTouchEvent(true);
    }
    }
    } break;

    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;
    // 跟踪滑动速度,是否采取 fling 操作
    if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
    setScrollState(SCROLL_STATE_IDLE);
    }
    resetScroll();
    } break;
    }
    }

    小结

    这里分两种情况:

  • 没有触发fling操作,直接调用scrollByInternal()方法,最终调用View的offsetLeftAndRight(offset)方法。
  • 触发fling操作,由mViewFlinger.fling(velocityX, velocityY)处理,最终在 mViewFlinger的run()方法中,调用View的offsetLeftAndRight(offset)方法。
  • 总结

    看到这里不知道大家有没有被绕晕,其实不管方法是哪个,都是调用每个子View的 offsetLeftAndRight(offset) 来实现列表的滑动

    手势滑动两者都会触发,只不过因为放不下了,没有在表格里展示 :)

    REF

    图文详解LinearLayoutManager填充、测量、布局过程: https://www.jianshu.com/p/e9752f8890c8

    View 的滑动原理和实现方式 : https://www.jianshu.com/p/a177869b0382

    用SmoothScroller实现RecyclerView滚动到指定位置并置顶: https://blog.csdn.net/weixin_39428125/article/details/89032646


    本文采用 知识共享署名 2.5 中国大陆许可协议 进行许可,欢迎转载,但转载请注明来自 Agehua’s Blog ,并保持转载后文章内容的完整。本人保留所有版权相关权利。

    本文链接: http://agehua.github.io/2020/12/04/RecyclerView_scroll_relatedapi/

    tag cloud

    AAC AES AGP AIDL AMS ANDROID ANR AOP AOSP ASM Alfred Workflow Android APP Bundles Android Framework Android Source Code Android system service Annotation AsyncInflate AsyncTask AudioRecord Basic Knowledge Binder CMD CoordinatorLayout EIT EJS Electron FrameLayout GC GCM HOOK HTTPS Handler HandlerThread Hexo Hexo structure IM Company IntentService Interview JNI JVM Java JavaScript Kotlin LeakCanary LocationListener Looper Measure Memory leaks MessageQueue Object-oriented Open souces PBOC PBOC 2.0 Patch Update Profiler Python RecyclerView RecyclerView optimization RelativeLayout RxAndroid RxJava Singleton TCP ThreadLocal TimerTask Transform android source code assertion azure bits operation byte[] download exitApplication genericity google map gradle gson handler iterm linux multi-process multi-thread net request new features nodejs notification picture-in-picture publickey qiniu read reading remove admob scp sdk compile self-introduction svn server third-party signin toastmaster view scroll webview

    archives