RecyclerView水平スクロールスナップを中央に配置


89

ここでRecyclerViewを使用してカルーセルのようなビューを作成しようとしています。スクロールすると、アイテムが画面の中央に1つずつスナップするようにします。使ってみました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();
                        }
                    }

右から左へのスワイプは現在正常に機能していますが、その逆ではありません。ここで何が欠けていますか?

回答:


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による解決策はうまくいきました)。実装しましたか?それからメソッドをオーバーライドすることになっていますか?クラスは箱から出してすべての仕事をするべきだと思います
galaxigirl 2016

はい、それゆえに答えです。しかし、私は徹底的にテストしていません。明確にするために、私はこれを水平方向LinearLayoutManagerで行い、ビューはすべて通常のサイズでした。上記のスニペット以外は必要ありません。
razzledazzle 2016

@galaxigirl謝罪、私はあなたが何を意味するのか理解し、それを認めていくつかの編集を行いました、コメントしてください。これは、を使用した代替ソリューションLinearSnapHelperです。
razzledazzle 2016

これは私にとってちょうどいい働きをしました。このメソッドをオーバーライドする@galaxigirlは、スクロールが落ち着くときに、線形スナップヘルパーが、次/前のアイテムだけでなく、最も近いアイテムにスナップするのに役立ちます。これをどうもありがとう、razzledazzle
AA_PV 2016

LinearSnapHelperは私にとって最初の解決策でしたが、PagerSnapHelperはスワイプにより良いアニメーションを提供したようです。
LeonardoSibela

76

Google I / O2019アップデート

ViewPager2はこちらです!

Googleはちょうど発表しました「のAndroidの新機能」の話で(別名「Androidの基調講演」)彼らはRecyclerViewに基づく新しいViewPagerに取り組んでいることを!

スライドから:

ViewPagerと似ていますが、より優れています

  • ViewPagerからの簡単な移行
  • RecyclerViewに基づく
  • 右から左へのモードのサポート
  • 垂直ページングを許可します
  • 改善されたデータセット変更通知

最新バージョンはこちらとリリースノートで確認できますこちらでます公式サンプルもあります

個人的な意見:これは本当に必要な追加だと思います。私は最近、多くの問題を抱えていますPagerSnapHelper 左右に無期限に振動ます-私が開いたチケットを参照してください。


新しい答え(2016)

これで、SnapHelperを使用できます。

ViewPagerと同様の中央揃えのスナップ動作が必要な場合は、PagerSnapHelperを使用してください

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

もあります LinearSnapHelperます。私はそれを試しました、そしてあなたがエネルギーで投げるならば、それは1つの投げで2つのアイテムをスクロールします。個人的には気に入らなかったのですが、自分で決めるだけです。試してみるのはほんの数秒です。


元の回答(2016)

SOで見つかった3つの異なるソリューションを時間も試した後私はついに、で見つかった動作を非常によく模倣するソリューションを構築しました。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をゆっくりスワイプすると、小さなバグが発生します。ありがとう!
Mauricio Sartori

マージンをサポートするために(android:layout_marginStart = "16dp")int screenwidth = getWidth();で試しました。そしてそれはうまくいくようです。
eigil 2016年

AWESOOOMMEEEEEEEEE
raditya gumay 2016

1
@galaxigirlいいえ、LinearSnapHelperはまだ試していません。人々が公式の解決策について知っているように、私はちょうど答えを更新しました。私のソリューションは、アプリで問題なく機能します。
アルバートビラカルボ2016

1
@AlbertVilaCalvoLinearSnapHelperを試しました。LinearSnapHelperはScrollerで動作します。重力とフリング速度に基づいてスクロールし、スナップします。速度が速いほど、より多くのページがスクロールされます。ViewPagerのような動作にはお勧めしません。
mipreamble 2016

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 そしてそれは私のために働きます
fanjavaid 2017

1
はい、スナップ効果はありませんLinearSnapHelper
Boonya Kitpitak 2017

1
これにより、アイテムがビューに貼り付けられ、スクロール可能になります
Sam

@BoonyaKitpitak、試してみLinearSnapHelperましたが、真ん中のアイテムにスナップします。PagerSnapHelper簡単にスクロールするのを妨げます(少なくとも、画像のリスト)。
CoolMind

@BoonyaKitpitakこれを行う方法を教えてください。1つのアイテムが完全に表示され、サイドアイテムが半分表示されますか?
Rucha BhattJoshi19年

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 2015

3
@ltsai screenWidthの代わりにgetWidth()(recyclerview.getWidth())に変更できます。RecyclerViewの幅が画面の幅よりも小さい場合、screenWidthは正しく機能しません。
VAdaihiep 2015年

RecyclerViewを拡張しようとしましたが、「クラスカスタムRecyclerViewの拡張エラー」が発生します。手伝ってくれますか?ありがとう

@bEtTyBarnes:別の質問フィードでそれを検索することをお勧めします。ここでの最初の質問では、かなり範囲外です。
eDizzle 2016

6

ただ、追加paddingmarginするrecyclerViewと、recyclerView 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());
}

結果:

ここに画像の説明を入力してください


アイテム間の可変パディングを実現する方法はありますか?次/前のカードと中央に配置されている中間のカードをより多く表示するために、最初と最後のカードを中央に配置する必要がない場合など。
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

PagerSnapHelperGridLayoutManagerspanCount> 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
    }
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.