RecyclerView中的快速滚动


74

我正在尝试将新RecyclerView类用于一种情况,在这种情况下,我希望组件在滚动时能够捕捉到特定元素(以旧AndroidGallery为例,此类列表带有中心锁定项)。

到目前为止,这是我采用的方法:

我有一个接口,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”的原理背道而驰。其次,如果我只想使用default LinearLayoutManager,这将变得有些复杂,因为我必须弄乱它的内部,以便了解其当前滚动状态并精确计算出滚动到的位置。最后,这甚至都无法解决所有可能的滚动情况,就像您先移动列表然后暂停然后松开手指一样,不会发生跳动事件(速度太低),因此列表保留在中间位置。可以通过在上添加滚动状态侦听器来解决此问题RecyclerView,但这也感觉很hacky。

我觉得我一定很想念东西。有一个更好的方法吗?

Answers:


79

使用LinearSnapHelper,现在非常容易。

您需要做的就是:

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

就这么简单!请注意,它LinearSnapHelper是从版本24.2.0开始添加到支持库中的。

意味着您必须将其添加到应用模块的 build.gradle

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

编辑:AndroidX LinearSnapHelper


4
不幸的是,它紧贴列表项的中间位置
sativa

10
值得一提的是,如果有人对此解决方案有相同的问题,那我会做:如果在设置recyclerview时收到“ IllegalStateException:OnFlingListener的实例已设置”,则应调用recyclerView.setOnFlingListener(null);。在snapHelper.attachToRecyclerView(recyclerView)之前;
Analizer

如何使用SnapHelper控制快照的速度?
泰勒·帕夫

3
@ @ sativa“该实现会将目标子视图的中心对齐到附加的RecyclerView的中心。如果您打算更改此行为,请重写calculateDistanceToFinalSnap(RecyclerView.LayoutManager,View)。
杰克

如何以编程方式捕捉,因为它不会被捕捉,直到我们轻按或滚动一点,任何变通方法?
NotABot

62

我最终提出了与上述内容略有不同的内容。这不是理想的方法,但是对我来说效果很好,并且可能会对其他人有所帮助。我不会接受这个答案,希望其他人会带来更好和更少hacky的东西(并且可能我误解了RecyclerView实现并缺少一些简单的方法,但是与此同时,这已经足够了用于政府工作!)

实现的基础如下:a中的滚动RecyclerView有点类似于RecyclerView和之间的拆分LinearLayoutManager。我需要处理两种情况:

  1. 用户浏览该视图。默认行为是RecyclerView将弹道传递到内部Scroller,然后内部执行滚动魔术。这是有问题的,因为这样RecyclerView通常会将它们固定在不被捕捉的位置。我通过重写RecyclerView fling()实现来解决此问题,而不是匆忙地将LinearLayoutManager其平滑滚动到一个位置。
  2. 用户以不足的速度抬起手指以启动滚动。在这种情况下不会发生甩动。如果视图不在捕捉位置,我想检测这种情况。我通过重写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;
    }
}

灵活的布局管理器的界面:

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

}

1
好答案!这也是其他自定义滚动行为的良好起点,您还希望自定义滚动/滚动结束位置。唯一的事情是Constant.INCHES_PER_METER不存在,因此我将其设置为39.3700787自己。
Mac_Cain13

+1其实这很好。它可以很容易地扩展通过更换支持静态GridLayouts+ 1以s + getSpanCount()。对于始终具有不同像元大小的动态网格布局,需要花费更多的工作才能计算出该值。谢谢@凯瑟琳!
塞巴斯蒂安·罗斯2014年

感谢您真正有用的课程。我发现,对于较大的recyclerView项(高度为1屏幕大小),向上滚动过于敏感。可以通过更改calcPosForVelocity()方法中的一行来解决此问题:return (int) Math.max(currPos + tempScroll / childSize, 0);return (int) Math.max(currPos + tempScroll / childSize + 2 , 0);
Uwais A

我提到的@UwaisA解决方案也适用于全屏项目。
wblaschko 2015年

1
@ ShahrozKhan91不好意思,老实说-我不明白该类的工作原理,只是发现那是我需要改变并进行一些试验和错误的参数。
Uwais 2015年

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

该逻辑很普通,可以在许多其他情况下使用它。在我的案例中,回收站视图是水平的,并且整个水平宽度都没有边距(基本上,回收站视图的中心X坐标是屏幕的中心)或填充不均匀。

如果有人面临问题,请发表评论。


13

我还需要一个快速的回收站视图。我想让回收者视图项目对齐到列的左侧。最后实现了一个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());

1
很棒的方法,但是在调用smoothScrollBy()之前,您应该检查getScrollDistanceOfColumnClosestToLeft()是否返回非零值,否则将导致无限次onScrollStateChanged(SCROLL_STATE_IDLE)调用。
Mihail Ignatiev

8

这是一个简单的技巧,可在发生事件时将其平滑滚动到某个位置:

@Override
public boolean fling(int velocityX, int velocityY) {

    smoothScrollToPosition(position);
    return super.fling(0, 0);
}

