如何告诉RecyclerView从特定项位置开始启动

15

我希望使用LinearLayoutManager作为RecyclerView的布局管理器,并在适配器更新后将滚动位置显示在特定项目上,而不是第一个/最后一个位置。 这意味着在第一次(重新)布局时,给定位置应该在可见区域内。 它不应该在顶部放置位置0,然后滚动到目标位置。

我的适配器从itemCount=0开始,在线程中加载其数据并稍后通知其真正的计数。但是,即使计数仍为0,起始位置也必须已经设置!

截至目前,我使用了某种包含scrollToPosition的post Runnable,但这会产生副作用(从第一个位置开始,立即跳转到目标位置(0->目标位置),并且似乎无法与DiffUtil很好地配合使用(0->目标位置->0)

编辑: 为澄清:我需要替代layoutManager.setStackFromEnd(true);的方法,类似于setStackFrom(position)。如果在itemCount仍为0时调用ScrollToPosition,它将被忽略。如果我在通知itemCount现在> 0时调用它,它将从0开始布局,然后短暂停留在目标位置。如果我使用DiffUtil.DiffResult.dispatchUpdatesTo(adapter)。则完全失败(从0开始,然后滚动到目标位置,然后再次返回位置0)

6个回答

12

我自己找到了解决方案:

我扩展了LayoutManager:

  class MyLayoutManager extends LinearLayoutManager {

    private int mPendingTargetPos = -1;
    private int mPendingPosOffset = -1;

    @Override
    public void onLayoutChildren(Recycler recycler, State state) {
        if (mPendingTargetPos != -1 && state.getItemCount() > 0) {
            /*
            Data is present now, we can set the real scroll position
            */
            scrollToPositionWithOffset(mPendingTargetPos, mPendingPosOffset);
            mPendingTargetPos = -1;
            mPendingPosOffset = -1;
        }
        super.onLayoutChildren(recycler, state);
    }

    @Override
    public void onRestoreInstanceState(Parcelable state) {
        /*
        May be needed depending on your implementation.

        Ignore target start position if InstanceState is available (page existed before already, keep position that user scrolled to)
         */
        mPendingTargetPos = -1;
        mPendingPosOffset = -1;
        super.onRestoreInstanceState(state);
    }

    /**
     * Sets a start position that will be used <b>as soon as data is available</b>.
     * May be used if your Adapter starts with itemCount=0 (async data loading) but you need to
     * set the start position already at this time. As soon as itemCount > 0,
     * it will set the scrollPosition, so that given itemPosition is visible.
     * @param position
     * @param offset
     */
    public void setTargetStartPos(int position, int offset) {
        mPendingTargetPos = position;
        mPendingPosOffset = offset;
    }
}

它存储我的目标位置。如果 RecyclerView 调用了 onLayoutChildren,它会检查适配器的 itemCount 是否已经 > 0。如果是 true,则调用 scrollToPositionWithOffset()

因此,我可以立即知道应该显示的位置,但在位置存在于适配器之前,它不会告诉 LayoutManager。


5
您可以尝试这个方法,它可以滚动到您想要的位置:
rv.getLayoutManager().scrollToPosition(positionInTheAdapter).

1
抱歉,这与所描述的情景不符。 - allofmex
这段代码在一切都布局好之后是可以正常工作的,但如果在设置视图时调用它则无法正常工作。RecyclerView 的设置还没有进行到足够的程度来正确执行它,因此它什么也不会做。 - johngray1965

0

对一个长期运行的问题的另一个贡献...

如上所述,layoutManager.scrollToPosition/WithOffset()确实可以使RecyclerView定位,但是时间控制可能会很棘手。例如,对于长度可变的项目视图,RecyclerView必须努力测量所有先前的项目视图。

下面的方法简单地延迟告诉RecyclerView有关先前项目的信息,然后调用notifyItemRangeInserted(0, offset)。这使得视图出现在正确的位置,开始时没有可见滚动,并准备好向后滚动。

private List<Bitmap> bitmaps = new ArrayList<>();

...

private class ViewAdapter extends RecyclerView.Adapter<ViewHolder> {
    private volatile int offset;
    private boolean offsetCancelled;

    ViewAdapter(int initialOffset) {
        this.offset = initialOffset;
        this.offsetCancelled = initialOffset > 0;
    }

    @Override
    public int getItemCount() {
        return bitmaps.size() - offset;
    }

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        ImageView imageView = new ImageView(MyActivity.this);  // Or getContext() from a Fragment

        RecyclerView.LayoutParams lp = new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        imageView.setLayoutParams(lp);

        return new ViewHolder(imageView);
    }

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        holder.imageView.setImageBitmap(bitmaps.get(position + offset));

        if (!offsetCancelled) {
            offsetCancelled = true;
            recyclerView.post(() -> {
                int previousOffset = offset;
                offset = 0;
                notifyItemRangeInserted(0, previousOffset);
                Log.i(TAG, "notifyItemRangeInserted(0, " + previousOffset + ")");
            });
        }
    }
}

