【问题标题】:RecyclerView horizontal scroll snap in centerRecyclerView 水平滚动对齐在中心
【发布时间】:2015-05-21 22:44:47
【问题描述】:

我正在尝试使用 RecyclerView 在此处制作类似轮播的视图,我希望该项目在滚动时捕捉到屏幕中间,一次一个项目。我试过使用recyclerView.setScrollingTouchSlop(RecyclerView.TOUCH_SLOP_PAGING);

但视图仍在平滑滚动,我也尝试使用滚动侦听器实现自己的逻辑,如下所示:

recyclerView.setOnScrollListener(new OnScrollListener() {
                @Override
                public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                    super.onScrollStateChanged(recyclerView, newState);
                    Log.v("Offset ", recyclerView.getWidth() + "");
                    if (newState == 0) {
                        try {
                               recyclerView.smoothScrollToPosition(layoutManager.findLastVisibleItemPosition());
                                recyclerView.scrollBy(20,0);
                            if (layoutManager.findLastVisibleItemPosition() >= recyclerView.getAdapter().getItemCount() - 1) {
                                Beam refresh = new Beam();
                                refresh.execute(createUrl());
                            }
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }

现在从右向左滑动可以正常工作,但反过来不行,我在这里缺少什么?

【问题讨论】:

    标签: android horizontal-scrolling android-recyclerview


    【解决方案1】:

    有了LinearSnapHelper,现在这很容易。

    您需要做的就是:

    SnapHelper helper = new LinearSnapHelper();
    helper.attachToRecyclerView(recyclerView);
    

    更新

    自 25.1.0 起可用,PagerSnapHelper 可以帮助实现ViewPager 类似的效果。像使用 LinearSnapHelper 一样使用它。

    旧的解决方法:

    如果您希望它的行为类似于 ViewPager,请尝试以下操作:

    LinearSnapHelper snapHelper = new LinearSnapHelper() {
        @Override
        public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, int velocityY) {
            View centerView = findSnapView(layoutManager);
            if (centerView == null) 
                return RecyclerView.NO_POSITION;
    
            int position = layoutManager.getPosition(centerView);
            int targetPosition = -1;
            if (layoutManager.canScrollHorizontally()) {
                if (velocityX < 0) {
                    targetPosition = position - 1;
                } else {
                    targetPosition = position + 1;
                }
            }
    
            if (layoutManager.canScrollVertically()) {
                if (velocityY < 0) {
                    targetPosition = position - 1;
                } else {
                    targetPosition = position + 1;
                }
            }
    
            final int firstItem = 0;
            final int lastItem = layoutManager.getItemCount() - 1;
            targetPosition = Math.min(lastItem, Math.max(targetPosition, firstItem));
            return targetPosition;
        }
    };
    snapHelper.attachToRecyclerView(recyclerView);
    

    上面的实现只是根据速度的方向返回当前项目旁边的位置(居中),而不考虑大小。

    前一个是包含在支持库版本 24.2.0 中的第一方解决方案。 这意味着您必须将其添加到您的应用模块的build.gradle 或更新它。

    compile "com.android.support:recyclerview-v7:24.2.0"
    

    【讨论】:

    • 这对我不起作用(但@Albert Vila Calvo 的解决方案确实如此)。你实施了吗?我们应该覆盖它的方法吗?我认为班级应该开箱即用地完成所有工作
    • 是的,因此得到了答案。然而,我还没有彻底测试过。需要明确的是,我使用水平的LinearLayoutManager 进行了此操作,其中所有视图的大小都是正常的。除了上面的 sn-p 之外什么都不需要。
    • @galaxigirl 道歉,我已经理解您的意思,并进行了一些编辑确认,请发表评论。这是LinearSnapHelper 的替代解决方案。
    • 这对我来说很合适。 @galaxigirl 覆盖此方法有助于线性捕捉帮助器在滚动稳定时捕捉到最近的项目,而不仅仅是下一个/上一个项目。非常感谢这个,炫目
    • LinearSnapHelper 是最初对我有用的解决方案,但 PagerSnapHelper 似乎为滑动提供了更好的动画效果。
    【解决方案2】:

    Google I/O 2019 更新

    ViewPager2 来了!

    Google just announced 在“What's New in Android”(又名“Android 主题演讲”)演讲中表示,他们正在开发基于 RecyclerView 的新 ViewPager!

    来自幻灯片:

    类似于 ViewPager,但更好

    • 从 ViewPager 轻松迁移
    • 基于 RecyclerView
    • 支持从右到左的模式
    • 允许垂直分页
    • 改进的数据集更改通知

    您可以查看最新版本here 和发行说明here。还有一个official sample2021 年 12 月更新:示例已移至 this other repo

    个人意见:我认为这是一个非常需要的补充。我最近在PagerSnapHelper oscillating left right indefinitely 遇到了很多麻烦 - 请参阅the ticket 我已经打开了。


    新答案(2016 年)

    您现在可以只使用SnapHelper

    如果您想要类似于ViewPager 的中心对齐捕捉行为,请使用PagerSnapHelper

    SnapHelper snapHelper = new PagerSnapHelper();
    snapHelper.attachToRecyclerView(recyclerView);
    

    还有一个LinearSnapHelper。我试过了,如果你用能量一扔,那么它会用 1 次扔滚动 2 个项目。我个人不喜欢它,但请自行决定 - 尝试只需几秒钟。


    原始答案(2016 年)

    经过许多小时的尝试,在 SO 中找到了 3 种不同的解决方案,我终于构建了一个非常接近 ViewPager 中的行为的解决方案。

    该解决方案基于@eDizzle solution,我相信我已经对其进行了足够的改进,可以说它几乎可以像ViewPager 一样工作。

    重要提示:我的RecyclerView 项目宽度与屏幕完全相同。我没有尝试过其他尺寸。我也将它与水平 LinearLayoutManager 一起使用。如果你想要垂直滚动,我认为你需要调整代码。

    这里有代码:

    public class SnappyRecyclerView extends RecyclerView {
    
        // Use it with a horizontal LinearLayoutManager
        // Based on https://*.com/a/29171652/4034572
    
        public SnappyRecyclerView(Context context) {
            super(context);
        }
    
        public SnappyRecyclerView(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
        }
    
        public SnappyRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
            super(context, attrs, defStyle);
        }
    
        @Override
        public boolean fling(int velocityX, int velocityY) {
    
            LinearLayoutManager linearLayoutManager = (LinearLayoutManager) getLayoutManager();
    
            int screenWidth = Resources.getSystem().getDisplayMetrics().widthPixels;
    
            // views on the screen
            int lastVisibleItemPosition = linearLayoutManager.findLastVisibleItemPosition();
            View lastView = linearLayoutManager.findViewByPosition(lastVisibleItemPosition);
            int firstVisibleItemPosition = linearLayoutManager.findFirstVisibleItemPosition();
            View firstView = linearLayoutManager.findViewByPosition(firstVisibleItemPosition);
    
            // distance we need to scroll
            int leftMargin = (screenWidth - lastView.getWidth()) / 2;
            int rightMargin = (screenWidth - firstView.getWidth()) / 2 + firstView.getWidth();
            int leftEdge = lastView.getLeft();
            int rightEdge = firstView.getRight();
            int scrollDistanceLeft = leftEdge - leftMargin;
            int scrollDistanceRight = rightMargin - rightEdge;
    
            if (Math.abs(velocityX) < 1000) {
                // The fling is slow -> stay at the current page if we are less than half through,
                // or go to the next page if more than half through
    
                if (leftEdge > screenWidth / 2) {
                    // go to next page
                    smoothScrollBy(-scrollDistanceRight, 0);
                } else if (rightEdge < screenWidth / 2) {
                    // go to next page
                    smoothScrollBy(scrollDistanceLeft, 0);
                } else {
                    // stay at current page
                    if (velocityX > 0) {
                        smoothScrollBy(-scrollDistanceRight, 0);
                    } else {
                        smoothScrollBy(scrollDistanceLeft, 0);
                    }
                }
                return true;
    
            } else {
                // The fling is fast -> go to next page
    
                if (velocityX > 0) {
                    smoothScrollBy(scrollDistanceLeft, 0);
                } else {
                    smoothScrollBy(-scrollDistanceRight, 0);
                }
                return true;
    
            }
    
        }
    
        @Override
        public void onScrollStateChanged(int state) {
            super.onScrollStateChanged(state);
    
            // If you tap on the phone while the RecyclerView is scrolling it will stop in the middle.
            // This code fixes this. This code is not strictly necessary but it improves the behaviour.
    
            if (state == SCROLL_STATE_IDLE) {
                LinearLayoutManager linearLayoutManager = (LinearLayoutManager) getLayoutManager();
    
                int screenWidth = Resources.getSystem().getDisplayMetrics().widthPixels;
    
                // views on the screen
                int lastVisibleItemPosition = linearLayoutManager.findLastVisibleItemPosition();
                View lastView = linearLayoutManager.findViewByPosition(lastVisibleItemPosition);
                int firstVisibleItemPosition = linearLayoutManager.findFirstVisibleItemPosition();
                View firstView = linearLayoutManager.findViewByPosition(firstVisibleItemPosition);
    
                // distance we need to scroll
                int leftMargin = (screenWidth - lastView.getWidth()) / 2;
                int rightMargin = (screenWidth - firstView.getWidth()) / 2 + firstView.getWidth();
                int leftEdge = lastView.getLeft();
                int rightEdge = firstView.getRight();
                int scrollDistanceLeft = leftEdge - leftMargin;
                int scrollDistanceRight = rightMargin - rightEdge;
    
                if (leftEdge > screenWidth / 2) {
                    smoothScrollBy(-scrollDistanceRight, 0);
                } else if (rightEdge < screenWidth / 2) {
                    smoothScrollBy(scrollDistanceLeft, 0);
                }
            }
        }
    
    }
    

    享受吧!

    【讨论】:

    • 很好地抓住了 onScrollStateChanged();否则,如果我们慢慢滑动 RecyclerView,就会出现一个小错误。谢谢!
    • 为了支持边距 (android:layout_marginStart="16dp") 我尝试使用 int screenwidth = getWidth();它似乎有效。
    • AWEOOOMMEEEEEEEEE
    • @galaxigirl 不,我还没有尝试过 LinearSnapHelper。我刚刚更新了答案,以便人们了解官方解决方案。我的解决方案在我的应用上没有任何问题。
    • @AlbertVilaCalvo 我尝试了 LinearSnapHelper。 LinearSnapHelper 与 Scroller 一起使用。它根据重力和抛掷速度滚动和捕捉。速度越高,滚动的页面就越多。对于类似 ViewPager 的行为,我不会推荐它。
    【解决方案3】:

    如果目标是让RecyclerView 模仿ViewPager 的行为,那么方法很简单

    RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.recycler_view);
    
    LinearLayoutManager layoutManager = new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false);
    SnapHelper snapHelper = new PagerSnapHelper();
    recyclerView.setLayoutManager(layoutManager);
    snapHelper.attachToRecyclerView(mRecyclerView);
    

    通过使用PagerSnapHelper,您可以获得类似ViewPager的行为

    【讨论】:

    • 我使用的是LinearSnapHelper 而不是PagerSnapHelper ,它对我有用
    • 是的,LinearSnapHelper没有快照效果
    • 这会将项目粘贴在视图上启用可滚动
    • @BoonyaKitpitak,我试过LinearSnapHelper,它会捕捉到一个中间项目。 PagerSnapHelper 阻碍轻松滚动(至少是图像列表)。
    • @BoonyaKitpitak 你能告诉我怎么做吗,一个中心的项目完全可见而侧面项目是半可见的?
    【解决方案4】:

    您需要使用 findFirstVisibleItemPosition 来反方向前进。为了检测滑动的方向,您需要获得抛掷速度或 x 的变化。我从与您略有不同的角度解决了这个问题。

    创建一个扩展 RecyclerView 类的新类,然后像这样重写 RecyclerView 的 fling 方法:

    @Override
    public boolean fling(int velocityX, int velocityY) {
        LinearLayoutManager linearLayoutManager = (LinearLayoutManager) getLayoutManager();
    
    //these four variables identify the views you see on screen.
        int lastVisibleView = linearLayoutManager.findLastVisibleItemPosition();
        int firstVisibleView = linearLayoutManager.findFirstVisibleItemPosition();
        View firstView = linearLayoutManager.findViewByPosition(firstVisibleView);
        View lastView = linearLayoutManager.findViewByPosition(lastVisibleView);
    
    //these variables get the distance you need to scroll in order to center your views.
    //my views have variable sizes, so I need to calculate side margins separately.     
    //note the subtle difference in how right and left margins are calculated, as well as
    //the resulting scroll distances.
        int leftMargin = (screenWidth - lastView.getWidth()) / 2;
        int rightMargin = (screenWidth - firstView.getWidth()) / 2 + firstView.getWidth();
        int leftEdge = lastView.getLeft();
        int rightEdge = firstView.getRight();
        int scrollDistanceLeft = leftEdge - leftMargin;
        int scrollDistanceRight = rightMargin - rightEdge;
    
    //if(user swipes to the left) 
        if(velocityX > 0) smoothScrollBy(scrollDistanceLeft, 0);
        else smoothScrollBy(-scrollDistanceRight, 0);
    
        return true;
    }
    

    【讨论】:

    • screenWidth 是在哪里定义的?
    • @Itsai:糟糕!好问题。这是我在课堂上其他地方设置的变量。它是设备的屏幕宽度,以像素为单位,而不是像素密度,因为 smoothScrollBy 方法需要您希望它滚动的像素数。
    • @ltsai 您可以更改为 getWidth() (recyclerview.getWidth()) 而不是 screenWidth。当 RecyclerView 小于屏幕宽度时 screenWidth 不能正常工作。
    • 我试图扩展 RecyclerView 但我得到“错误膨胀类自定义 RecyclerView”。你能帮我吗?谢谢
    • @bEtTyBarnes :您可能想在另一个问题提要中搜索它。这里最初的问题超出了范围。
    【解决方案5】:

    只需将paddingmargin 添加到recyclerViewrecyclerView item

    recyclerView 项目:

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/parentLayout"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_marginLeft="8dp" <!-- here -->
        android:layout_marginRight="8dp" <!-- here  -->
        android:layout_width="match_parent"
        android:layout_height="200dp">
    
       <!-- child views -->
    
    </RelativeLayout>
    

    recyclerView:

    <androidx.recyclerview.widget.RecyclerView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingLeft="8dp" <!-- here -->
        android:paddingRight="8dp" <!-- here -->
        android:clipToPadding="false" <!-- important!-->
        android:scrollbars="none" />
    

    并设置PagerSnapHelper

    int displayWidth = Resources.getSystem().getDisplayMetrics().widthPixels;
    parentLayout.getLayoutParams().width = displayWidth - Utils.dpToPx(16) * 4;
    SnapHelper snapHelper = new PagerSnapHelper();
    snapHelper.attachToRecyclerView(recyclerView);
    

    dp 到 px:

    public static int dpToPx(int dp) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, Resources.getSystem().getDisplayMetrics());
    }
    

    结果:

    【讨论】:

    • 有没有办法实现项目之间的可变填充?比如第一张和最后一张卡片不需要居中以显示更多的下一张/上一张卡片而不是中间卡片居中?
    【解决方案6】:

    我的解决方案:

    /**
     * Horizontal linear layout manager whose smoothScrollToPosition() centers
     * on the target item
     */
    class ItemLayoutManager extends LinearLayoutManager {
    
        private int centeredItemOffset;
    
        public ItemLayoutManager(Context context) {
            super(context, LinearLayoutManager.HORIZONTAL, false);
        }
    
        @Override
        public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
            LinearSmoothScroller linearSmoothScroller = new Scroller(recyclerView.getContext());
            linearSmoothScroller.setTargetPosition(position);
            startSmoothScroll(linearSmoothScroller);
        }
    
        public void setCenteredItemOffset(int centeredItemOffset) {
            this.centeredItemOffset = centeredItemOffset;
        }
    
        /**
         * ********** Inner Classes **********
         */
    
        private class Scroller extends LinearSmoothScroller {
    
            public Scroller(Context context) {
                super(context);
            }
    
            @Override
            public PointF computeScrollVectorForPosition(int targetPosition) {
                return ItemLayoutManager.this.computeScrollVectorForPosition(targetPosition);
            }
    
            @Override
            public int calculateDxToMakeVisible(View view, int snapPreference) {
                return super.calculateDxToMakeVisible(view, SNAP_TO_START) + centeredItemOffset;
            }
        }
    }
    

    我将此布局管理器传递给 RecycledView 并设置居中项目所需的偏移量。我所有的项目都有相同的宽度,所以恒定的偏移量是可以的

    【讨论】:

      【解决方案7】:

      PagerSnapHelper 不适用于 spanCount > 1 的GridLayoutManager,所以我在这种情况下的解决方案是:

      class GridPagerSnapHelper : PagerSnapHelper() {
          override fun findTargetSnapPosition(layoutManager: RecyclerView.LayoutManager?, velocityX: Int, velocityY: Int): Int {
              val forwardDirection = if (layoutManager?.canScrollHorizontally() == true) {
                  velocityX > 0
              } else {
                  velocityY > 0
              }
              val centerPosition = super.findTargetSnapPosition(layoutManager, velocityX, velocityY)
              return centerPosition +
                  if (forwardDirection) (layoutManager as GridLayoutManager).spanCount - 1 else 0
          }
      }
      

      【讨论】: