Android ViewPager在onResume后setCurrentItem无效

47
我遇到了一个奇怪的问题,ViewPagersetCurrentItem(position, false)方法一开始运行得很好,但是当我切换到另一个活动后,再回到这个活动时,ViewPager总是最终停留在第一个项目上。即使我将setCurrentItem添加到onResume方法中,它仍然会忽略它。当我尝试将项目设置为超出边界索引时,甚至没有抛出任何异常。 但是之后当我调用这个方法,当“下一个”按钮被点击时,它就像预期的那样工作。 我已经检查了我的代码10次,以寻找是否有任何可能调用setCurrentItem(0)或类似的东西,但根本没有相关代码。

1
任何代码片段都可以。 - imthegiga
1
我曾经遇到过类似的问题,我只是将设置当前项的代码移动到了设置适配器之后。我的意思是确保你先设置适配器,然后再设置位置。 - hazimdikenli
19个回答

95

我无法确切地回答为什么会出现这种情况,但是如果您将setCurrentItem调用延迟几毫秒,它应该可以正常工作。我猜测可能是因为在onResume期间还没有进行渲染,而ViewPager需要一次渲染或类似的操作。

private ViewPager viewPager;

@Override
public void onResume() {
    final int pos = 3;
    viewPager.postDelayed(new Runnable() {

        @Override
        public void run() {
            viewPager.setCurrentItem(pos);
        }
    }, 100);
}

更新:故事时间

今天我遇到了一个问题,ViewPager忽略了我的setCurrentItem操作,于是我在stackoverflow上寻找解决方案。我找到了一个与我相同问题的人以及一个解决方法;我实现了这个解决方法,但它不起作用。哇!回到stackoverflow去给那个虚假的解决方案提供者点踩,然后...

原来是我自己。我实现了自己错误的非解决方案,这是我第一次遇到这个问题时想出来的(后来被遗忘了)。现在我必须给自己点踩,因为我提供了错误的信息。


我的最初的“修复”之所以有效,并不是因为“渲染通道”的问题,而是因为Pager的内容受一个Spinner控制。Spinner和Pager的状态都在onResume恢复,因此在下一个事件传播周期中调用Spinner的onItemSelected监听器,从而重新填充ViewPager - 这次使用不同的默认值。
在初始状态恢复期间删除并重置监听器可以解决该问题。

上述修复方法之所以有点用,是因为它在onItemSelected事件触发之后设置了Pager的当前位置。后来,由于一些原因(可能是应用程序变得太慢了 - 在我的实现中,我没有使用100ms,而是使用10ms),它不再起作用。然后,在清理周期中删除了postDelayed,因为它并未改变已有的错误行为。

更新2:我无法点踩自己的帖子。我想,光荣切腹是唯一剩下的选择。


你说“分页器的内容受到 spinner 的控制”,是指 ViewPager 的底层实现,还是表示 ViewPager 中每个页面的内容都包含一个 spinner?即使我在 onResume() 中首先调用 super.onResume(),仍然出现这个 bug。通过这样做,视图状态应该被恢复,我不应该再遇到上述描述的问题。 - Andrew Orobator
1
不,我的意思是:根据Spinner中选择的项目,ViewPager(即适配器)将填充不同的项目。在onResume中,Spinner和Pager都被重新初始化,并且Spinner的onItemSelected回调用于更新ViewPager“覆盖”了正确设置的ViewPager位置。这是我的错,ViewPager正常工作。 - stefs
1
就像我所说的,对我来说问题完全不同,即调用setCurrentItem()两次。我猜在你的情况下也是一样的。 - stefs
1
@user3560827,我相信你要么在之后再次调用了setCurrentItem,要么分配了一个新的适配器,这可能是由于某些生命周期问题或者在一些异步操作之后。不要依赖postDelayed,它是一种hack技巧,实际上是技术债务。你的问题在其他地方! - stefs
3
哇,那个故事值得被Netflix改编。 - Ilya E
显示剩余10条评论

34

在我的Activity的OnCreate中遇到了类似的问题。适配器已经正确设置了计数,并且我在将适配器设置为ViewPager之后应用了setCurrentItem,但是它会返回索引越界。我认为在我设置当前项时ViewPager尚未加载所有的Fragment。通过在ViewPager上发布一个可运行的Runnable,我能够解决这个问题。以下是带有一些上下文的示例。

    // Locate the viewpager in activity_main.xml
    final ViewPager viewPager = (ViewPager) findViewById(R.id.pager);

    // Set the ViewPagerAdapter into ViewPager
    viewPager.setAdapter(new ViewPagerAdapter(getSupportFragmentManager()));

    viewPager.setOffscreenPageLimit(2);

    viewPager.post(new Runnable() {
        @Override
        public void run() {
            viewPager.setCurrentItem(ViewPagerAdapter.CENTER_PAGE);
        }
    });

@TylerPfaff 这对我也起作用了。显然,ViewPager会忽略“早期进入活动”的指令,因为它还不知道要渲染什么。 - Chisko
1
@TylerPfaff 我认为这与 ViewPager 及其子项何时被测量有关。它似乎会忽略 setCurrentItem,直到测量完成。ViewPager 上发布的可运行项保证会在测量之后执行。 - Martin Price

