【问题标题】:Get current fragment with ViewPager2使用 ViewPager2 获取当前片段
【发布时间】:2019-04-17 13:29:51
【问题描述】:

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

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

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

ViewPager2 有什么遗漏吗?

【问题讨论】:

  • 您的查询有什么解决方案吗?
  • 不,还没有解决方案。
  • FragmentStateAdapter 中的 fragmentManager.findFragmentById(getItemId(index).toInt()) 似乎可以工作,但只有在添加了片段之后。从文档来看,如果片段被移动,这可能不起作用。 Default implementation works for collections that don't add, move, remove items
  • 只是给未来读者的一般说明(我知道这不是你的问题)。如果您正在寻找一种方法来通知片段已显示,您可以使用 ViewModel 并让每个片段观察相同的 ViewModel。然后,如果它们的javaClass.name 与传入的内容匹配,则每个片段都会响应。

标签: android android-viewpager androidx


【解决方案1】:

ViewPager2 中,FragmentManager 默认为片段分配标签,如下所示:

第一个位置的片段有一个标签"f0"

第二个位置的片段有一个标签"f1"

第三个位置的片段有一个标签"f2" 等等......所以你可以得到你的片段的标签,并将“f”与你的片段的位置连接起来。要获取当前 Fragment,您可以从 viewPager2 位置获取当前位置,并使您的标签像这样(对于 Kotlin):

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

对于某个位置的片段

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

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

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

记住

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

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

或者简单地说:

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

如果您在 Fragment 中托管 ViewPager2,请使用 childFragmentManager 而不是 supportFragmentManager

【讨论】:

  • 如果明天他们决定更改标签怎么办?
  • 0.1% 这个数字你怎么能这么确定?
  • @Dr.aNdRO 是对的,但内部命名方案不是 API 的一部分,因此它可以随时出于任何原因更改,而不会发出任何警告。这是一个有效的解决方案,但它很脆弱,你不能依赖它,所以如果这对你很重要,你需要了解风险
  • 如果您有多个视图寻呼机怎么办?我根本不喜欢这个解决方案
  • 不,它不是那样工作的。 ViewPager 的命名约定已经在 PlayStore 中的 APK 上。因此,除非您更改 ViewPager2 库,否则无需更改代码。 @RafaelLima
【解决方案2】:

我在迁移到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 中显示的每个片段都继承自公共超类MyPageFragment。该类负责将其实例注册到onResume中的宿主片段:

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

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

abstract fun someOperation1()

abstract fun someOperation2()

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

currentPage?.someOperation1()

我并不是说这是一个很好的解决方案,但我认为它比依赖 ViewPager's 适配器的内部结构和我们之前必须使用的 instantiateItem 方法更优雅。

【讨论】:

  • 另一个非常好的答案是覆盖此站点中显示的 onPageSelected 函数proandroiddev.com/look-deep-into-viewpager2-13eb8e06e419
  • @BenAkin 这篇文章非常好,但是我找不到如何获取当前位置的片段,你能在这里分享一下吗?
  • @Micer 你需要创建一个像这样的类 ViewPager2PageChangeCallback(private val listener: (Int) -> Unit) : ViewPager2.OnPageChangeCallback() { override fun onPageSelected(position: Int) { super. onPageSelected(position) Log.d(Util.DEBUG_LOG, "this current is the postion:" + position) } } then.... 在使用 viewpager 片段的类/活动中,将其添加到 onCreate: mPager.registerOnPageChangeCallback (viewPager2PageChangeCallback!!)
  • @BenAkin 这个回调给你一个position: Int,但是你仍然没有Fragment 对象。还是我错过了什么?
【解决方案3】:

通过标签查找当前 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 中初始化的 Fragment。因此,您只能获得可见 + 1 的位置。
  • Lint 会建议您从 fun ViewPager2.findFragmentAtPosition 中删除 ViewPager2.,因为您不使用 ViewPager2 类中的任何内容。我认为它应该保留在那里,因为这种解决方法仅适用于 ViewPager2。

