ここでは、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();
}
}
右から左へのスワイプは現在正常に機能していますが、逆ではありません。ここで何が欠けていますか?
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"
Google I/O 2019アップデート:
ViewPager2 はこちら!
Google ちょうど発表された 講演「Androidの新機能」(別名「Android基調講演」)で、RecyclerViewに基づいた新しいViewPagerに取り組んでいる!
スライドから:
ViewPagerに似ていますが、より優れています
- ViewPagerからの簡単な移行
- RecyclerViewに基づく
- 右から左へのモードのサポート
- 垂直ページングが可能
- データセット変更通知の改善
最新バージョン here およびリリースノート here を確認できます。
個人的な意見:これは本当に必要な追加だと思います。最近、PagerSnapHelper
で多くの問題が発生しました 無期限に左右に振動 -参照してください チケット 私は開きました。
新しい回答(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://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);
}
}
}
}
楽しい!
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
のような動作を得ることができます
逆方向に進むには、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;
}
私の解決策:
/**
* 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に渡し、アイテムの中央揃えに必要なオフセットを設定します。すべてのアイテムの幅は同じなので、一定のオフセットで問題ありません
padding
とmargin
を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());
}
結果:
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
}
}