RecyclerView - 滚动到指定位置不总是有效

45

我已经实现了一个横向滚动的RecyclerView。我的RecyclerView使用LinearLayoutManager,我遇到的问题是,当我尝试使用scrollToPosition(position)smoothScrollToPosition(position)或从LinearLayoutManagerscrollToPositionWithOffset(position)时,它们都不适用于我。要么滚动调用无法滚动到所需位置,要么它不会调用OnScrollListener

到目前为止,我尝试了很多不同的代码组合,无法在这里全部张贴。以下是对我有用(但仅部分)的代码:

public void smoothUserScrollTo(final int position) {

    if (position < 0 || position > getAdapter().getItemCount()) {
        Log.e(TAG, "An attempt to scroll out of adapter size has been stopped.");
        return;
    }

    if (getLayoutManager() == null) {
        Log.e(TAG, "Cannot scroll to position a LayoutManager is not set. " +
                "Call setLayoutManager with a non-null layout.");
        return;
    }

    if (getChildAdapterPosition(getCenterView()) == position) {
        return;
    }

    stopScroll();

    scrollToPosition(position);

    if (lastScrollPosition == position) {

        addOnLayoutChangeListener(new OnLayoutChangeListener() {
            @Override
            public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {

                if (left == oldLeft && right == oldRight && top == oldTop && bottom == oldBottom) {
                    removeOnLayoutChangeListener(this);

                    updateViews();

                    // removing the following line causes a position - 3 effect.
                    scrollToView(getChildAt(0));
                }
            }
        });
    }

    lastScrollPosition = position;
}

@Override
public void scrollToPosition(int position) {
    if (position < 0 || position > getAdapter().getItemCount()) {
        Log.e(TAG, "An attempt to scroll out of adapter size has been stopped.");
        return;
    }

    if (getLayoutManager() == null) {
        Log.e(TAG, "Cannot scroll to position a LayoutManager is not set. " +
                "Call setLayoutManager with a non-null layout.");
        return;
    }

//      stopScroll();

        ((LinearLayoutManager) getLayoutManager()).scrollToPositionWithOffset(position, 0);
//        getLayoutManager().scrollToPosition(position);
    }

由于这个情况,我选择了scrollToPositionWithOffset(),但是由于我使用的是LinearLayoutManager而不是GridLayoutManager,所以情况可能不同。但是这个解决方案对我也有效,但是如我之前所说,只部分有效。

  • 当从第0个位置到totalSize-7时进行滚动工作得很好。
  • 当从totalSize-7滚动到totalSize-3时,第一次我只能滚动到列表中的倒数第七项。然而第二次我可以正常滚动。
  • 当从totalSize-3滚动到totalSize时,我开始出现意外行为。

如果有人找到解决方法,我会很感激。这是我自定义的ReyclerView代码的要点

19个回答

62

几周前我也遇到了同样的问题,只找到了一个非常糟糕的解决方法。必须使用200-300毫秒的postDelayed

new Handler().postDelayed(new Runnable() {
    @Override
    public void run() {
        yourList.scrollToPosition(position);
    }
}, 200);

如果您找到了更好的解决方案,请告诉我!祝您好运!


2
虽然我也认为这个解决方案很粗糙,但目前这是唯一可行的解决方案:\。无论如何还是谢谢! - aldok
2
这是唯一有效的解决方案。 我首先调用了 chatAdapter.notifyDataSetChanged() ; 然后使用 new Handler().postDelayed(new Runnable() {……} , 200); - Däñish Shärmà
2
似乎有些东西与OnCreate()并行运行,会重置滚动位置。如果在没有延迟的情况下在OnCreate()、OnResume()或OnStart()中设置滚动位置,它将被这个神秘的并行进程覆盖。我在调试模式下尝试了这个方法,并在scrolltoposition()方法之前和之后逐步执行代码,结果正常,可能是因为通过逐行运行代码,我引入了一个延迟,让这个神秘的并行进程先完成,然后再运行scrolltoposition()方法。我认为这就是这里给出的答案有效的原因。 - Arash Fotouhi
3
我找到的解决方案是,在UI线程上运行调用setAdapter(使用相同的适配器),接着使用notifyDataSetChange,然后从我正在使用的后台线程中调用myRecycleView.post(...),里面包含了scrollToPosition。这感觉像巫术,但对我有效。 - evenro
10
建议使用myRecycleView.post(() -> myRecycleView.smoothScrollToPosition(position)),它能更顺畅地滚动至目标位置。 - Sp4Rx
显示剩余5条评论

19

被采纳的答案可以使用,但也可能出现问题。这个问题的主要原因是当您请求滚动时,回收站视图可能尚未准备好。解决方案是等待回收站视图准备好后再滚动。幸运的是,Android提供了一种这样的选项。下面的解决方案适用于Kotlin,您也可以尝试Java的替代方法,它也可以工作。

newsRecyclerView.post {
    layoutManager?.scrollToPosition(viewModel.selectedItemPosition)
}
每个视图元素都可以使用post runnable方法,在视图准备就绪时执行,确保代码在需要的时候准确执行。

每个视图元素都有一个post runnable方法,当视图准备好后就会执行,从而确保代码在需要时得到准确执行。


18

结果发现我遇到了类似的问题,直到我使用了

myRecyclerview.scrollToPosition(objectlist.size()-1)

当只输入对象列表大小时,它将始终保持在顶部。这是直到我决定将大小设置为一个变量。再次尝试后,仍然没有起作用。然后我假设可能处理了一个未告知我的越界异常。所以我将其减去1。然后它就起作用了。


