在RecyclerView中快速滚动
我正在尝试使用新的RecyclerView
类作为一种场景,我希望组件在滚动时捕捉到一个特定的元素(旧的Android Gallery
作为这样一个中心locking项目列表的例子)。
这是我迄今采取的方法:
我有一个接口, ISnappyLayoutManager
,其中包含一个方法, getPositionForVelocity
,它计算在哪个位置视图应该结束滚动给定的初始飞行速度。
public interface ISnappyLayoutManager { int getPositionForVelocity(int velocityX, int velocityY); }
然后我有一个类, SnappyRecyclerView
,它的子类RecyclerView
并重写它的SnappyRecyclerView
()方法的方式,以抛出视图的确切数量:
public final class SnappyRecyclerView extends RecyclerView { /** other methods deleted **/ @Override public boolean fling(int velocityX, int velocityY) { LayoutManager lm = getLayoutManager(); if (lm instanceof ISnappyLayoutManager) { super.smoothScrollToPosition(((ISnappyLayoutManager) getLayoutManager()) .getPositionForVelocity(velocityX, velocityY)); } return true; } }
我对这种方法并不满意,原因有几个。 首先,与“RecyclerView”的哲学似乎相反,必须将其子类化以实现某种types的滚动。 其次,如果我只想使用默认的LinearLayoutManager
,这将变得有点复杂,因为我不得不乱搞它的内部,以便了解它的当前滚动状态并计算出滚动到的位置。 最后,这甚至不关心所有可能的滚动场景,就像移动列表然后暂停然后抬起手指一样,没有发生滑动事件(速度太低),所以列表保持半途位置。 这可能是通过添加一个滚动状态侦听器到RecyclerView
,但是这也感觉非常黑客。
我觉得我一定会错过一些东西。 有没有更好的方法来做到这一点?
最后我想出了一些与上面略有不同的东西。 这不是理想的,但它对我来说工作得可以接受,可能对别人有帮助。 我不会接受这个答案,希望别人能够做出更好,更简单的做法(我可能误解了RecyclerView
实现,并且错过了一些简单的方法,但是同时这很好足够政府工作!)
实现的基础是这样的: RecyclerView
的滚动是RecyclerView
和LinearLayoutManager
之间的分离。 有两种情况我需要处理:
- 用户转动视图。 默认行为是
RecyclerView
将RecyclerView
传递给内部Scroller
,然后执行滚动魔术。 这是有问题的,因为RecyclerView
通常安置在未绑定的位置。 我通过覆盖RecyclerView
flingfling()
实现来解决这个问题,而不是扔掉,将LinearLayoutManager
平滑滚动到一个位置。 - 用户以不足的速度抬起手指以开始滚动。 在这种情况下不会发生任何反应。 如果视图不处于捕捉位置,我想检测这种情况。 我通过覆盖
onTouchEvent
方法来做到这一点。
SnappyRecyclerView
:
public final class SnappyRecyclerView extends RecyclerView { public SnappyRecyclerView(Context context) { super(context); } public SnappyRecyclerView(Context context, AttributeSet attrs) { super(context, attrs); } public SnappyRecyclerView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } @Override public boolean fling(int velocityX, int velocityY) { final LayoutManager lm = getLayoutManager(); if (lm instanceof ISnappyLayoutManager) { super.smoothScrollToPosition(((ISnappyLayoutManager) getLayoutManager()) .getPositionForVelocity(velocityX, velocityY)); return true; } return super.fling(velocityX, velocityY); } @Override public boolean onTouchEvent(MotionEvent e) { // We want the parent to handle all touch events--there's a lot going on there, // and there is no reason to overwrite that functionality--bad things will happen. final boolean ret = super.onTouchEvent(e); final LayoutManager lm = getLayoutManager(); if (lm instanceof ISnappyLayoutManager && (e.getAction() == MotionEvent.ACTION_UP || e.getAction() == MotionEvent.ACTION_CANCEL) && getScrollState() == SCROLL_STATE_IDLE) { // The layout manager is a SnappyLayoutManager, which means that the // children should be snapped to a grid at the end of a drag or // fling. The motion event is either a user lifting their finger or // the cancellation of a motion events, so this is the time to take // over the scrolling to perform our own functionality. // Finally, the scroll state is idle--meaning that the resultant // velocity after the user's gesture was below the threshold, and // no fling was performed, so the view may be in an unaligned state // and will not be flung to a proper state. smoothScrollToPosition(((ISnappyLayoutManager) lm).getFixScrollPos()); } return ret; } }
快速布局pipe理器的界面:
/** * An interface that LayoutManagers that should snap to grid should implement. */ public interface ISnappyLayoutManager { /** * @param velocityX * @param velocityY * @return the resultant position from a fling of the given velocity. */ int getPositionForVelocity(int velocityX, int velocityY); /** * @return the position this list must scroll to to fix a state where the * views are not snapped to grid. */ int getFixScrollPos(); }
这里是一个LayoutManager
的例子,它将LinearLayoutManager
子类LinearLayoutManager
一个带有平滑滚动的LayoutManager
:
public class SnappyLinearLayoutManager extends LinearLayoutManager implements ISnappyLayoutManager { // These variables are from android.widget.Scroller, which is used, via ScrollerCompat, by // Recycler View. The scrolling distance calculation logic originates from the same place. Want // to use their variables so as to approximate the look of normal Android scrolling. // Find the Scroller fling implementation in android.widget.Scroller.fling(). private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1) private static float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9)); private static double FRICTION = 0.84; private double deceleration; public SnappyLinearLayoutManager(Context context) { super(context); calculateDeceleration(context); } public SnappyLinearLayoutManager(Context context, int orientation, boolean reverseLayout) { super(context, orientation, reverseLayout); calculateDeceleration(context); } private void calculateDeceleration(Context context) { deceleration = SensorManager.GRAVITY_EARTH // g (m/s^2) * 39.3700787 // inches per meter // pixels per inch. 160 is the "default" dpi, ie one dip is one pixel on a 160 dpi // screen * context.getResources().getDisplayMetrics().density * 160.0f * FRICTION; } @Override public int getPositionForVelocity(int velocityX, int velocityY) { if (getChildCount() == 0) { return 0; } if (getOrientation() == HORIZONTAL) { return calcPosForVelocity(velocityX, getChildAt(0).getLeft(), getChildAt(0).getWidth(), getPosition(getChildAt(0))); } else { return calcPosForVelocity(velocityY, getChildAt(0).getTop(), getChildAt(0).getHeight(), getPosition(getChildAt(0))); } } private int calcPosForVelocity(int velocity, int scrollPos, int childSize, int currPos) { final double dist = getSplineFlingDistance(velocity); final double tempScroll = scrollPos + (velocity > 0 ? dist : -dist); if (velocity < 0) { // Not sure if I need to lower bound this here. return (int) Math.max(currPos + tempScroll / childSize, 0); } else { return (int) (currPos + (tempScroll / childSize) + 1); } } @Override public void smoothScrollToPosition(RecyclerView recyclerView, State state, int position) { final LinearSmoothScroller linearSmoothScroller = new LinearSmoothScroller(recyclerView.getContext()) { // I want a behavior where the scrolling always snaps to the beginning of // the list. Snapping to end is also trivial given the default implementation. // If you need a different behavior, you may need to override more // of the LinearSmoothScrolling methods. protected int getHorizontalSnapPreference() { return SNAP_TO_START; } protected int getVerticalSnapPreference() { return SNAP_TO_START; } @Override public PointF computeScrollVectorForPosition(int targetPosition) { return SnappyLinearLayoutManager.this .computeScrollVectorForPosition(targetPosition); } }; linearSmoothScroller.setTargetPosition(position); startSmoothScroll(linearSmoothScroller); } private double getSplineFlingDistance(double velocity) { final double l = getSplineDeceleration(velocity); final double decelMinusOne = DECELERATION_RATE - 1.0; return ViewConfiguration.getScrollFriction() * deceleration * Math.exp(DECELERATION_RATE / decelMinusOne * l); } private double getSplineDeceleration(double velocity) { return Math.log(INFLEXION * Math.abs(velocity) / (ViewConfiguration.getScrollFriction() * deceleration)); } /** * This implementation obviously doesn't take into account the direction of the * that preceded it, but there is no easy way to get that information without more * hacking than I was willing to put into it. */ @Override public int getFixScrollPos() { if (this.getChildCount() == 0) { return 0; } final View child = getChildAt(0); final int childPos = getPosition(child); if (getOrientation() == HORIZONTAL && Math.abs(child.getLeft()) > child.getMeasuredWidth() / 2) { // Scrolled first view more than halfway offscreen return childPos + 1; } else if (getOrientation() == VERTICAL && Math.abs(child.getTop()) > child.getMeasuredWidth() / 2) { // Scrolled first view more than halfway offscreen return childPos + 1; } return childPos; } }
使用LinearSnapHelper
,现在这非常简单。
所有你需要做的是这样的:
SnapHelper helper = new LinearSnapHelper(); helper.attachToRecyclerView(recyclerView);
就这么简单! 请注意,从版本24.2.0开始, LinearSnapHelper
已添加到支持库中。
这意味着你必须添加到你的应用程序模块的build.gradle
compile "com.android.support:recyclerview-v7:24.2.0"
我设法find一个更干净的方式来做到这一点。 @凯瑟琳(OP)让我知道,如果这可以改善,或者你觉得是对你的改进:)
这是我使用的滚动监听器。
https://github.com/humblerookie/centerlockrecyclerview/
我在这里省略了一些小的假设,例如。
1) 初始和最终填充 :水平滚动中的第一个和最后一个项目需要分别设置初始和最终填充,以便当分别滚动到第一个和最后一个时,初始视图和最终视图位于中间。例如,在onBindViewHolder中,您可以像这样的东西。
@Override public void onBindViewHolder(ReviewHolder holder, int position) { holder.container.setPadding(0,0,0,0);//Resetpadding if(position==0){ //Only one element if(mData.size()==1){ holder.container.setPadding(totalpaddinginit/2,0,totalpaddinginit/2,0); } else{ //>1 elements assign only initpadding holder.container.setPadding(totalpaddinginit,0,0,0); } } else if(position==mData.size()-1){ holder.container.setPadding(0,0,totalpaddingfinal,0); } } public class ReviewHolder extends RecyclerView.ViewHolder { protected TextView tvName; View container; public ReviewHolder(View itemView) { super(itemView); container=itemView; tvName= (TextView) itemView.findViewById(R.id.text); } }
逻辑是漂亮的通用的,可以用于很多其他情况。 我的情况是,回收者视图是水平的,并且无边距地延伸整个水平宽度(基本上回收站的中心X坐标是屏幕的中心)或不均匀的填充。
任何人都面临着问题的评论。
我还需要一个活泼的回收者视图。 我想让回收者查看项目捕捉到列的左侧。 最后实现了一个我在回收视图中设置的SnapScrollListener。 这是我的代码:
SnapScrollListener:
class SnapScrollListener extends RecyclerView.OnScrollListener { @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { if (RecyclerView.SCROLL_STATE_IDLE == newState) { final int scrollDistance = getScrollDistanceOfColumnClosestToLeft(mRecyclerView); if (scrollDistance != 0) { mRecyclerView.smoothScrollBy(scrollDistance, 0); } } } }
捕捉的计算:
private int getScrollDistanceOfColumnClosestToLeft(final RecyclerView recyclerView) { final LinearLayoutManager manager = (LinearLayoutManager) recyclerView.getLayoutManager(); final RecyclerView.ViewHolder firstVisibleColumnViewHolder = recyclerView.findViewHolderForAdapterPosition(manager.findFirstVisibleItemPosition()); if (firstVisibleColumnViewHolder == null) { return 0; } final int columnWidth = firstVisibleColumnViewHolder.itemView.getMeasuredWidth(); final int left = firstVisibleColumnViewHolder.itemView.getLeft(); final int absoluteLeft = Math.abs(left); return absoluteLeft <= (columnWidth / 2) ? left : columnWidth - absoluteLeft; }
如果第一个可见视图的滚动次数超出屏幕的半宽度,则下一个可见列将向左alignment。
设置监听器:
mRecyclerView.addOnScrollListener(new SnapScrollListener());
这里有一个简单的黑客平滑滚动到一定的位置在一个投掷事件:
@Override public boolean fling(int velocityX, int velocityY) { smoothScrollToPosition(position); return super.fling(0, 0); }
通过调用smoothScrollToPosition(int position)来重写fling方法,其中“int position”是您在适配器中所需视图的位置。 您需要以某种方式获得该职位的价值,但这取决于您的需求和实施。
一个非常简单的方法来实现alignment的行为 –
recyclerView.setOnScrollListener(new OnScrollListener() { private boolean scrollingUp; @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { // Or use dx for horizontal scrolling scrollingUp = dy < 0; } @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { // Make sure scrolling has stopped before snapping if (newState == RecyclerView.SCROLL_STATE_IDLE) { // layoutManager is the recyclerview's layout manager which you need to have reference in advance int visiblePosition = scrollingUp ? layoutManager.findFirstVisibleItemPosition() : layoutManager.findLastVisibleItemPosition(); int completelyVisiblePosition = scrollingUp ? layoutManager .findFirstCompletelyVisibleItemPosition() : layoutManager .findLastCompletelyVisibleItemPosition(); // Check if we need to snap if (visiblePosition != completelyVisiblePosition) { recyclerView.smoothScrollToPosition(visiblePosition); return; } } });
唯一的一个小缺点是,当你滚动的时候,当你滚动不到半个可见的单元格的一半时,它将不会反转 – 但是如果这不会影响你,那么这是一个干净而简单的解决scheme。
用RecyclerView搞了一会儿之后,这就是我现在想到的,现在正在使用的东西。 它有一个小小的缺陷,但我不会泄漏豆(还),因为你可能不会注意到。
https://gist.github.com/lauw/fc84f7d04f8c54e56d56
它只支持横向循环查看和捕捉到中心,也可以根据他们离中心有多远缩小视图。 用作RecyclerView的替代品。
编辑:08/2016成为一个存储库:
https://github.com/lauw/Android-SnappingRecyclerView
我将继续保持这一点,同时更好地实施。
我已经实现了RecyclerView的水平方向的工作解决scheme,它只是在第一个MOVE和UP上读取坐标onTouchEvent。 在UP计算我们需要去的位置。
public final class SnappyRecyclerView extends RecyclerView { private Point mStartMovePoint = new Point( 0, 0 ); private int mStartMovePositionFirst = 0; private int mStartMovePositionSecond = 0; public SnappyRecyclerView( Context context ) { super( context ); } public SnappyRecyclerView( Context context, AttributeSet attrs ) { super( context, attrs ); } public SnappyRecyclerView( Context context, AttributeSet attrs, int defStyle ) { super( context, attrs, defStyle ); } @Override public boolean onTouchEvent( MotionEvent e ) { final boolean ret = super.onTouchEvent( e ); final LayoutManager lm = getLayoutManager(); View childView = lm.getChildAt( 0 ); View childViewSecond = lm.getChildAt( 1 ); if( ( e.getAction() & MotionEvent.ACTION_MASK ) == MotionEvent.ACTION_MOVE && mStartMovePoint.x == 0) { mStartMovePoint.x = (int)e.getX(); mStartMovePoint.y = (int)e.getY(); mStartMovePositionFirst = lm.getPosition( childView ); if( childViewSecond != null ) mStartMovePositionSecond = lm.getPosition( childViewSecond ); }// if MotionEvent.ACTION_MOVE if( ( e.getAction() & MotionEvent.ACTION_MASK ) == MotionEvent.ACTION_UP ){ int currentX = (int)e.getX(); int width = childView.getWidth(); int xMovement = currentX - mStartMovePoint.x; // move back will be positive value final boolean moveBack = xMovement > 0; int calculatedPosition = mStartMovePositionFirst; if( moveBack && mStartMovePositionSecond > 0 ) calculatedPosition = mStartMovePositionSecond; if( Math.abs( xMovement ) > ( width / 3 ) ) calculatedPosition += moveBack ? -1 : 1; if( calculatedPosition >= getAdapter().getItemCount() ) calculatedPosition = getAdapter().getItemCount() -1; if( calculatedPosition < 0 || getAdapter().getItemCount() == 0 ) calculatedPosition = 0; mStartMovePoint.x = 0; mStartMovePoint.y = 0; mStartMovePositionFirst = 0; mStartMovePositionSecond = 0; smoothScrollToPosition( calculatedPosition ); }// if MotionEvent.ACTION_UP return ret; }}
对我来说工作得很好,让我知道是否有问题。
Snap Scroll现在是支持库24.2.0的一部分
https://developer.android.com/topic/libraries/support-library/revisions.html
相关类:
https://developer.android.com/reference/android/support/v7/widget/SnapHelper.html
https://developer.android.com/reference/android/support/v7/widget/LinearSnapHelper.html
以下是一个很好的例子:
要更新humblerookie的答案:
这个滚动监听器确实有效的中心lockinghttps://github.com/humblerookie/centerlockrecyclerview/
但是,在回收站视图的开始和结束处添加填充以便将其元素居中放置,这是一种更简单的方法:
mRecycler.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { int childWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, CHILD_WIDTH_IN_DP, getResources().getDisplayMetrics()); int offset = (mRecycler.getWidth() - childWidth) / 2; mRecycler.setPadding(offset, mRecycler.getPaddingTop(), offset, mRecycler.getPaddingBottom()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { mRecycler.getViewTreeObserver().removeOnGlobalLayoutListener(this); } else { mRecycler.getViewTreeObserver().removeGlobalOnLayoutListener(this); } } });
如果你需要抓取支持开始,顶部,结束或底部,使用GravitySnapHelper( https://github.com/rubensousa/RecyclerViewSnap/blob/master/app/src/main/java/com/github/rubensousa/recyclerviewsnap/GravitySnapHelper .java )。
攫取中心:
SnapHelper snapHelper = new LinearSnapHelper(); snapHelper.attachToRecyclerView(recyclerView);
捕捉GravitySnapHelper开始:
startRecyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)); SnapHelper snapHelperStart = new GravitySnapHelper(Gravity.START); snapHelperStart.attachToRecyclerView(startRecyclerView);
用GravitySnapHelper与顶部alignment:
topRecyclerView.setLayoutManager(new LinearLayoutManager(this)); SnapHelper snapHelperTop = new GravitySnapHelper(Gravity.TOP); snapHelperTop.attachToRecyclerView(topRecyclerView);
而另一个更清洁的select是使用自定义LayoutManager
,你可以检查https://github.com/apptik/multiview/tree/master/layoutmanagers
它正在开发中,但工作得很好。 快照可用: https : //oss.sonatype.org/content/repositories/snapshots/io/apptik/multiview/layoutmanagers/
例:
recyclerView.setLayoutManager(new SnapperLinearLayoutManager(getActivity()));
我需要一些与以上所有答案有所不同的东西。
主要要求是:
- 当用户fl动或刚刚释放他的手指时,它的工作原理是一样的。
- 使用本机滚动机制与普通的
RecyclerView
具有相同的“感觉”。 - 停止时,开始平滑滚动到最近的捕捉点。
- 不需要使用自定义
LayoutManager
或RecyclerView
。 只需一个RecyclerView.OnScrollListener
,然后由recyclerView.addOnScrollListener(snapScrollListener)
附加。 这样的代码更清洁。
以下两个例子中的两个非常具体的要求应该很容易改变,以适应您的情况:
- 水平工作。
- 将项目的左边缘捕捉到
RecyclerView
的特定点。
该解决scheme使用本地LinearSmoothScroller
。 不同之处在于,在最后一步中,当find“目标视图”时,它会改变偏移量的计算,以便捕捉到特定的位置。
public class SnapScrollListener extends RecyclerView.OnScrollListener { private static final float MILLIS_PER_PIXEL = 200f; /** The x coordinate of recycler view to which the items should be scrolled */ private final int snapX; int prevState = RecyclerView.SCROLL_STATE_IDLE; int currentState = RecyclerView.SCROLL_STATE_IDLE; public SnapScrollListener(int snapX) { this.snapX = snapX; } @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); currentState = newState; if(prevState != RecyclerView.SCROLL_STATE_IDLE && currentState == RecyclerView.SCROLL_STATE_IDLE ){ performSnap(recyclerView); } prevState = currentState; } private void performSnap(RecyclerView recyclerView) { for( int i = 0 ;i < recyclerView.getChildCount() ; i ++ ){ View child = recyclerView.getChildAt(i); final int left = child.getLeft(); int right = child.getRight(); int halfWidth = (right - left) / 2; if (left == snapX) return; if (left - halfWidth <= snapX && left + halfWidth >= snapX) { //check if child is over the snapX position int adapterPosition = recyclerView.getChildAdapterPosition(child); int dx = snapX - left; smoothScrollToPositionWithOffset(recyclerView, adapterPosition, dx); return; } } } private void smoothScrollToPositionWithOffset(RecyclerView recyclerView, int adapterPosition, final int dx) { final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); if( layoutManager instanceof LinearLayoutManager) { LinearSmoothScroller scroller = new LinearSmoothScroller(recyclerView.getContext()) { @Override public PointF computeScrollVectorForPosition(int targetPosition) { return ((LinearLayoutManager) layoutManager).computeScrollVectorForPosition(targetPosition); } @Override protected void onTargetFound(View targetView, RecyclerView.State state, Action action) { final int dy = calculateDyToMakeVisible(targetView, getVerticalSnapPreference()); final int distance = (int) Math.sqrt(dx * dx + dy * dy); final int time = calculateTimeForDeceleration(distance); if (time > 0) { action.update(-dx, -dy, time, mDecelerateInterpolator); } } @Override protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) { return MILLIS_PER_PIXEL / displayMetrics.densityDpi; } }; scroller.setTargetPosition(adapterPosition); layoutManager.startSmoothScroll(scroller); } }