【问题标题】:How to tell RecyclerView to start at specific item position如何告诉 RecyclerView 从特定项目位置开始
【发布时间】:2019-01-01 03:32:41
【问题描述】:

我希望我的带有 LinearLayoutManager 的 RecyclerView 在适配器更新后显示特定项目的滚动位置。 (不是第一个/最后一个位置) 表示第一次(重新)布局,这个给定的位置应该在可见区域。 它不应将位置 0 放在顶部并随后滚动到目标位置。

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

到目前为止,我使用了某种 post Runnable 包含scrollToPosition 但这有副作用(从第一个 pos 开始并立即跳转到目标位置(0 -> 目标)并且似乎不适用于 DiffUtil(0 -> 目标 -> 0))

编辑: 澄清:我需要layoutManager.setStackFromEnd(true); 的替代品,例如setStackFrom(position)。 ScrollToPosition 不起作用,如果我在 itemCount 仍然为 0 时调用它,那么它会被忽略。如果我在通知 itemCount 现在 > 0 时调用它,它将从 0 开始布局并在目标位置之后快速跳转。如果我使用 DiffUtil.DiffResult.dispatchUpdatesTo(adapter)`,它会完全失败。 (从 0 开始显示,然后滚动到目标位置,然后再次回到位置 0)

【问题讨论】:

    标签: android android-recyclerview position


    【解决方案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()

    所以我可以立即知道什么位置应该是可见的,但在位置存在于适配器之前不会告诉 LayoutManager。

    【讨论】:

      【解决方案2】:

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

      rv.getLayoutManager().scrollToPosition(positionInTheAdapter).
      

      【讨论】:

      • 对不起,这与描述的场景不匹配。
      【解决方案3】:

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

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

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

      您只能通过LayoutManager 实现此目的。

      rv.getLayoutManager().scrollToPosition(youPositionInTheAdapter);
      

      【讨论】:

      • 谢谢。但我不想滚动,我想在适配器中的这个位置可用时开始在 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】:

      如果您的唯一动机是从特定位置启动 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()中偏移量的使用。

        【讨论】:

          猜你喜欢
          • 2015-03-20
          • 1970-01-01
          • 1970-01-01
          • 2023-03-28
          • 1970-01-01
          • 1970-01-01
          • 2019-10-27
          • 1970-01-01
          • 2015-10-02
          相关资源
          最近更新 更多