2
我一直在想为什么 smoothScrollToPosition() 能够正常工作,但 scrollToPosition() 却不能,这就是答案!平滑滚动不会在意我是否试图滚动超出列表的长度,因此它可以正常工作。非常感谢,否则我永远不会注意到这个问题! - mpellegr

9

我的问题是我在一个NestedScrollView中使用了一个RecyclerView。花了一些时间才发现这是问题所在。解决方法是(Kotlin):

val childY = recycler_view.y + recycler_view.getChildAt(position).y
nested_scrollview.smoothScrollTo(0, childY.toInt())

Java(感谢Himagi提供 https://dev59.com/9ek5XIcBkEYKwwoY7eME#50367883

float y = recyclerView.getY() + recyclerView.getChildAt(selectedPosition).getY();    
scrollView.smoothScrollTo(0, (int) y);

关键是将嵌套的滚动视图滚动到Y,而不是RecyclerView。这在Android 5.0三星J5和搭载Android 9的华为P30 Pro上运行良好。


9

您可以使用LinearSmoothScroller,这在我的情况下每次都有效:

  1. 首先创建一个LinearSmoothScroller的实例:
  LinearSmoothScroller smoothScroller=new LinearSmoothScroller(activity){
            @Override
            protected int getVerticalSnapPreference() {
                return LinearSmoothScroller.SNAP_TO_START;
            }
        };
  1. 当您想要滚动RecyclerView到任何位置时,请执行以下操作:
smoothScroller.setTargetPosition(pos);  // pos on which item you want to scroll recycler view
recyclerView.getLayoutManager().startSmoothScroll(smoothScroller);
完成。

更平滑的滚动对于长列表来说并不是一个好主意。让我们以500个或更多项目的列表为例。你真的想要从位置0平滑地滚动到499吗? - portfoliobuilder

8

我也曾遇到类似的问题(当列表正在更新时必须向上滚动),但以上选项都没有完全解决。

然而,最终我在 https://dev.to/aldok/how-to-scroll-recyclerview-to-a-certain-position-5ck4 找到了一个可行的解决方案 归档链接

概要

scrollToPosition 似乎只有在底层数据集准备好后才能工作。因此postDelay可以工作(随机),但它依赖于设备/应用程序的速度。如果超时时间太短,则会失败。只有在适配器不忙时,smoothScrollToPosition才能正常工作。

为了观察数据集何时准备好,可以添加AdapterDataObserver并覆盖某些方法。
解决我的问题的代码:
adapter.registerAdapterDataObserver( object : RecyclerView.AdapterDataObserver() {
    override fun onItemRangeInserted(
        positionStart: Int,
        itemCount: Int
    ) {
        // This will scroll to the top when new data was inserted
        recyclerView.scrollToPosition(0)
    }
}

1
这个解决方案实际上很有效,post或者doOnNextLayout有时候工作不稳定,当已经滚动到底部并且添加视图到底部时根本不起作用。好发现! - Rowan Berry

5

对我来说,似乎没有任何方法有效。只有以下这行代码有效。

((LinearLayoutManager)mRecyclerView.getLayoutManager()).scrollToPositionWithOffset(adapter.currentPosition(),200);

第二个参数是偏移量,实际上是项视图的起始边缘和 RecyclerView 的起始边缘之间的距离(以像素为单位)。我已经提供了一个恒定的值,使顶部的项目也可见。
查看更多参考这里

4

在 Fragment 或 Activity 中使用 Kotlin 协程,同时使用 lifecycleScope,因为在此范围内启动的任何协程都将在生命周期销毁时被取消。

  lifecycleScope.launch {
                delay(100)
                recyclerView.scrollToPosition(0)

2

这对我很有帮助

Handler().postDelayed({
                    (recyclerView.getLayoutManager() as LinearLayoutManager).scrollToPositionWithOffset( 0, 0)

}, 100)

1
我在创建循环/圆形适配器时遇到了同样的问题,当位置初始化为0时,我只能向下滚动而不能向上滚动。我最初考虑使用Robert的方法,但这种方法不够可靠,因为处理程序只触发一次,如果我运气不好,在某些情况下位置就不会被初始化。
为了解决这个问题,我创建了一个间隔Observable,每隔XXX时间检查一次是否成功初始化,然后将其处理掉。对于我的用例来说,这种方法非常可靠。
private fun initialisePositionToAllowBidirectionalScrolling(layoutManager: LinearLayoutManager, realItemCount: Int) {
        val compositeDisposable = CompositeDisposable() // Added here for clarity, make this into a private global variable and clear in onDetach()/onPause() in case auto-disposal wouldn't ever occur here
        val initPosition = realItemCount * 1000

        Observable.interval(INIT_DELAY_MS, TimeUnit.MILLISECONDS)
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe ({
                    if (layoutManager.findFirstVisibleItemPosition() == 0) {
                        layoutManager.scrollToPositionWithOffset(initPosition, 0)

                        if (layoutManager.findFirstCompletelyVisibleItemPosition() == initPosition) {
                            Timber.d("Adapter initialised, setting position to $initPosition and disposing interval subscription!")
                            compositeDisposable.clear()
                        }
                    }
                }, {
                    Timber.e("Failed to initialise position!\n$it")
                    compositeDisposable.clear()
                }).let { compositeDisposable.add(it) }
    }

点赞。似乎比仅仅希望200毫秒更好的选择。 - ror

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