使用ViewPager2获取当前片段

时间:2019-04-17 13:29:52

标签: android android-viewpager androidx

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

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

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

我是否缺少ViewPager2的东西?

13 个答案:

答案 0 :(得分:30)

ViewPager2中,默认情况下,FragmentManager已将标签分配给片段,如下所示:

第1位的片段的标签为"f0"

第2个位置的片段的标签为"f1"

第3个位置的片段的标签为"f2",依此类推...因此,您可以获取片段的标签,并将“ f”与片段的位置串联起来。要获取当前的Fragment,您可以从viewPager2位置获取当前位置,并使标签像这样(对于Kotlin):

val myFragment = supportFragmentManager.findFragmentByTag("f" + viewpager.currentItem)

对于特定位置的片段

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

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

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

记住

如果适配器中有overridengetItemId(position: Int)。那么您的情况就不同了。应该是:

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

或简单地:

val myFragment = supportFragmentManager.findFragmentByTag("f" + adapter.getItemId(position))

如果您在Fragment中托管ViewPager2,请使用childFragmentManager代替supportFragmentManager

答案 1 :(得分:6)

通过其标签查找当前Fragment的解决方案似乎最适合我。我已经为此创建了这些扩展功能:

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中初始化的片段。因此,您只能获得可见+ 1的位置。
  • Lint建议您从ViewPager2.中删除fun ViewPager2.findFragmentAtPosition,因为您不使用ViewPager2类中的任何内容。我认为它应该留在那儿,因为此解决方法仅适用于ViewPager2。

答案 2 :(得分:4)

迁移到ViewPager2时遇到类似的问题。

在我的情况下,我决定使用parentFragment属性(我认为您也可以使其用于活动),并希望ViewPager2仅保留当前的片段。 (即最后恢复的页面片段是当前片段。)

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

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

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

我决定使用WeakReference,所以我不会泄漏不活动的Fragment实例

我在ViewPager2中显示的每个片段都继承自普通的超类MyPageFragment。此类负责向onResume中的主机片段注册其实例:

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

我还使用此类定义了分页片段的通用接口:

abstract fun someOperation1()

abstract fun someOperation2()

然后我可以像这样从HostFragment打电话给他们:

currentPage?.someOperation1()

我并不是说这是一个不错的解决方案,但我认为它比依靠ViewPager's适配器的内部结构和我们以前必须使用的instantiateItem方法更为美观。

答案 3 :(得分:2)

我能够使用 reflection 来访问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)

答案 4 :(得分:1)

也可以发布我的解决方案-它与@Almighty的基本方法相同,只是我将Fragment弱引用保留在PagerAdapter的查找表中:

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时,适配器实际上并没有保留它们,一旦实际使用了片段的组件完成了使用,它们就不会在缓存中了。因此,您可以查找所有当前片段。


如果需要,您可以让吸气剂仅返回查找的(可为空)结果。如果该片段尚不存在,该版本显然会创建该片段。如果您希望它存在,这将很有用。如果您使用的是ViewPager2.OnPageChangeCallback,这会很方便,它将在视图分页器创建片段之前的新页面编号之前触发。-您可以“获取”页面,该页面将创建并对其进行缓存,并且当寻呼机调用createFragment时,它仍应位于缓存中,并避免重新创建它。

这不能保证弱引用不会在这两个时刻之间被垃圾收集,因此,如果您要在该片段实例上设置内容(而不是像从其中读取内容一样)您要显示的标题)请注意!

答案 5 :(得分:1)

如果您希望当前的 Fragment 只在其中执行某些操作,您可以使用 SharedViewModel 容器及其 ​​ViewPager 之间共享的 Fragment 并传递每个片段的 Identifier 并观察 LiveData 中的 SharedViewModel。将该 LiveData 的值设置为一个对象,该对象由您要更新的片段的 Identifier 组成(即 Pair<String, MyData>,其中 StringIdentifier 的类型) .然后在您的观察者内部检查当前发出的 Identifer 是否与片段的 Identifier 相同,如果相等则消耗数据。
它不像使用fragment的标签来查找它们那么简单,但至少你不需要担心ViewPager2如何为每个fragment创建标签的变化。

答案 6 :(得分:0)

ViewPagerAdapter旨在隐藏所有这些实现细节,这就是为什么没有简单的方法做到这一点的原因。

getItem()中实例化片段时,您可以尝试在片段上设置,设置ID或标记,然后使用fragmentManager.findFragmentById()fragmentManager.findFragmentByTag()进行检索。

但是,这样做有点代码味道。它向我暗示,应该在片段中(或其他地方)完成活动中的工作。

也许还有另一种方法可以实现您想要的,但是在不知道为什么需要获取当前片段的情况下很难给出建议。

答案 7 :(得分:0)

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

FragmentStateAdapter中与placeFragmentInViewHolder(@NonNull final FragmentViewHolder holder)一起添加片段

