使用ViewPager2获取当前片段

106

我正在将我的 ViewPager 迁移到 ViewPager2,因为后者应该解决前者的所有问题。不幸的是,当与 FragmentStateAdapter 一起使用时,我找不到任何方法来获取当前显示的片段。

viewPager.getCurrentItem() 给出当前显示的索引,adapter.getItem(index) 通常为当前索引创建一个新的 Fragment。除非在 getItem() 中保留对于所有创建的片段的引用,否则我不知道如何访问当前显示的片段。

对于旧的 ViewPager,一个解决方案是调用 adapter.instantiateItem(index),它将返回所需索引处的片段。

我是否遗漏了 ViewPager2 的某些功能?


1
你解决了你的问题吗? - Aditi
还没有解决方案。 - guillaume-tgl
1
fragmentManager.findFragmentById(getItemId(index).toInt()) from within the FragmentStateAdapter seems to work but only after the fragment has been added. From the docs this may not work though if the fragments are moved. Default implementation works for collections that don't add, move, remove items - Markymark
4
给未来的读者一个一般性的提示(我知道这不是你的问题)。如果你正在寻找一种通知片段已被显示的方法,你可以使用一个ViewModel,并使每个片段观察同一个ViewModel。然后,如果它们的“javaClass.name”与传递的内容匹配,那么每个片段都会做出响应。 - Markymark
可行的解决方案 https://dev59.com/P1MI5IYBdhLWcg3wn9D6#69440532 - Jaydipsinh Zala
18个回答

110

ViewPager2 默认会给 Fragment 分配标签,比如:

第一个位置的 Fragment 的标签是 "f0"

第二个位置的 Fragment 的标签是 "f1"

第三个位置的 Fragment 的标签是 "f2",以此类推…… 因此你可以获取你的 Fragment 的标签,并将 "f" 和你的 Fragment 位置连接起来。要获取当前 Fragment,您可以从 ViewPager2 位置获取当前位置并像这样创建您的标签(对于 Kotlin):

val myFragment = supportFragmentManager.findFragmentByTag("f" + viewpager.currentItem)
在特定位置的片段
val myFragment = supportFragmentManager.findFragmentByTag("f" + position)

如果您使用此技术,可以转换 Fragment 并始终检查其是否不为 null。

如果您将 ViewPager2 托管在 Fragment 中,请改用 childFragmentManager

请记住:

如果您已经 重写 了适配器中的 getItemId(position: Int) 方法,则情况有所不同。它应该是:

val myFragment = supportFragmentManager.findFragmentByTag("f" + your_id_at_that_position)

或者简单来说:

val myFragment = supportFragmentManager.findFragmentByTag("f" + adapter.getItemId(position))
如果您将ViewPager2放在Fragment中,则使用childFragmentManager而不是supportFragmentManager

56
如果明天他们决定更改标签,会怎样呢? - Cyph3rCod3r
4
你是如何确定这个0.1%的数字的? - Cyph3rCod3r
7
@Dr.aNdRO 是正确的,内部命名方案不是API的一部分,因此它可能会随时更改,原因也可能是任何的,并且没有任何警告。这是一个可行的解决方案,但是很脆弱,不能依赖它,如果这对你很重要,你需要意识到风险。 - cactustictacs
3
如果你有多个视图翻页器怎么办?我一点也不喜欢这个解决方案。 - Josef Grunig
4
不,它不是那样工作的。ViewPager的命名惯例将已经在您的PlayStore APK中。因此,除非更改ViewPager2库,否则无需更改您的代码。@RafaelLima - Xenolion
显示剩余8条评论

24

对我来说,通过标签查找当前片段的解决方案似乎最合适。 我为此创建了这些扩展函数:

fun ViewPager2.findCurrentFragment(fragmentManager: FragmentManager): Fragment? {
    return fragmentManager.findFragmentByTag("f$currentItem")
}