18

ViewTreeObserver 可用于避免静态延迟。

Kotlin:

可以使用 Kotlin 扩展作为简洁选项。

view_pager.doOnPreDraw {
    view_pager.currentItem = 1
}
请确保您有Gradle依赖项:implementation 'androidx.core:core-ktx:1.3.2'或更高版本。 Java
OneShotPreDrawListener.add(view_pager, () -> view_pager.currentItem = 1);

1
很棒的答案 - 在viewpager2中唯一有效的解决方案! - Henrique Vasconcellos
1
你救了我的一天。谢谢。ViewPager 2 工作正常。 - Arda Kazancı

17

我找到了一个非常简单的解决方法:

    if (mViewPager.getAdapter() != null)
        mViewPager.setAdapter(null);
    mViewPager.setAdapter(mPagerAdapter);
    mViewPager.setCurrentItem(desiredPos);

如果那不起作用,您可以将其放在处理程序中,但没有必要进行定时延迟:

        new Handler().post(new Runnable() {
            @Override
            public void run() {
                mViewPager.setCurrentItem(desiredPos);
            }
        });

3
谢谢,第二种方法解决了我的问题。 - Lazy

6
在现代的 FragmentActivity 中,一种常见的做法是在以Dispatchers.Main 为上下文的协程中调用ViewPager.setcurrentItem(Int)函数:
lifecycleScope.launch(Dispatchers.Main) {
   val index = 1
   viewPager.setCurrentItem(index)
}

无法工作。似乎当调用设置当前项时,该片段尚未准备好。 java.lang.IllegalArgumentException: 未找到ID为0x7f09021d的视图。 - Gean Carlos Brandão

5

我曾在代码中遇到类似的错误,问题在于我在修改数据之前设置了位置。

解决方法很简单,只需在修改数据后设置位置并通知数据已更改即可。

notifyDataSetChanged()
setCurrentItem()

ViewPager2 在 notifyDataSetChanged() 后调用 setCurrentItem(1, true) 不起作用! - lihansey

4
我有相同的问题,我进行了编辑。
@Override
public int getCount() { return NUM_PAGES; }

我错误地将 NUM_PAGES 设置为了 1。


那也是我的问题...谢谢! - Dani

4

我使用了这里描述的post()方法,并且在某些情况下它确实很好用,但由于我的数据来自服务器,它并不是完美的解决方案。

我的问题是我想要有

notifyDataSetChanged

在任意时间调用并切换到我的viewPager的标签页。因此,在通知调用之后,我有以下操作:

ViewUtilities.waitForLayout(myViewPager, new Runnable() {
    @Override
    public void run() {
        myViewPager.setCurrentItem(tabIndex , false);
    }
});

并且

public final class ViewUtilities {
    public static void waitForLayout(final View view, final Runnable runnable) {
        view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            //noinspection deprecation
                view.getViewTreeObserver().removeGlobalOnLayoutListener(this);

                runnable.run();
            }
        });
    }
}

趣闻:结尾处的//noinspection deprecation 是因为在 API 16 之后修复了 API 中的拼写错误。

removeOnGlobalLayoutListener
       ^^
      ON Global

替代

removeGlobalOnLayoutListener
            ^^
            ON Layout

对我来说,这似乎覆盖了所有情况。


4

这里有人在论坛上写道:https://code.i-harness.com/en/q/126bff9 对我很有效。

 if (mViewPager.getAdapter() != null)
    mViewPager.setAdapter(null);
mViewPager.setAdapter(mPagerAdapter);
mViewPager.setCurrentItem(desiredPos);

1
你是最棒的。这最终起作用了。我正在从Volley JSON请求中获取数据。视图最初是空的。我尝试了很多解决方案,但都没有起作用。但是你的就像魔法一样起作用了。谢谢。 - MindRoasterMir

4
以下是与ViewModel等相关的Kotlin解决方案,用于在ActivityonCreate中设置当前项,而无需使用hackyRunnable“解决方案”:
class MyActivity : AppCompatActivity() {
    lateinit var mAdapter: MyAdapter
    lateinit var mPager: ViewPager
    // ...

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.fragment_pager)
        // ...
        mainViewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
        mAdapter = MyAdapter(supportFragmentManager)
        mPager = findViewById(R.id.pager)

        mainViewModel.someData.observe(this, Observer { items ->
            items?.let {
                // first give the data to the adapter
                // this is where the notifyDataSetChanged() happens
                mAdapter.setItems(it) 
                mPager.adapter = mAdapter // assign adapter to pager
                mPager.currentItem = idx // finally set the current page
            }
        })

这将显然按照正确的操作顺序进行,无需使用Runnable或延迟等技巧。

为了完整起见,在此案例中,您通常会像这样实现适配器(在本例中是FragmentStatePagerAdapter)的setItems()

internal fun setItems(items: List<Item>) {
    this.items = items
    notifyDataSetChanged()
}

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