mFragmentManager.beginTransaction()
                    .add(fragment, "f" + holder.getItemId())
                    .setMaxLifecycle(fragment, STARTED)
                    .commitNow()

答案 8 :(得分:0)

类似于这里的其他答案,这是我当前正在使用的。
我不清楚它的可靠性如何,但其中至少有一个是可以使用的。?

//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)
}

fmsupportFragmentManager

答案 9 :(得分:0)

我有同样的问题。我使用FragmentStateAdapter从ViewPager转换为ViewPager2。就我而言,我有一个DetailActivity类(扩展了AppCompatActivity),该类包含ViewPager2,该类用于在较小尺寸的设备上分页浏览数据列表(联系人,媒体等)。

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

我首先按照某些人的建议开始了注册onPageChangeCallback侦听器的工作,但是我很快遇到了问题:

  1. 我最初在ViewPager2的adapter.createFragment()调用期间创建了标签,这是有人建议的,目的是将带有标签的新创建的片段添加到Bundle对象(使用FragmentManager.put())中。这样,我就可以在配置更改中保存它们。这里的问题是在createFragment()期间,该片段实际上还不是FragmentManager的一部分,因此put()调用失败。
  2. 另一个问题是,如果基本思想是使用内部生成的“ f” +位置标记名使用OnPageChangeCallback的onPageSelected()方法查找片段,那么我们又遇到了相同的计时问题:时间,onPageSelected()在适配器上的createFragment()调用之前被调用-因此尚无片段被创建并添加到FragmentManager,因此我无法使用“ f0”来引用该第一个片段“标签。
  3. 然后,我试图找到一种方法,可以保存传递给onPageSelected的位置,然后尝试在适配器进行createFragment()调用后引用该位置,以便检索该片段-但我可以无法识别适配器内的任何类型的处理程序,相关的recyclerview,viewpager等,这些处理程序允许我显示片段列表,然后可以将该片段列表引用到该侦听器中标识的位置。奇怪的是,例如,一个看起来非常有前途的适配器方法是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()中添加了代码,以查看相关活动是否已在此类中实现了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是此类的一个属性,它在其他各种地方都使用过。

答案 10 :(得分:0)

按位置获取片段的解决方案:

class ClubPhotoAdapter(
    private val dataList: List<MyData>,
    fm: FragmentManager,
    lifecycle: Lifecycle
) : FragmentStateAdapter(fm, lifecycle) {

    private val fragmentMap = mutableMapOf<Int, WeakReference<MyFragment>>()

    override fun getItemCount(): Int = dataList.size

    override fun createFragment(position: Int): Fragment {
        val fragment = MyFragment[dataList.getOrNull(position)]

        fragmentMap[position] = WeakReference(fragment)

        return fragment
    }

    fun getItem(position: Int): MyFragment? = fragmentMap[position]?.get()
}

答案 11 :(得分:-1)

面临同样的问题,现在可以通过在适配器中添加一个对象来解决

struct ContainerView: View {
    @State var hasSet = false
    var body: some View {
        Group {
            MyView(a: $hasSet)
            MyView() // should be equivalent to MyView(a: .constant(true))
        }
    }
}

创建适配器后,我向class MyViewPager2Adapter(fragmentActivity: FragmentActivity) : FragmentStateAdapter(fragmentActivity) { private val FRAGMENTS_SIZE = 2 var currentFragmentWeakReference: WeakReference<Fragment>? = null override fun getItemCount(): Int { return this.FRAGMENTS_SIZE } override fun createFragment(position: Int): Fragment { when (position) { 0 -> { currentFragmentWeakReference= MyFirstFragment() return MyFirstFragment() } 1 -> { currentFragmentWeakReference= MySecondFragment() return MySecondFragment() } } return MyFirstFragment() /for default view } } 注册了Viewpager 2 并覆盖了其方法ViewPager2.OnPageChangeCallback()

现在很简单地用此技巧来获取当前片段

onPageSelected

我只在ViewPager2中用2个片段对此进行了测试

干杯,希望这对您有帮助。

答案 12 :(得分:-4)

我也遇到了您的问题,并使用了这个技巧

    Map<Integer, Fragment> map = new HashMap<>();

@Override
        public Fragment createFragment(int position) {
            Fragment fragment;
            switch (position) {
                case 0:
                    fragment = new CircularFragment();
                    map.put(0, fragment);
                    return fragment;
                case 1:
                    fragment = new LocalFragment();
                    map.put(1, fragment);
                    return fragment;
                case 2:
                    fragment = new SettingsFragment();
                    map.put(2, fragment);
                    return fragment;
            }
            fragment = new CircularFragment();
            map.put(0, fragment);
            return fragment;
        }

您会得到这样的片段

 LocalFragment f = (LocalFragment) map.get(1);