fun ViewPager2.findFragmentAtPosition(
    fragmentManager: FragmentManager,
    position: Int
): Fragment? {
    return fragmentManager.findFragmentByTag("f$position")
}
  • 如果您的 ViewPager2 所属宿主是 Activity,请使用 supportFragmentManagerfragmentManager
  • 如果您的 ViewPager2 所属宿主是 Fragment,请使用 childFragmentManager

请注意:

  • findFragmentAtPosition 仅适用于在 ViewPager2 的 RecyclerView 中初始化的 Fragment。因此,您只能获取可见位置 +1 的位置。
  • Lint 将建议您从 fun ViewPager2.findFragmentAtPosition 中移除 ViewPager2。 ,因为您没有使用 ViewPager2 类中的任何内容。我认为它应该留在那里,因为这个解决方法仅适用于 ViewPager2。

19

当我迁移到ViewPager2时,遇到了类似的问题。

在我的情况下,我决定使用parentFragment属性(我认为你也可以让它适用于活动),并希望ViewPager2只保留当前片段处于恢复状态。(即最后一次被恢复的页片段是当前页。)

因此,在包含ViewPager2视图的主片段(HostFragment)中,我创建了以下属性:

private var _currentPage: WeakReference<MyPageFragment>? = null
val currentPage
    get() = _currentPage?.get()

fun setCurrentPage(page: MyPageFragment) {
    _currentPage = WeakReference(page)
}

我决定使用 WeakReference,这样我就不会泄漏不活动的Fragment实例。

我在 ViewPager2 中显示的每个Fragment都继承自通用超类 MyPageFragment。该类负责在 onResume 中向宿主Fragment注册其实例:

override fun onResume() {
    super.onResume()
    (parentFragment as HostFragment).setCurrentPage(this)
}

我还使用这个类来定义分页片段的通用接口:

abstract fun someOperation1()

abstract fun someOperation2()

然后我可以这样从HostFragment中调用它们:

currentPage?.someOperation1()

我不是说这是一个好的解决方案,但我认为它比之前我们必须使用的ViewPager适配器中的instantiateItem方法更加优雅。


1
另一个非常好的答案是覆盖 onPageSelected 函数,该函数在此网站 https://proandroiddev.com/look-deep-into-viewpager2-13eb8e06e419 中显示。 - Ben Akin
@BenAkin 这是一篇非常好的文章,但我没有找到如何获取当前位置的片段,你能在这里分享一下吗? - Micer
@Micer 你需要创建一个类,像这样:class ViewPager2PageChangeCallback(private val listener: (Int) -> Unit) : ViewPager2.OnPageChangeCallback() { override fun onPageSelected(position: Int) { super.onPageSelected(position) Log.d(Util.DEBUG_LOG, "this currently is the postion: " + position) } } 然后...在使用viewpager fragments的类/活动中,在onCreate中添加以下内容:mPager.registerOnPageChangeCallback(viewPager2PageChangeCallback!!) - Ben Akin
5
@BenAkin这个回调函数会返回一个position: Int,但是你仍然没有Fragment对象。或者我有所遗漏吗? - Micer

6

我使用反射在FragmentStateAdapter中获取了当前片段的访问权限。

Kotlin中的扩展函数:

fun FragmentStateAdapter.getItem(position: Int): Fragment? {
    return this::class.superclasses.find { it == FragmentStateAdapter::class }
        ?.java?.getDeclaredField("mFragments")
        ?.let { field ->
            field.isAccessible = true
            val mFragments = field.get(this) as LongSparseArray<Fragment>
            return@let mFragments[getItemId(position)]
        }
}

如有需要,请添加 Kotlin 反射依赖:
implementation "org.jetbrains.kotlin:kotlin-reflect:1.3.61"

示例调用:

val tabsAdapter = viewpager.adapter as FragmentStateAdapter
val currentFragment = tabsAdapter.getItem(viewpager.currentItem)

