我正在尝试将新的RecyclerView
类用于我希望组件在滚动时捕捉到特定元素的场景(作为此类示例的一个例子,我想到了旧的Android Gallery
带有中心锁定项目的列表)。
这是我到目前为止采用的方法:
我有一个界面ISnappyLayoutManager
,其中包含一个方法getPositionForVelocity
,该方法计算视图在给定初始投掷速度时应该结束滚动的位置。
public interface ISnappyLayoutManager {
int getPositionForVelocity(int velocityX, int velocityY);
}
然后我有一个类SnappyRecyclerView
,其子类RecyclerView
并覆盖其fling()方法,以便将视图推送到正确的数量:
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”的哲学相反,它必须将其子类化以实现某种类型的滚动。其次,如果我只想使用默认的LinearLayoutManager
,这会变得有点复杂,因为我必须弄乱它的内部,以便了解它的当前滚动状态并精确计算滚动到的位置。最后,这甚至没有处理所有可能的滚动方案,就像你移动列表然后暂停然后举起手指一样,没有发生任何事件(速度太低),所以列表仍然处于中途位置。这可以通过向RecyclerView
添加一个on scroll状态监听器来处理,但这也感觉非常hacky。
我觉得我一定错过了什么。有更好的方法吗?
答案 0 :(得分:65)
使用LinearSnapHelper
,现在非常容易。
您需要做的就是:
SnapHelper helper = new LinearSnapHelper();
helper.attachToRecyclerView(recyclerView);
就这么简单!请注意,从版本24.2.0开始,在支持库中添加了LinearSnapHelper
。
这意味着您必须将此添加到您的应用模块的build.gradle
compile "com.android.support:recyclerview-v7:24.2.0"
答案 1 :(得分:58)
我最终提出了与上述略有不同的东西。它并不理想,但它对我来说效果很好,可能对其他人有帮助。我不会接受这个答案,希望别人能有更好的东西和更少的hacky(我可能会误解RecyclerView
实施并错过一些简单的方法这样做,但与此同时,这对政府工作来说已经足够了!)
实施的基础是:RecyclerView
中的滚动在RecyclerView
和LinearLayoutManager
之间分开。我需要处理两种情况:
RecyclerView
将fling传递给内部Scroller
,然后执行滚动魔术。这是有问题的,因为RecyclerView
通常会处于未被破坏的位置。我通过覆盖RecyclerView
fling()
实现来解决此问题,而不是投掷,将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;
}
}
snappy布局管理器的界面:
/**
* 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
子类化为带有平滑滚动的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, i.e. 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;
}
}
答案 2 :(得分:14)
我设法找到一种更清洁的方法来做到这一点。 @Catherine(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);
}
}
逻辑是通用的,可以用于很多其他情况。我的情况是,回收者视图是水平的,并且拉伸整个水平宽度而没有边距(基本上是recyclerview的中心X坐标是屏幕的中心)或不均匀的填充。
任何人都要面对有问题的评论。
答案 3 :(得分:13)
我还需要一个快速的回收者视图。我想让回收器视图项目捕捉到列的左侧。它最终实现了我在Recycler视图上设置的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;
}
如果第一个可见视图滚动超过屏幕的半宽,则下一个可见列将向左移动。
设置监听器:
mRecyclerView.addOnScrollListener(new SnapScrollListener());
答案 4 :(得分:7)
这是一个更简单的黑客,可以在投掷事件中平滑滚动到某个位置:
@Override
public boolean fling(int velocityX, int velocityY) {
smoothScrollToPosition(position);
return super.fling(0, 0);
}
通过调用smoothScrollToPosition(int position)覆盖fling方法,其中&#34; int position&#34;是适配器中所需视图的位置。您需要以某种方式获得职位的价值,但这取决于您的需求和实施。
答案 5 :(得分:6)
在使用了RecyclerView之后,这就是我到目前为止所提出的以及我现在正在使用的内容。它有一个小缺陷,但我不会泄漏豆子(因为你可能不会注意到)。
https://gist.github.com/lauw/fc84f7d04f8c54e56d56
它仅支持水平回收视图和捕捉到中心,还可以根据它们离中心的距离缩小视图。用作RecyclerView的替代品。
编辑:08/2016
把它变成一个存储库:
https://github.com/lauw/Android-SnappingRecyclerView
我会在改进实施的同时保持这一点。
答案 6 :(得分:5)
实现快速定位行为的一种非常简单的方法 -
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;
}
}
});
唯一的小缺点是当你滚动不到部分可见单元格的一半时它不会向后突然移动 - 但如果这不会让你烦恼,那么它就是一个干净而简单的解决方案。
答案 7 :(得分:5)
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
以下是一个很好的例子:
答案 8 :(得分:4)
如果您需要捕捉开始,顶部,结尾或底部的支持,请使用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捕捉顶部:
topRecyclerView.setLayoutManager(new LinearLayoutManager(this));
SnapHelper snapHelperTop = new GravitySnapHelper(Gravity.TOP);
snapHelperTop.attachToRecyclerView(topRecyclerView);
答案 9 :(得分:3)
我已经为RecyclerView的水平方向实现了一个工作解决方案,它只是在第一个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;
}}
对我来说很好,如果出现问题,请告诉我。
答案 10 :(得分:2)
更新humblerookie的答案:
这个滚动侦听器对于中心锁定确实有效 https://github.com/humblerookie/centerlockrecyclerview/
但是这里有一种更简单的方法可以在recyclerview的开头和结尾添加填充以使其元素居中:
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);
}
}
});
答案 11 :(得分:1)
另一个更清洁的选择是使用自定义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()));
答案 12 :(得分:-1)
我需要的东西与上面的所有答案略有不同。
主要要求是:
RecyclerView
。LayoutManager
或RecyclerView
。只需RecyclerView.OnScrollListener
,然后由recyclerView.addOnScrollListener(snapScrollListener)
附加。这样代码就更清晰了。以下示例中的两个非常具体的要求应该很容易改变,以适应您的情况:
RecyclerView
。此解决方案使用原生LinearSmoothScroller
。不同之处在于,在最后一步,当&#34;目标视图&#34;发现它改变了偏移的计算,以便它捕捉到特定的位置。
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);
}
}