通过调用smoothScrollToPosition(int position)来覆盖fling方法,其中“ int position”是所需视图在适配器中的位置。您将需要以某种方式获得该职位的价值,但这取决于您的需求和实施。


6

与RecyclerView纠缠了一段时间之后,这就是我到目前为止的想法以及我现在正在使用的东西。它有一个小缺陷,但由于您可能不会注意到,所以我不会(尽管)漏掉豆子。

https://gist.github.com/lauw/fc84f7d04f8c54e56d56

它仅支持水平的回收站视图和捕捉到中心,也可以根据视图到中心的距离缩小视图。用作RecyclerView的替代品。

编辑:08/2016使其进入存储库:
https//github.com/lauw/Android-SnappingRecyclerView
我将在保持更好的实现的同时保持这一点。


这个解决方案是最好的,谢谢!:D另外,我找到了你的豆子。
Pkmmte

谢谢!最初显示SnappingRecyclerView时会有一个小的延迟。第一项从左边开始,然后在中间显示第一项。对此有什么解决方案?
wouter88

奇迹般有效!希望将实施方式更改为使用RecyclerView
Akshay Chordiya

@ wouter88,您可以从另一个位置开始,例如:“ mSnappingRecyclerView.getLayoutManager()。scrollToPosition(YOUR_POSITION);”
Ali_dev


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

        }
    });

唯一的小缺点是,当您滚动不到部分可见的单元格的一半时,它不会向后弹跳-但是,如果这不打扰您,那将是一个干净,简单的解决方案。


我认为这不会滚动到中心。它(smoothScrollToPosition)仅将视图带到可见区域。
humblerookie 2015年

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

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

对我来说效果很好,如果有问题请通知我。


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

2
嘿@gomino,很高兴您发现它很方便,但是您建议的填充将减少recyclerview的滚动区域。但是,将填充添加到子级(第一个和最后一个)将可以有效地工作。
humblerookie 2015年

简单的@humblerookie只需在xml布局文件中添加android:clipToPadding="false"并添加android:clipChildren="false"到recyclerview
Gomino 2015年

1

另一个更清洁的选择是使用custom 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()));

您的方法的问题在于,它们都扩展了LinearLayoutManager。如果您需要任何其他LayoutManager怎么办。这是附加滚动侦听器和自定义滚动器的最大优势。您可以将其附加到所需的任何LayoutManager。否则,这将是一个不错的解决方案,您的LayoutManagers干净整洁。
米哈尔ķ

谢谢!您是对的,在许多情况下,使用滚动侦听器可以更加灵活。我选择仅对LinearLayoutManager这样做是因为我没有发现将其用于网格或交错的实际需求,而且我自然发现这种行为是由LayoutManager处理的。
kalin

-1

我需要的东西与以上所有答案略有不同。

主要要求是:

  1. 当用户猛扑或松开手指时,其作用相同。
  2. 使用本机滚动机制具有与常规相同的“感觉” RecyclerView
  3. 停止时,它将开始平滑滚动到最近的捕捉点。
  4. 无需使用customLayoutManagerRecyclerView。只是一个RecyclerView.OnScrollListener,然后由附加recyclerView.addOnScrollListener(snapScrollListener)。这样,代码就更整洁了。

还有两个非常具体的要求,在下面的示例中应易于更改以适合您的情况:

  1. 水平工作。
  2. 将项目的左边缘对齐到中的特定点RecyclerView

此解决方案使用native LinearSmoothScroller。不同之处在于,在最后一步中,找到“目标视图”后,它将更改偏移量的计算,以使其捕捉到特定位置。

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

    }
}

此代码有许多棘手的要点,其中滚动方案将无法正确处理,并且在不同屏幕上的行为也不同。这也与“ RecyclerView”的原理背道而驰,在RecyclerView中,您实际上限制了自定义滚动条的选项。
kalin

1.什么是棘手的场景?我想不出一开始就没有提到的内容(如果您需要处理的话,这很容易处理)。2.它在不同的屏幕上没有不同的行为,是什么让您认为它呢?3.您能否为我提供RecyclerView原理的链接,它说我们应该使用LayoutManager而不是滚动条?附加滚动器是在公共API中,所以我绝对不知道您在说什么。您是否读过代码,因为我认为不是;)
MichałK

0)总的来说,我喜欢您在很多情况下都具有滚动侦听器的想法。1)我不会详细介绍,但是调用平滑快照的onScrollStateChanged将会由平滑的滚动器动作再次调用,这可能导致StackOverflow另一方面,您从不检查会导致严重问题的方向,并考虑在一定范围内进行递归调用堆栈溢出。2)u使用像素来获得更好地使用%的位置,尤其是当用户旋转屏幕时,绝对可以确定不会出现捕捉点。
kalin

3)RV是超级模块化的,这就是为什么它这么好。可以使用附加的SmoothScroller,但是您将其与捕捉紧密链接,从而消除了这种灵活性,它也仅适用于LinearLayoutManager实例,从而消除了您的想法的灵活性。我读了代码:) {干杯}
kalin
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.