感谢您指出这一点。已添加有关Kotlin反射依赖项的注释。 - AndrazP
1
反射是昂贵的,请看看我的解决方案。@AndrazP - Atif AbbAsi

4
如果您只想让当前的片段执行某些操作,可以使用一个SharedViewModel。它在ViewPager容器和其Fragment之间共享,并将一个Identifier传递给每个Fragment,并观察SharedViewModel中的LiveData。将该LiveData的值设置为一个对象,其中包含您想要更新的片段的Identifier(即Pair<String, MyData>,其中StringIdentifier的类型)。然后在观察者内部检查当前发出的Identifier是否与片段的Identifier相同,如果相同则消耗数据。
这并不像使用片段标签那样简单,但至少您无需担心ViewPager2如何为每个片段创建标签的更改。

4

我也发表一下我的解决方案 - 和@Almighty的基本方法相同,只是我在PagerAdapter中使用一个查找表来保留Fragment的弱引用:

private class PagerAdapter(fm: FragmentManager, lifecycle: Lifecycle) : FragmentStateAdapter(fm, lifecycle) {

    // only store as weak references, so you're not holding discarded fragments in memory
    private val fragmentCache = mutableMapOf<Int, WeakReference<Fragment>>()

    override fun getItemCount(): Int = tabList.size
    
    override fun createFragment(position: Int): Fragment {
        // return the cached fragment if there is one
        fragmentCache[position]?.get()?.let { return it }

        // no fragment found, time to make one - instantiate one however you
        // like and add it to the cache
        return tabList[position].fragment.newInstance()
            .also { fragmentCache[position] = WeakReference(it) }
            .also { Timber.d("Created a fragment! $it") }
    }

    // not necessary, but I think the code reads better if you
    // can use a getter when you want to... try to get an existing thing
    fun getFragment(position: Int) = createFragment(position)
}

然后您可以根据适当的页面编号调用getFragment,比如adapter.currentPage或其他。

所以基本上,适配器保持着它自己创建的片段的缓存,但是使用了WeakReference,因此它实际上并没有持有它们。一旦实际使用片段的组件完成对它们的使用,它们就不会再存在于缓存中了。因此,您可以为所有当前片段保留查找。


如果您希望,getter函数只返回查找结果(可为空)。这个版本显然会在不存在时创建片段,如果您期望它存在,则非常有用。如果您正在使用ViewPager2.OnPageChangeCallback,它将在视图页面创建片段之前触发新的页面号码 - 您可以"获取"该页面,这将创建和缓存它,并且当pager调用createFragment时,它应该仍然在缓存中,避免重新创建。

但是不能保证弱引用在这两个时刻之间没有被垃圾回收,因此,如果您设置该片段实例上的东西(而不仅仅是从中读取某些内容,比如要显示的标题),请注意这一点!


很棒的解决方案! - Manuela
无法工作,createFragment并不总是被调用,因为存在某种childFragmentManager或savedinstance状态。 - George Shalvashvili

4
supportFragmentManager.findFragmentByTag("f" + viewpager.currentItem)

在`placeFragmentInViewHolder(@NonNull final FragmentViewHolder holder)`中使用`FragmentStateAdapter`添加Fragment。
mFragmentManager.beginTransaction()
                    .add(fragment, "f" + holder.getItemId())
                    .setMaxLifecycle(fragment, STARTED)
                    .commitNow()

请问您能否添加一些关于这行代码为什么以及如何提供答案的信息?谢谢。 - deHaar
8
调试时我确实看到添加的片段标记是"f0"、"f1"等,但我不确定这有多可靠。 - Markymark
是的,如何在片段状态适配器中使用此代码片段? - Dyno Cris
哦,我想通了!使用类似这样的代码:mSomeButton.setOnClickListener(v -> { OneFragment oneFragment = (OneFragment) getSupportFragmentManager().findFragmentByTag("f" + 0); if (oneFragment != null) oneFragment.setText("你好");}); - Dyno Cris