【讨论】:

    【解决方案4】:

    我能够使用 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)
    

    【讨论】:

    • 感谢您指出。添加了关于 Kotlin 反射依赖的注释。
    • 反射很昂贵看看我的解决方案。 @AndrazP
    【解决方案5】:
    supportFragmentManager.findFragmentByTag("f" + viewpager.currentItem)
    

    placeFragmentInViewHolder(@NonNull final FragmentViewHolder holder)添加片段中使用FragmentStateAdapter

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

    【讨论】:

    • 您能否添加一些关于为什么如何这行代码提供问题的答案的信息?谢谢...
    • 调试时我确实看到添加片段的标签确实是“f0”、“f1”等,但我想知道它有多可靠。
    • 是的,如何将这个sn-p与片段状态适配器一起使用?
    • 哦,我想通了!使用类似这样的东西 mSomeButton.setOnClickListener(v -> { OneFragment oneFragment = (OneFragment) getSupportFragmentManager().findFragmentByTag("f" + 0); if (oneFragment != null) oneFragment.setText("Hello");});
    【解决方案6】:

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

    【讨论】:

      【解决方案7】:

      也可以发布我的解决方案 - 它与 @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 或其他。

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


      如果您愿意,您可以让 getter 只返回查找的(可为空的)结果。如果片段尚不存在,则此版本显然会创建片段,如果您希望片段存在,这将很有用。如果您使用ViewPager2.OnPageChangeCallback,这可能会很方便,它将使用新的页码视图寻呼机创建片段之前触发 - 您可以“获取”页面,该页面将创建并缓存它,并且当寻呼机调用createFragment 时,它应该仍然在缓存中并避免重新创建它。

      虽然不能保证弱引用在这两个时刻之间不会被垃圾回收,所以如果你在那个片段实例上设置东西(而不是仅仅从中读取一些东西,比如要显示的标题)请注意!

      【讨论】:

      • 很好的解决方案!
      【解决方案8】:

      与此处的其他一些答案类似,这是我目前正在使用的。
      我不清楚它有多可靠,但至少其中一个肯定会起作用?。

      //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】:

        您可以从ViewPager2 获取当前片段,如下所示,

        adapter.getRegisteredFragment(POSITION);
        

        示例FragmentStateAdapter

        public class ViewPagerAdapterV2 extends FragmentStateAdapter {
            private final FragmentActivity context;
            private final HashMap<Integer, Fragment> mapFragments;
        
            public ViewPagerAdapterV2(FragmentActivity fm) {
                super(fm);
                this.context = fm;
                this.mapFragments = new HashMap<>();
            }
        
            public Context getContext() {
                return context;
            }
        
            @Override
            public int getItemCount() {
                return NUMBER_OF_PAGES;
            }
        
            public Fragment getRegisteredFragment(int position) {
                return mapFragments.get(position);
            }
        
            @NonNull
            @Override
            public Fragment createFragment(int position) {    
                MyFragment fragment = new MyFragment();
                mapFragments.put(position, fragment);
                return fragment;
            }
        }
        

        【讨论】:

        • 这似乎是个坏主意,因为您正在存储片段,而 ViewPager 可能正在回收它(根据需要销毁和重新创建)。
        【解决方案10】:

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

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

        但是,这样做有点代码味道。它向我表明,应该在片段(或其他地方)中完成的事情正在活动中完成。

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

        【讨论】:

          【解决方案11】:

          我遇到了同样的问题。我使用 FragmentStateAdapter 从 ViewPager 转换为 ViewPager2。在我的例子中,我有一个 DetailActivity 类(扩展 AppCompatActivity),其中包含 ViewPager2,它用于在较小的外形设备上对数据列表(联系人、媒体等)进行分页。

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

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

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

          【讨论】:

            【解决方案12】:

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

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

            【讨论】:

              【解决方案13】:

              我发现即使在活动被销毁并重新创建后,这对我也有效。 密钥可以是任何类型。假设您的片段使用了 ViewModel。

              fun findFragment(fragmentId: String?): YourFragment? {
                  fragmentId?: return null
                  return supportFragmentManager.fragments.filterIsInstance(YourFragment::class.java).find { it.viewModel.yourData.value.id == fragmentId}
              }
              

              【讨论】:

                【解决方案14】:

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

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

                创建适配器后,我使用 ViewPager2.OnPageChangeCallback() 注册了我的 Viewpager 2 并覆盖了它的方法onPageSelected

                现在很简单地做了这个技巧来获取当前片段

                 private fun getCurrentFragment() :Fragment?{
                
                        val fragment = (binding!!.pager.adapter as MyViewPager2Adapter).currentFragmentWeakReference?.get()
                
                        retrun fragment
                    }
                

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

                干杯,希望这对你有帮助!

                【讨论】:

                • 此解决方案不处理进程配置更改(即手机在后台杀死应用程序)-createFragment 不会在发生这种情况时被调用,因为它只是恢复以前的片段状态。您的 weakReference 将为空。
                • 那你会泄露的
                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 2022-01-12
                • 2021-12-15
                • 1970-01-01
                • 1970-01-01
                • 2011-10-08
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多