private class ViewHolder extends RecyclerView.ViewHolder {
    private final ImageView imageView;

    ViewHolder(@NonNull ImageView itemView) {
        super(itemView);
        this.imageView = itemView;
    }
}

为了更好地理解,这是一个基于ImageView和Bitmap的完整示例。关键在于在getItemCount()onBindViewHolder()中使用偏移量。


0
如果你想要滚动到指定的位置,并且该位置是适配器的位置,那么你可以使用 StaggeredGridLayoutManagerscrollToPosition 方法。
StaggeredGridLayoutManager staggeredGridLayoutManager = new StaggeredGridLayoutManager(1, StaggeredGridLayoutManager.VERTICAL);
       staggeredGridLayoutManager.scrollToPosition(10);
       recyclerView.setLayoutManager(staggeredGridLayoutManager);

如果我理解正确的话,您想滚动到特定位置,但该位置是适配器的位置而不是RecyclerView的项位置。
您只能通过LayoutManager来实现这一点。
rv.getLayoutManager().scrollToPosition(youPositionInTheAdapter);

谢谢。但我不想滚动,我想在适配器中的myPosition可用时立即开始 - allofmex
所以,您需要设置您想要的位置。@allofmex - Prince Dholakiya
但是在我需要设置位置的时候,这个位置在适配器中还不存在。(但我知道当数据加载完成后它很快就会存在) - allofmex
所以,我的建议是,在您的数据完全加载之前,可以显示加载视图,然后设置滚动到指定位置。 - Prince Dholakiya

0
如果你只是想从特定位置开始recyclerView,而不带有任何滚动动画的话,我建议使用StaggeredGridLayoutManager。
val staggeredGridLayoutManager = StaggeredGridLayoutManager(1, StaggeredGridLayoutManager.HORIZONTAL)//or VERTICAL
            staggeredGridLayoutManager.scrollToPosition(specificPosition)

recyclerView.apply{
    layoutManager = staggeredGridLayoutManager
}

0

以上任何一种方法都对我无效。我使用ViewTreeObserver完成了以下操作,一旦它的子项添加/更改可见性即会触发。

recyclerView.apply {
   adapter = ...
   layoutManager = ...

   val itemCount = adapter?.itemCount ?: 0
   if(itemCount > 1) {
      viewTreeObserver.addOnGlobalLayoutListener(object: ViewTreeObserver.OnGlobalLayoutListener {
      override fun onGlobalLayout() {
         viewTreeObserver.removeOnGlobalLayoutListener(this)
         (layoutManager as? LinearLayoutManager)?.scrollToPosition(#PositionToStartAt)
      }
   }
}

可以将#PositionToStartAt设置为特定值。您还可以确保在布局了特定数量的子项后触发RecyclerView的初始位置设置,以确保其正确设置。

if(recyclerView.childCount() > #PositionCheck) {
   viewTreeObserver.removeOnGlobalLayoutListener(this)
   (layoutManager as? LinearLayoutManager)?.scrollToPosition(#PositionToStartAt)
}

2
但是这样会再次在错误的位置(位置0)进行布局,然后滚动到目标位置。我想要的是在第一个(子)布局中立即获得正确的位置! - allofmex
您不能滚动到尚不存在的子项或位置... 这就像试图在一个空数组中检索第2个位置一样。我建议您可以使用View.INVISIBLE将RecyclerView设置为不可见,触发滚动,然后再使用View.VISIBLE。如果您想要更加优雅,还可以使用ALPHA = 0f,然后使用淡入淡出的动画效果将其动画化至ALPHA = 1f。 - Daniel Kim

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接