简体   繁体   English

RecyclerView 水平滚动对齐居中

[英]RecyclerView horizontal scroll snap in center

I'm trying to make a carousel-like view here using RecyclerView, I want the item to snap in the middle of the screen when scrolling, one item at a time.我正在尝试使用 RecyclerView 在这里制作类似轮播的视图,我希望该项目在滚动时捕捉到屏幕中间,一次一个项目。 I've tried using recyclerView.setScrollingTouchSlop(RecyclerView.TOUCH_SLOP_PAGING);我试过使用recyclerView.setScrollingTouchSlop(RecyclerView.TOUCH_SLOP_PAGING);

but the view is still scrolling smoothly, I've also tried to implement my own logic using scroll listener like so:但视图仍在平滑滚动,我也尝试使用滚动侦听器实现我自己的逻辑,如下所示:

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

The swipe from right to left is working fine now, but not the other way around, what am I missing here ?从右向左滑动现在工作正常,但反过来不行,我在这里错过了什么?

With LinearSnapHelper , this is now very easy.使用LinearSnapHelper ,这现在非常容易。

All you need to do is this:您需要做的就是:

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

Update更新

Available since 25.1.0, PagerSnapHelper can help achieve ViewPager like effect.从 25.1.0 开始可用, PagerSnapHelper可以帮助实现类似ViewPager效果。 Use it as you would use the LinearSnapHelper .像使用LinearSnapHelper一样使用它。

Old workaround:旧的解决方法:

If you wish for it to behave akin to the ViewPager , try this instead:如果您希望它的行为类似于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);

The implementation above just returns the position next to the current item (centered) based on the direction of the velocity, regardless of the magnitude.上面的实现只是根据速度的方向返回当前项目旁边的位置(居中),而不管大小。

The former one is a first party solution included in the Support Library version 24.2.0.前者是支持库版本 24.2.0 中包含的第一方解决方案。 Meaning you have to add this to your app module's build.gradle or update it.这意味着您必须将此添加到您的应用程序模块的build.gradle或更新它。

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

Google I/O 2019 Update Google I/O 2019 更新

ViewPager2 is here! ViewPager2来了!

Google just announced at the talk 'What's New in Android' (aka 'The Android keynote') that they are working on a new ViewPager based on RecyclerView!谷歌刚刚在“Android 的新特性”(又名“Android 主题演讲”)演讲中宣布,他们正在开发基于 RecyclerView 的新 ViewPager!

From the slides:从幻灯片:

Like ViewPager, but better像 ViewPager,但更好

  • Easy migration from ViewPager从 ViewPager 轻松迁移
  • Based on RecyclerView基于 RecyclerView
  • Right-to-Left mode support从右到左模式支持
  • Allows vertical paging允许垂直分页
  • Improved dataset change notifications改进的数据集更改通知

You can check the latest version here and the release notes here .您可以在此处查看最新版本以及在此处查看发行说明。 There is also an official sample .还有一个官方样本

Personal opinion: I think this is a really needed addition.个人意见:我认为这是一个非常需要的补充。 I've recently had a lot of trouble with the PagerSnapHelper oscillating left right indefinitely - see the ticket I've opened.我最近在PagerSnapHelper 无限期PagerSnapHelper 摆动时遇到了很多麻烦 - 请参阅我打开的票证


New answer (2016)新答案(2016)

You can now just use a SnapHelper .您现在可以只使用SnapHelper

If you want a center-aligned snapping behavior similar to ViewPager then use PagerSnapHelper :如果您想要类似于ViewPager的居中对齐对齐行为, 使用PagerSnapHelper

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

There is also a LinearSnapHelper .还有一个LinearSnapHelper I've tried it and if you fling with energy then it scrolls 2 items with 1 fling.我试过了,如果你充满能量,那么它会用 1 次投掷滚动 2 个项目。 Personally I didn't like it, but just decide by yourself - trying it only takes seconds.就我个人而言,我不喜欢它,但只能自己决定 - 尝试只需几秒钟。


Original answer (2016)原始答案 (2016)

After many hours of trying 3 different solutions found here in SO I've finally built a solution that mimics very closely the behavior found in a ViewPager .经过3个尝试不同的解决方案小时发现这里于是我终于建立了一个解决方案,模拟非常密切的行为中发现ViewPager

The solution is based on the @eDizzle solution , which I believe I've improved enough to say that it works almost like a ViewPager .该解决方案基于@eDizzle解决方案,我相信我已经改进到足以说它几乎像ViewPager一样ViewPager

Important: my RecyclerView items width is exactly the same as the screen.重要提示:我的RecyclerView项目宽度与屏幕完全相同。 I haven't tried with other sizes.其他尺寸我没试过。 Also I use it with an horizontal LinearLayoutManager .我也将它与水平LinearLayoutManager I think that you will need to adapt the code if you want vertical scroll.我认为如果你想要垂直滚动,你需要调整代码。

Here you have the code:代码如下:

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

}

Enjoy!享受!

If the goal is to make the RecyclerView mimic the behavior of ViewPager there is quite easy approach如果目标是使RecyclerView模仿ViewPager的行为, 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);

By using PagerSnapHelper you can get the behavior like ViewPager通过使用PagerSnapHelper您可以获得类似ViewPager的行为

You need to use findFirstVisibleItemPosition for going in the opposite direction.您需要使用 findFirstVisibleItemPosition 进行相反的方向。 And for detecting which direction the swipe was in, youll need to get either the fling velocity or the change in x.为了检测滑动的方向,您需要获得投掷速度或 x 的变化。 I approached this problem from a slightly different angle than you have.我从与您略有不同的角度来解决这个问题。

Create a new class that extends the RecyclerView class and then override RecyclerView's fling method like so:创建一个扩展 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;
}

Just add padding and margin to recyclerView and recyclerView item :只需向recyclerViewrecyclerView item添加paddingmargin

recyclerView item:回收者查看项目:

<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" />

and set PagerSnapHelper :并设置PagerSnapHelper

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

dp to px: dp 到 px:

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

result:结果:

在此处输入图片说明

My solution:我的解决方案:

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

I pass this layout manager to the RecycledView and set the offset required to center items.我将此布局管理器传递给 RecycledView 并设置居中项目所需的偏移量。 All my items have the same width so constant offset is ok我所有的项目都有相同的宽度,所以恒定的偏移量是可以的

PagerSnapHelper doesn't work with GridLayoutManager with spanCount > 1, so my solution under this circumstance is: 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
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM