如何告诉RecyclerView在特定项目的位置开始

How to tell RecyclerView to start at specific item position


问题

我想让我的带有LinearLayoutManager的RecyclerView在适配器更新后在特定的项目上显示滚动位置。(不是第一个/最后一个位置) 意味着在第一次(重新)布局时,这个给定的位置应该是在可见区域。 它不应该以0的位置在上面布局,然后再滚动到目标位置。

我的适配器以 itemCount=0 开始,在线程中加载其数据,并在稍后通知其真实计数。但是,当计数还是0的时候,开始的位置必须已经被设置好了。

到目前为止,我使用了某种包含 scrollToPositionpost Runnable ,但这有副作用(从第一个位置开始,并立即跳到目标位置(0 -> target),而且似乎与DiffUtil(0 -> target -> 0)不能很好地工作)

编辑: 为了明确:我需要替代 layoutManager.setStackFromEnd(true); ,类似 setStackFrom(position) 。如果我在itemCount还是0的时候调用它,ScrollToPosition不起作用,所以它被忽略了。如果我在通知itemCount现在是>0的时候调用它,它就会从0开始布局,然后短时跳到目标位置。如果我使用DiffUtil.DiffResult.dispatchUpdatesTo(adapter)`,就会完全失败。(从0开始显示,然后滚动到目标位置,然后再次回到0的位置)

答案1

我自己找到了一个解决方案:

我扩展了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;
    }
}

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

所以我可以立即告诉你什么位置应该是可见的,但是在Adapter中存在位置之前,它不会被告诉LayoutManager。

答案2

你可以试试这个,它将滚动到你想要的位置:

rv.getLayoutManager().scrollToPosition(positionInTheAdapter).
对不起,这不符合描述的情况。
答案3

如果你想滚动到一个特定的位置,并且这个位置是适配器的位置,那么你可以使用 StaggeredGridLayoutManager scrollToPosition

StaggeredGridLayoutManager staggeredGridLayoutManager = new StaggeredGridLayoutManager(1, StaggeredGridLayoutManager.VERTICAL);
       staggeredGridLayoutManager.scrollToPosition  
; recyclerView.setLayoutManager(staggeredGridLayoutManager);

如果我理解这个问题,你想滚动到一个特定的位置,但是这个位置是适配器的位置,而不是RecyclerView的项目位置。

你只能通过 LayoutManager 实现这个目的。

rv.getLayoutManager().scrollToPosition(youPositionInTheAdapter);
谢谢。但是我不想滚动,我想 s 在myPosition的位置上 ,只要这个位置在适配器中可用。
所以,你需要设置你想要的位置。@allofmex
但是在我需要设置位置的时候,这个位置还不存在于适配器中。(但我知道当数据加载完成后,它很快就会存在)
答案4

上面的方法对我都不起作用。我使用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)
}
但是这又会在错误的位置(位置0)布局一次,之后再滚动到目标位置。我想要的是在第一次(子)布局时就有正确的位置!
你不能滚动到一个还不存在的孩子或位置...... 这就像在一个空的数组中试图检索位置2一样。我想说的是,你可以用View.INVISIBLE使你的RecyclerView隐身,触发滚动,然后用View.VISIBLE。如果你想让它变得优雅,你可以用ALPHA=0f来代替,然后用Fade将其动画化为ALPHA=1f。
答案5

如果你的唯一动机是在没有任何 s 类似于滚动的动画 的情况下从一个特定的位置启动recyclerView,我建议使用 StaggeredGridLayoutManager

val staggeredGridLayoutManager = StaggeredGridLayoutManager(1, StaggeredGridLayoutManager.HORIZONTAL)//or VERTICAL
            staggeredGridLayoutManager.scrollToPosition(specificPosition)

recyclerView.apply{
    layoutManager = staggeredGridLayoutManager
}
答案6

对一个长期存在的问题的另一个贡献......

如前所述, 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() 中使用了偏移量。