RecyclerView水平滚动对齐中心


89

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

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

Answers:


161

使用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进行了此操作,其中所有视图的大小都是规则的。除了上面的摘录,仅此而已。
2016年

@galaxigirl道歉,我了解您的意思,并进行了一些确认,请发表评论。这是的替代解决方案LinearSnapHelper
2016年

这正好适合我。@galaxigirl重写此方法有助于在滚动稳定时线性捕捉帮助器将捕捉到最近的项目,而不仅仅是下一个/上一个项目。非常感谢您,爵士乐
AA_PV'Dec 25'16

LinearSnapHelper是最适合我的初始解决方案,但似乎PagerSnapHelper为滑动提供了更好的动画效果。
LeonardoSibela

76

Google I / O 2019更新

ViewPager2在这里!

Google刚刚在演讲“ Android的新增功能”(又称“ Android主题演讲”)中宣布,他们正在开发基于RecyclerView的新ViewPager!

从幻灯片:

类似于ViewPager,但效果更好

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

您可以在此处查看最新版本并在此处查看发行说明。还有一个官方样品

个人意见:我认为这是真的需要的补充。我最近在PagerSnapHelper 无限地左右摆动时遇到了很多麻烦-请参阅我打开的票


新答案(2016)

现在,您只需使用SnapHelper即可

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

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

还有一个LinearSnapHelper。我已经尝试过了,如果您精力充沛,它会滚动2个项目,其中包含1个。我个人不喜欢它,而是自己决定-尝试只需几秒钟。


原始答案(2016)

经过很多尝试3个不同的解决方案小时内发现这里的SO我终于建立了一个解决方案,模拟非常密切的行为中找到的ViewPager

该解决方案基于@eDizzle解决方案,我相信我已经对其进行了改进,可以说它的工作原理几乎与ViewPager

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

这里有代码:

public class SnappyRecyclerView extends RecyclerView {

    // Use it with a horizontal LinearLayoutManager
    // Based on https://stackoverflow.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(); 而且似乎可行。
eigil

AWESOOOMMEEEEEEEEEEE
raditya gumay

1
@galaxigirl不,我还没有尝试过LinearSnapHelper。我刚刚更新了答案,以便人们知道正式的解决方案。我的解决方案可以在我的应用程序上正常运行。
艾伯特·维拉·卡尔沃

1
@AlbertVilaCalvo我尝试了LinearSnapHelper。LinearSnapHelper与Scroller一起使用。它根据重力和甩动速度滚动和捕捉。速度越高,滚动的页面越多。我不会推荐它用于像行为的ViewPager。
mipreamble

46

如果目标是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 它对我
有用

1
是的,有中没有捕捉效果LinearSnapHelper
Boonya Kitpitak

1
这会将项目粘贴在视图上以使可滚动
Sam Sam

@BoonyaKitpitak,我尝试过LinearSnapHelper,它捕捉到一个中间项目。PagerSnapHelper妨碍轻松滚动(至少是图像列表)。
CoolMind

@BoonyaKitpitak你能告诉我该怎么做,一个东西完全可见,而旁边的东西一半可见?
Rucha Bhatt Joshi

30

您需要使用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在哪里定义?
ltsai 2015年

@Itsai:糟糕!好问题。这是我在课堂上其他地方设置的变量。它是设备的屏幕宽度(以像素为单位,而不是密度像素),因为smoothScrollBy方法需要滚动显示的像素数。
eDizzle

3
@ltsai您可以更改为getWidth()(recyclerview.getWidth())而不是screenWidth。当RecyclerView的宽度小于屏幕的宽度时,screenWidth无法正常工作。
VAdaihiep

我试图扩展RecyclerView,但出现“错误使类自定义RecyclerView膨胀”。你能帮我吗?谢谢

@bEtTyBarnes:您可能想在另一个问题提要中进行搜索。这里的最初问题超出了范围。
eDizzle '16

6

只需添加padding,并marginrecyclerViewrecyclerView item

recycler查看项目:

<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 '19

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
    }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.