ViewPager2 在慢速设备上或调试时无法显示正确的片段

Mic*_*cer 6 android android-fragments android-viewpager2

我正在使用ViewPager2withTabLayout和FragmentStateAdapter在选项卡中显示片段。我发现,当我在较慢的设备(即 Nexus 5X)上运行它时,或者当我在createFragment()方法中放置断点时,在我更改为带有片段的稍后选项卡(尚未创建)后,会显示最后一个位置的片段。

例如我有 20 个项目。我选择位置 15 上的选项卡。位置 15、14、16、17、18、19 和 20 上的片段被创建(createFragment()称为)。我预计只会创建 14、15、16 个。在慢速设备上或在createFragment()方法内部停止调试器时,会显示位置 20 上的片段。

在未减速的设备上不会发生此问题。

主要活动:

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    initViewPager2WithFragments()
}

private fun initViewPager2WithFragments() {
    val viewPager2: ViewPager2 = findViewById(R.id.viewpager)
    val adapter = ExampleStateAdapter(supportFragmentManager, lifecycle)
    viewPager2.adapter = adapter

    val tablayout: TabLayout = findViewById(R.id.tablayout)
    val names: Array<String> = arrayOf(
        "Fragment A",
        "Fragment B",
        "Fragment C",
        "Fragment D",
        "Fragment E",
        "Fragment F",
        "Fragment G",
        "Fragment H",
        "Fragment I",
        "Fragment J",
        "Fragment K",
        "Fragment L",
        "Fragment M",
        "Fragment N"
    )
    TabLayoutMediator(tablayout, viewPager2) { tab, position ->
        tab.text = names[position]
    }.attach()
}
}
Run Code Online (Sandbox Code Playgroud)

状态适配器示例:

class ExampleStateAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) :
    FragmentStateAdapter(fragmentManager, lifecycle) {

    override fun getItemCount(): Int {
        return 14
    }

    override fun createFragment(position: Int): Fragment {
        Log.d("frg", "createFragment: $position")
        return when (position) {
            0 -> SomeFragment.newInstance("A")
            1 -> SomeFragment.newInstance("B")
            2 -> SomeFragment.newInstance("C")
            3 -> SomeFragment.newInstance("D")
            4 -> SomeFragment.newInstance("E")
            5 -> SomeFragment.newInstance("F")
            6 -> SomeFragment.newInstance("G")
            7 -> SomeFragment.newInstance("H")
            8 -> SomeFragment.newInstance("I")
            9 -> SomeFragment.newInstance("J")
            10 -> SomeFragment.newInstance("K")
            11 -> SomeFragment.newInstance("L")
            12 -> SomeFragment.newInstance("M")
            13 -> SomeFragment.newInstance("N")
            else -> SomeFragment.newInstance("0")
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

一些片段:

class SomeFragment : Fragment() {

    var TAG = "frg 0"

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {

        TAG = "frg ${arguments?.getString(CATEGORY)}"
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_some, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        view.findViewById<TextView>(R.id.textView).apply {
            text = arguments?.let {
                it.getString(CATEGORY, "")
            } ?: "not found"
        }
    }

    override fun onStart() {
        Log.d(TAG, "onStart()")
        super.onStart()
    }

    override fun onResume() {
        Log.d(TAG, "onResume()")
        super.onResume()
    }

    override fun onPause() {
        Log.d(TAG, "onPause()")
        super.onPause()
    }

    override fun onStop() {
        Log.d(TAG, "onStop()")
        super.onStop()
    }

    companion object {

        fun newInstance(
            categoryName: String
        ) = SomeFragment().apply {
            arguments = bundleOf(
                CATEGORY to categoryName
            )
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

片段_some.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/ConstraintLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#CDDC39"
    tools:context=".fragments.SomeFragment">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Fragment"
        android:textSize="30sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Run Code Online (Sandbox Code Playgroud)

活动主文件

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">


    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/viewpager"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tablayout" />


    <com.google.android.material.tabs.TabLayout
        android:id="@+id/tablayout"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/btn"
        app:tabMode="scrollable" />

</androidx.constraintlayout.widget.ConstraintLayout>
Run Code Online (Sandbox Code Playgroud)

依赖项:

// ViewPager2
implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01'
// CardView
implementation 'androidx.cardview:cardview:1.0.0'
// Tablayout + other stuff...
implementation 'com.google.android.material:material:1.2.1'
Run Code Online (Sandbox Code Playgroud)

我尝试记录一些日志,看看发生了什么。列表中有 14 个片段,我选择位置 9 上的选项卡。

正常情况(不停止createFragment()方法中的调试器):

2020-09-22 15:10:33.515 26627-26627/com.example.viewpager2_example_2 D/frg: createFragment: 5
2020-09-22 15:10:33.534 26627-26627/com.example.viewpager2_example_2 D/frg F: onStart()
2020-09-22 15:10:33.552 26627-26627/com.example.viewpager2_example_2 D/frg D: onStop()
2020-09-22 15:10:33.573 26627-26627/com.example.viewpager2_example_2 D/frg: createFragment: 6
2020-09-22 15:10:33.587 26627-26627/com.example.viewpager2_example_2 D/frg G: onStart()
2020-09-22 15:10:33.597 26627-26627/com.example.viewpager2_example_2 D/frg: createFragment: 7
2020-09-22 15:10:33.609 26627-26627/com.example.viewpager2_example_2 D/frg H: onStart()
2020-09-22 15:10:33.633 26627-26627/com.example.viewpager2_example_2 D/frg C: onStop()
2020-09-22 15:10:33.636 26627-26627/com.example.viewpager2_example_2 D/frg: createFragment: 8
2020-09-22 15:10:33.647 26627-26627/com.example.viewpager2_example_2 D/frg I: onStart()
2020-09-22 15:10:33.752 26627-26627/com.example.viewpager2_example_2 D/frg B: onStop()
2020-09-22 15:10:33.967 26627-26627/com.example.viewpager2_example_2 D/frg: createFragment: 9
2020-09-22 15:10:33.972 26627-26627/com.example.viewpager2_example_2 D/frg A: onPause()
2020-09-22 15:10:33.973 26627-26627/com.example.viewpager2_example_2 D/frg A: onStop()
2020-09-22 15:10:34.018 26627-26627/com.example.viewpager2_example_2 D/frg F: onStop()
2020-09-22 15:10:34.023 26627-26627/com.example.viewpager2_example_2 D/frg I: onResume()
Run Code Online (Sandbox Code Playgroud)

有问题的场景(createFragment()每次在方法中停止调试器 1-2 秒):

2020-09-22 15:14:26.419 26627-26627/com.example.viewpager2_example_2 D/frg: createFragment: 5
2020-09-22 15:14:28.551 26627-26627/com.example.viewpager2_example_2 D/frg F: onStart()
2020-09-22 15:14:28.577 26627-26627/com.example.viewpager2_example_2 D/frg D: onStop()
2020-09-22 15:14:28.602 26627-26627/com.example.viewpager2_example_2 D/frg: createFragment: 6
2020-09-22 15:14:29.591 26627-26627/com.example.viewpager2_example_2 D/frg G: onStart()
2020-09-22 15:14:29.615 26627-26627/com.example.viewpager2_example_2 D/frg: createFragment: 7
2020-09-22 15:14:30.962 26627-26627/com.example.viewpager2_example_2 D/frg H: onStart()
2020-09-22 15:14:30.999 26627-26627/com.example.viewpager2_example_2 D/frg C: onStop()
2020-09-22 15:14:31.003 26627-26627/com.example.viewpager2_example_2 D/frg B: onStop()
2020-09-22 15:14:31.006 26627-26627/com.example.viewpager2_example_2 D/frg: createFragment: 8
2020-09-22 15:14:31.922 26627-26627/com.example.viewpager2_example_2 D/frg I: onStart()
2020-09-22 15:14:31.956 26627-26627/com.example.viewpager2_example_2 D/frg A: onPause()
2020-09-22 15:14:31.957 26627-26627/com.example.viewpager2_example_2 D/frg A: onStop()
2020-09-22 15:14:31.965 26627-26627/com.example.viewpager2_example_2 D/frg: createFragment: 9
2020-09-22 15:14:33.210 26627-26627/com.example.viewpager2_example_2 D/frg J: onStart()
2020-09-22 15:14:33.240 26627-26627/com.example.viewpager2_example_2 D/frg G: onStop()
2020-09-22 15:14:33.248 26627-26627/com.example.viewpager2_example_2 D/frg: createFragment: 10
2020-09-22 15:14:34.247 26627-26627/com.example.viewpager2_example_2 D/frg K: onStart()
2020-09-22 15:14:34.276 26627-26627/com.example.viewpager2_example_2 D/frg F: onStop()
2020-09-22 15:14:34.280 26627-26627/com.example.viewpager2_example_2 D/frg: createFragment: 11
2020-09-22 15:14:35.286 26627-26627/com.example.viewpager2_example_2 D/frg L: onStart()
2020-09-22 15:14:35.316 26627-26627/com.example.viewpager2_example_2 D/frg H: onStop()
2020-09-22 15:14:35.320 26627-26627/com.example.viewpager2_example_2 D/frg: createFragment: 12
2020-09-22 15:14:36.407 26627-26627/com.example.viewpager2_example_2 D/frg M: onStart()
2020-09-22 15:14:36.435 26627-26627/com.example.viewpager2_example_2 D/frg I: onStop()
2020-09-22 15:14:36.439 26627-26627/com.example.viewpager2_example_2 D/frg: createFragment: 13
2020-09-22 15:14:37.632 26627-26627/com.example.viewpager2_example_2 D/frg N: onStart()
2020-09-22 15:14:37.656 26627-26627/com.example.viewpager2_example_2 D/frg J: onStop()
Run Code Online (Sandbox Code Playgroud)

任何人都可以解释一下里面发生了什么样的巫术ViewPager2吗?

Mar*_*ini 2

我做了一个新的例子,使用:

\n
Android Studio 4.0.1\nBuild #AI-193.6911.18.40.6626763, built on June 25, 2020\nRuntime version: 1.8.0_242-release-1644-b3-6222593 amd64\nVM: OpenJDK 64-Bit Server VM by JetBrains s.r.o\nLinux 5.4.0-7642-generic\n
Run Code Online (Sandbox Code Playgroud)\n

我基本上做了:

\n

文件、新项目、min API 26(这样我就可以在我的 Nexus 5X 上运行),然后选择模板Empty Activity。

\n

然后,我一一添加了您在此处共享的代码,并进行了非常小的更改(对于初学者来说,您的布局包含对id/btn不存在的约束,因此我假设您只粘贴了相关位,请检查我的存储库中的布局 xml)

\n

结果

\n

无论是在不使用附加调试器(但显然是调试版本)时还是在 中放置断点时createFragment(),我仍然观察到预期的行为。

\n

我测试了:

\n
    \n
  • 快速模拟器(具有 API 30 的 Pixel 3a)
  • \n
  • 搭载 Android 8.1 的 Real Nexus 5X
  • \n
\n

我注意到片段在需要时被正确创建(仅当我“滑动”时);这是因为它ExampleStateAdapter是一个子类,FragmentStateAdapter其设计行为与原始类具有相似的行为FragmentStatePagerAdapter,这对于创建“惰性”的大型页面集很有好处。

\n

你可以很容易地测试这个项目,我把它放在GitHub上:

\n

https://github.com/Gryzor/TabbedVp2

\n

结论

\n

您确定您的应用程序没有其他问题导致管理片段时出现不必要的延迟吗?布局中的其他内容可能会导致测量/布局过程花费更长的时间?

\n

日志(过滤依据frg)

\n

新应用程序启动:

\n
D/frg: createFragment: 0\nD/frg\xc2\xa0A: onStart()\nD/frg\xc2\xa0A: onResume()\n
Run Code Online (Sandbox Code Playgroud)\n

一切顺利,第一个片段 (0) 已创建并启动。\n请注意,没有片段 B 是“预先创建的”;这可能是片段适配器的设计选择,没有查看其源代码。从下一次滑动开始,这种行为就会有所不同......

\n

滑动以显示片段 B(并隐藏 A):

\n
1 D/frg: createFragment: 1\n2 D/frg\xc2\xa0B: onStart()\n3 D/frg: createFragment: 2\n4 D/frg\xc2\xa0A: onPause()\n5 D/frg\xc2\xa0B: onResume()\n
Run Code Online (Sandbox Code Playgroud)\n

这里唯一“奇怪”的事情是第 3 行中的调用。createFragment: 2让我们逐行查看:

\n
    \n
  1. 片段 0 是当前片段,下一个片段是“1”,因此调用是正确的。

    \n
  2. \n
  3. 片段 1(“b”)现在正在创建并“启动”。

    \n
  4. \n
  5. 状态适配器正在抢先创建片段“C”(编号 2),但请注意它尚未启动或恢复。\n当我们显示片段 A 时,它没有对片段 B 执行此操作,我认为这是设计使然,所以如果/当您显示第一项时,不会预先创建任何内容;我没有意识到这一点,也没有检查源代码,但这就是它的行为方式。从现在开始,每次创建新片段时,都会在幕后“预先创建”下一个片段,以便准备启动。

    \n
  6. \n
  7. 当片段 B 启动时,片段 B 现在因离开屏幕而暂停。

    \n
  8. \n
  9. 片段 B 最终恢复(在启动之后),因为它现在可见。

    \n
  10. \n
\n

现在滑回到“A”(为了好玩)

\n
D/frg\xc2\xa0B: onPause()\nD/frg\xc2\xa0A: onResume()\n
Run Code Online (Sandbox Code Playgroud)\n

没什么可看的,B 现在已暂停,A 已恢复,因为它已创建并启动。

\n

让我们滑回“B”

\n
D/frg\xc2\xa0A: onPause()\nD/frg\xc2\xa0B: onResume()\n
Run Code Online (Sandbox Code Playgroud)\n

相反的情况发生,这里没有什么可看的。

\n

现在让我们滑动到 C

\n
1 D/frg\xc2\xa0C: onStart()\n2 D/frg: createFragment: 3\n3 D/frg\xc2\xa0B: onPause()\n4 D/frg\xc2\xa0C: onResume()\n
Run Code Online (Sandbox Code Playgroud)\n
    \n
  1. C 是预先创建的,但从未启动,现在已启动。
  2. \n
  3. 同样,适配器正在预先创建下一个(“D”)。
  4. \n
  5. B 即将离开,所以暂停。
  6. \n
  7. 最后 C 恢复并可见。
  8. \n
\n

最终测试,继续滑动至 D...

\n
1 D/frg\xc2\xa0D: onStart()\n2 D/frg: createFragment: 4\n3 D/frg\xc2\xa0A: onStop()\n4 D/frg\xc2\xa0C: onPause()\n5 D/frg\xc2\xa0D: onResume()\n
Run Code Online (Sandbox Code Playgroud)\n

我想达到“D”(或者更确切地说,远离 A)是有原因的:)

\n

让我们看看这些日志

\n
    \n
  1. D 是在上一步“1”中预先创建的,因此现在已启动。
  2. \n
  3. 现在已经预先创建了 E,以防我们再次滑动。
  4. \n
  5. A现在已停止,并且在需要时很容易被销毁。由于我们现在已经超出了默认值(总共 3 个,如果我没记错的话,就是当前的 + 和 - 1 个),因此适配器将“停止”未使用的片段以在需要时回收内存。所以A已经半死了。
  6. \n
  7. 现在我们像往常一样暂停 C,因为它会消失而有利于 D。
  8. \n
  9. ...以及最近开始的简历 D。
  10. \n
\n

此时,你就掌握了流量。如果继续转到“E”,那么片段“B”将会停止:

\n
D/frg\xc2\xa0E: onStart()\nD/frg: createFragment: 5\nD/frg\xc2\xa0B: onStop()\nD/frg\xc2\xa0D: onPause()\nD/frg\xc2\xa0E: onResume()\n
Run Code Online (Sandbox Code Playgroud)\n

照常营业。

\n

更新

\n

我决定再测试一件事,所以我推动了这个提交。它添加了一个按钮,用于选择第 10 个片段(恰好是“K”)。

\n
    \n
  1. 应用程序启动,A可见。
  2. \n
\n
D/frg: createFragment: 0\nD/frg\xc2\xa0A: onStart()\nD/frg\xc2\xa0A: onResume()\n
Run Code Online (Sandbox Code Playgroud)\n
    \n
  1. 按钮被按下,K现在可见(在动画之后)
  2. \n
\n
01 D/frg: createFragment: 7\n02 D/frg\xc2\xa0H: onStart()\n03 D /frg: createFragment: 8\n04 D/frg\xc2\xa0I: onStart()\n05 D/frg: createFragment: 9\n06 D/frg\xc2\xa0J: onStart()\n07 D/frg: createFragment: 10\n08 D/frg\xc2\xa0K: onStart()\n09 D/frg: createFragment: 11\n10 D/frg\xc2\xa0A: onPause()\n11 D/frg\xc2\xa0A: onStop()\n12 D/frg\xc2\xa0H: onStop()\n13 D/frg\xc2\xa0K: onResume()\n
Run Code Online (Sandbox Code Playgroud)\n

我们后面的位置是 10,它实际上是第 11 个元素,因为它是“从零开始的”(所以 A 是零,而不是 1)。

\n

StateAdapter 试图变得聪明(?),并在位置 7 (H)、8 (I)、9 (J)、10 (K) 和 11 (L) 创建 Fragment,如第 1、3、5 行所示, 7、9。

\n

适配器的“智能”愚蠢之处来了:

\n

它一一开始并开始:H 在第 2 行,I 在第 4 行,J 在第 6 行,K 在第 8 行。

\n

然后它停止 A(第 11 行),因为它超出了要维护的状态范围,并且还停止 H,因为由于某种原因它决定先发制人,只是意识到它不再处于状态中。 “范围”,因此 H 也在第 12 行停止。这里效率很低。

\n

最后 K 被“恢复”,因为它......嗯......可见!

\n

这就是我无法解释的奇怪行为开始的地方:\n我向 Google 提出的问题是(如果我们查看状态适配器的源代码,可能很容易回答,但是......现在谁想这样做?) 。

\n
    \n
  • 为什么片段 H 启动然后停止?\n我的理论是逻辑被触发,并且 H 到“K”的距离与“A”到“D”的距离一样远(中间还有另外两个片段),因此为什么后来停止了,但为什么又开始了?
  • \n
  • 为什么片段 I、J(H 和 K 之间的片段)也开始了,但没有停止?
  • \n
\n

我认为这与当您从 C 滑动到 D 时,A 停止,但 B 和 C 没有停止,它们暂停的原因相同。B 已经暂停,当您转到 D 并且 A 停止时,C 也会暂停,但在从 A 到 K 的“跳转”中,这些片段并未暂停,这是一种不同的状态,可能是一个错误。在某种程度上,它们是开始的而不是恢复的,因此这是“暂停状态”,这就是为什么它有效并且“有点一致”。

\n

我本以为至少我和 J 至少会暂停,因为如果你一张一张地滑动,它们就会变成这样,但事实是它们从未恢复,所以没有理由打电话暂停。

\n

最后,在从 A 到 K 的这次大“跳跃”之后,片段处于片段 K 中的预期状态:

\n
    \n
  1. 在“H”之前(包括)没有任何内容被创建(或开始/恢复)。正确的。
  2. \n
  3. 向前(L 及以后)仅预先创建“L”(上一个日志中的第 9 行,其中创建了片段“11”但未启动)。正确的。
  4. \n
  5. 片段 I 和 J 已“启动”并准备就绪,但到目前为止从未恢复(因此它们已暂停)。如果您“返回”至 J,它将恢复(比必须创建/启动更快)。
  6. \n
\n

好吧,你已经“记录”了行为(没有解释,因为我没有进一步挖掘)。

\n

我希望这可以帮助您澄清适配器正在尝试对您的片段和生命周期执行哪些操作(如果有的话)。现在,在较旧的 API 和速度较慢的设备中,整个过程可能需要几毫秒(而在较新的设备上只需很少的毫秒)。

\n

更新二

\n

如果您禁用动画(或者通过TabLayoutMediator您发现的那样,或者那么viewPager.setCurrentItem(nn, false)这个问题就不会发生。这使我相信“​​动画/交易”系统正在被触发,因为“动画”“转到”所选项目”正在构建和执行。

\n

与此同时,谷歌问题已被提交。

\n