3

我只是在我的选项卡片段中使用了这个

fun currentFragment() = childFragmentManager.fragments.find { it.isResumed }

2

我有同样的问题。我从ViewPager转换到了ViewPager2,使用FragmentStateAdapter。在我的情况下,我有一个DetailActivity类(扩展自AppCompatActivity),其中包含ViewPager2,用于在较小的设备上翻页浏览数据列表(联系人、媒体等)。

我需要知道当前显示的片段(它是我自己的类DetailFragment,它扩展了androidx.fragment.app.Fragment),因为该类包含我用来更新DetailActivity工具栏上标题的字符串。

一开始我尝试注册onPageChangeCallback监听器,如一些人建议的那样,但我很快就遇到了问题:

  1. 我最初在ViewPager2的adapter.createFragment()调用期间创建标签,如一些人建议的那样,想法是将新创建的片段添加到Bundle对象中(使用FragmentManager.put())并使用该标签。这样,我就可以在配置更改时保存它们。问题在于,在createFragment()期间,片段实际上还没有成为FragmentManager的一部分,因此put()调用失败。
  2. 另一个问题是,如果基本思想是使用OnPageChangeCallback的onPageSelected()方法通过内部生成的“f”+position标记名称找到片段 - 我们再次遇到相同的时间问题:第一次进入时,onPageSelected()在adapter的createFragment()调用之前被调用 - 因此还没有片段被创建并添加到FragmentManager中,因此我无法使用“f0”标记获取对第一个片段的引用。
  3. 然后我尝试找到一种方法,在onPageSelected中保存传递的位置,然后尝试在其他地方引用它以便在adapter进行createFragment()调用之后检索片段 - 但是我无法确定在adapter、相关的recyclerview、viewpager等中是否有任何类型的处理程序,使我可以显示可以引用该监听器中标识的位置的片段列表。奇怪的是,例如,一个看起来非常有前途的adapter方法是onViewAttachedToWindow() - 但它被标记为final,因此无法重写(即使JavaDoc明确预期以这种方式使用它)。

所以我最终做的是:

  1. 在我的DetailFragment类中,我创建了一个接口,可以由托管活动实现:
    public interface DetailFragmentShownListener {
        // Allows classes that extend this to update visual items after shown
        void onDetailFragmentShown(DetailFragment me);
    }
  1. 然后我在DetailFragment的onResume()方法中添加了代码,以检查相关联的Activity是否在该类中实现了DetailFragmentShownListener接口。如果是,则进行回调:
    public void onResume() {
        super.onResume();
        View v = getView();
        if (v!=null && v.getViewTreeObserver().isAlive()) {
            v.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
                @Override
                public void onGlobalLayout() {
                    v.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                    // Let our parent know we are laid out
                    if ( getActivity() instanceof DetailFragmentShownListener ) {
                        ((DetailFragmentShownListener) getActivity()).onDetailFragmentShown(DetailFragment.this);
                    }
                }
            });
        }
    }
  1. 那么当我需要知道该片段何时显示(例如在DetailActivity中),我实现该接口,当它接收到此回调时,我便知道这是当前的片段:
    @Override
    public void onDetailFragmentShown(DetailFragment me) {
        mCurrentFragment = me;
        updateToolbarTitle();
    }

mCurrentFragment是这个类的一个属性,因为它在其他地方也被使用。


2

和其他答案类似,这是我目前正在使用的方法。
我不确定它有多可靠,但至少其中一个肯定会起作用。

//Unclear how reliable this is
fun getFragmentAtIndex(index: Int): Fragment? {
    return fm.findFragmentByTag("f${getItemId(index)}")
        ?: fm.findFragmentByTag("f$index")
        ?: fm.findFragmentById(getItemId(index).toInt())
        ?: fm.findFragmentById(index)
}

fm is the supportFragmentManager


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