RecyclerView横向滚动居中对齐

111

我正在尝试使用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();
                        }
                    }

现在从右向左的滑动已经正常工作,但是反过来就不行了,我错过了什么吗?

7个回答

191

有了 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);

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

前者是Support Library版本24.2.0中包含的第一方解决方案。 这意味着您需要将其添加到应用程序模块的build.gradle或进行更新。

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

这对我没有起作用(但 @Albert Vila Calvo 的解决方案起了作用)。您实施了吗?我们应该覆盖其中的方法吗?我认为该类应该开箱即用,完成所有工作。 - galaxigirl
是的,因此答案是肯定的。不过我没有进行彻底的测试。需要明确的是,我是使用水平的LinearLayoutManager来完成的,其中所有视图的大小都是常规的。除了上面的代码片段之外,不需要其他任何东西。 - razzledazzle
@galaxigirl 抱歉,我已经理解了你的意思,并进行了一些编辑以承认这一点,请务必评论。这是使用 LinearSnapHelper 的另一种解决方案。 - razzledazzle
这对我来说正好适用。@galaxigirl 覆盖此方法有助于在滚动稳定时,线性捕捉助手否则会捕捉到最近的项目,而不仅仅是下一个/上一个项目。非常感谢你,razzledazzle。 - AA_PV
LinearSnapHelper 是最初对我有用的解决方案,但似乎 PagerSnapHelper 给滑动带来了更好的动画效果。 - Leonardo Sibela

106

Google I/O 2019 更新

ViewPager2 来了!

在“Android 有什么新东西”(又称“Android 主题演讲”)中,Google 刚刚宣布 他们正在开发一种基于 RecyclerView 的新 ViewPager!

来自幻灯片:

像 ViewPager 一样,但更好

  • 易于从 ViewPager 迁移
  • 基于 RecyclerView
  • 支持从右到左模式
  • 允许垂直分页
  • 改进的数据集更改通知
你可以在这里检查最新版本和此处的发布说明。还有一个官方示例2021年12月更新:示例已移至另一个存储库

个人观点:我认为这是一个非常必要的补充。最近,我在使用PagerSnapHelper时遇到了很多问题(无限左右摆动)-请参见我打开的工单


新答案(2016年)

现在您可以使用SnapHelper

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

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

还有一个LinearSnapHelper。我试过了,如果你用力甩动,它会一次滚动2个项目。个人认为不太喜欢,但是你可以自己决定——只需要几秒钟就能试一下。


原始答案(2016年)

在 Stack Overflow 上尝试了三种不同的解决方案后,经过许多小时的努力,我终于构建出了一个非常接近 ViewPager 行为的解决方案。

该解决方案基于 @eDizzle 的 solution,我认为我已经对其进行了足够的改进,可以说它几乎像一个 ViewPager

重要提示:我的 RecyclerView 项的宽度与屏幕完全相同。我没有尝试其他大小。此外,我使用它与水平的 LinearLayoutManager。如果您想要垂直滚动,我认为您需要调整代码。

这里是代码:

public class SnappyRecyclerView extends RecyclerView {

    // Use it with a horizontal LinearLayoutManager
    // Based on https://dev59.com/dl4b5IYBdhLWcg3wchNL#29171652

    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,则会出现一个小错误。谢谢! - Mauricio Sartori
这在较低的API中如何工作?由于RecyclerView需要与android.support.v7.widget.RecyclerView一起使用才能适应较低的API,而这种方法要求您使用自己的xml标记。 - Gary Holiday
1
@galaxigirl,不,我还没有尝试过LinearSnapHelper。我刚刚更新了答案,以便让大家知道官方解决方案。我的解决方案在我的应用程序上没有任何问题。 - Albert Vila Calvo
1
@AlbertVilaCalvo 我尝试了LinearSnapHelper。LinearSnapHelper与Scroller一起使用。它根据重力和甩动速度滚动和捕捉。速度越快,滚动的页面就越多。我不建议将其用于类似ViewPager的行为。 - mipreamble
电话旋转时UI错位。 - Desmond Lua
显示剩余11条评论

52

如果目标是使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的行为。


4
我正在使用LinearSnapHelper而不是PagerSnapHelper,这对我有效。 - fanjavaid
1
是的,在LinearSnapHelper中没有快照效果。 - Boonya Kitpitak
1
这会将项目固定在视图上并启用滚动。 - Samir
@BoonyaKitpitak,我尝试了LinearSnapHelper,它可以将视图对齐到中间项。但是PagerSnapHelper会阻碍轻松滚动(至少在图像列表中)。 - CoolMind
@BoonyaKitpitak,你能告诉我如何实现这个效果吗?就是让中间的项目完全可见,而侧边的项目只显示一半。 - Rucha Bhatt Joshi

31

你需要使用findFirstVisibleItemPosition方法来反向遍历。而要检测滑动的方向,你需要获取fling velocity或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;
}

屏幕宽度在哪里定义? - ltsai
@Itsai:糟糕!好问题。这是我在类中其他地方设置的变量。它是设备屏幕宽度(以像素为单位),而不是密度像素,因为smoothScrollBy方法需要您想要滚动的像素数。 - eDizzle
3
你可以改用getWidth() (recyclerview.getWidth()),而不是screenWidth。当RecyclerView比屏幕宽度小时,screenWidth无法正确工作。 - VAdaihiep
我尝试扩展RecyclerView,但是出现了“错误膨胀类自定义RecyclerView”的问题。你能帮忙吗?谢谢。 - user1410081
@bEtTyBarnes:你可能想在另一个问题反馈中搜索这个。它与此处的初始问题相当无关。 - eDizzle
@bEtTyBarnes:你必须创建与超类匹配的所有三个构造函数。 - Sajal

8
只需在recyclerViewrecyclerView项中添加paddingmargin即可:

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

结果:

在此输入图像描述


这是一个图片链接,无法提供更多信息。

有没有办法在项目之间实现可变填充?比如,如果第一张和最后一张卡片不需要居中显示以展示更多的下一个/上一个卡片,而中间的卡片需要居中显示呢? - RamPrasadBismil

2

我的解决方案:

/**
 * 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,并设置所需的偏移量以使项目居中。我的所有项目都具有相同的宽度,因此恒定的偏移量是可以的。

0

PagerSnapHelper 无法与 GridLayoutManager 在 spanCount > 1 的情况下正常工作,因此在这种情况下我的解决方案是:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接