在RecyclerView中滚动滚动

Cat*_*ine 69 android

我正在尝试将新RecyclerView类用于我希望组件在滚动时捕捉到特定元素的场景(将旧版Android Gallery作为具有中心锁定项的此类列表的示例).

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

我有一个接口,ISnappyLayoutManager它包含一个方法,getPositionForVelocity它计算视图应该在给定初始fling速度的情况下结束滚动的位置.

public interface ISnappyLayoutManager {
    int getPositionForVelocity(int velocityX, int velocityY);  
}
Run Code Online (Sandbox Code Playgroud)

然后我有一个类,它以这样的方式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;
    }
}
Run Code Online (Sandbox Code Playgroud)

由于几个原因,我对这种方法不是很满意.首先,似乎与"RecyclerView"的哲学相反,它必须将其子类化以实现某种类型的滚动.其次,如果我只想使用默认值LinearLayoutManager,这会变得有点复杂,因为我必须弄乱它的内部,以便了解它的当前滚动状态并精确计算滚动到的位置.最后,这甚至没有处理所有可能的滚动方案,就像你移动列表然后暂停然后举起手指一样,没有发生任何事件(速度太低),所以列表仍然处于中途位置.这可以通过添加一个on scroll状态监听器来处理RecyclerView,但也感觉非常hacky.

我觉得我必须遗漏一些东西.有一个更好的方法吗?

raz*_*zle 70

有了LinearSnapHelper,现在很容易.

你需要做的就是:

SnapHelper helper = new LinearSnapHelper();
helper.attachToRecyclerView(recyclerView);
Run Code Online (Sandbox Code Playgroud)

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

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

compile "com.android.support:recyclerview-v7:24.2.0"
Run Code Online (Sandbox Code Playgroud)

  • 值得注意的是,如果有人遇到与此解决方案相同的问题,那么我做了:如果在设置recyclerview时遇到"IllegalStateException:OnFlingListener的实例已设置",则应调用recyclerView.setOnFlingListener(null); 在snapHelper.attachToRecyclerView(recyclerView)之前; (8认同)
  • 不幸的是,它会捕捉到列表项的中间 (3认同)
  • @sativa"实现将目标子视图的中心捕捉到附加的RecyclerView的中心.如果您打算更改此行为,则覆盖calculateDistanceToFinalSnap(RecyclerView.LayoutManager,View)." (3认同)

Cat*_*ine 59

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

实现的基础是这些:a中的滚动RecyclerView是在RecyclerView和之间分开的LinearLayoutManager.我需要处理两种情况:

  1. 用户将视图翻转.默认行为是RecyclerView将fling传递给内部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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

}
Run Code Online (Sandbox Code Playgroud)

以下是一个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;
    }

}
Run Code Online (Sandbox Code Playgroud)


hum*_*kie 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

逻辑是通用的,可以用于许多其他情况.我的情况是,回收者视图是水平的,并且拉伸整个水平宽度而没有边距(基本上是recyclerview的中心X坐标是屏幕的中心)或不均匀的填充.

任何人都面临着问题的善意评论.


Tho*_* R. 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);
            }
        }
    }

}
Run Code Online (Sandbox Code Playgroud)

快照的计算:

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;
}
Run Code Online (Sandbox Code Playgroud)

如果第一个可见视图滚动超过屏幕的半宽,则下一个可见列向左移动.

设置监听器:

mRecyclerView.addOnScrollListener(new SnapScrollListener());
Run Code Online (Sandbox Code Playgroud)


eDi*_*zle 7

这是一个更简单的黑客,可以平滑滚动到某个事件的某个位置:

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

    smoothScrollToPosition(position);
    return super.fling(0, 0);
}
Run Code Online (Sandbox Code Playgroud)

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


Lau*_*auw 6

在使用了RecyclerView之后,这就是我到目前为止所提出的以及我现在正在使用的内容.它有一个小缺陷,但我不会溢出豆子(还),因为你可能不会注意到.

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

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

编辑:08/2016将其放入存储库:
https://github.com/lauw/Android-SnappingRecyclerView
我会在改进实施的同时保持这一点.


Nat*_*ein 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;
                }

        }
    });
Run Code Online (Sandbox Code Playgroud)

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