使用getItemPosition(Object对象)在FragmentStatePagerAdapter中重新排序页面

我相信,当重写getItemPosition(Object object)时,FragmentStatePagerAdapter在重新排序页面的时候并不正确。

下面是一个简单的例子。 在初始状态下,页面的顺序是{A,B,C}。 在调用toggleState() ,页面的顺序变为{A,C,B}。 通过重写getItemPosition(Object object) ,我们确保当前正在查看的页面(A,B或C)不会改变。

 public static class TestPagerAdapter extends FragmentStatePagerAdapter { private boolean mState = true; public TestPagerAdapter(FragmentManager fragmentManager) { super(fragmentManager); } @Override public int getCount() { return 3; } private void toggleState() { mState = !mState; notifyDataSetChanged(); } private String getLabel(int position) { switch (position) { case 0: return "A"; case 1: return mState ? "B" : "C"; default: return mState ? "C" : "B"; } } @Override public int getItemPosition(Object object) { String label = ((TestFragment) object).getLabel(); if (label.equals("A")) { return 0; } else if (label.equals("B")) { return mState ? 1 : 2; } else { return mState ? 2 : 1; } } @Override public CharSequence getPageTitle(int position) { return getLabel(position); } @Override public Fragment getItem(int position) { return TestFragment.newInstance(getLabel(position)); } } 

我遇到了两个单独的行为,看起来不正确。

  1. 如果我立即调用toggleState() (在查看页面A之前,滑动到任何其他页面),应用程序崩溃。

     java.lang.IndexOutOfBoundsException: Invalid index 2, size is 2 at java.util.ArrayList.throwIndexOutOfBoundsException(ArrayList.java:251) at java.util.ArrayList.set(ArrayList.java:477) at android.support.v4.app.FragmentStatePagerAdapter.destroyItem(FragmentStatePagerAdapter.java:136) at android.support.v4.view.ViewPager.populate(ViewPager.java:867) at android.support.v4.view.ViewPager.setCurrentItemInternal(ViewPager.java:469) at android.support.v4.view.ViewPager.setCurrentItemInternal(ViewPager.java:441) at android.support.v4.view.ViewPager.dataSetChanged(ViewPager.java:766) at android.support.v4.view.ViewPager$PagerObserver.onChanged(ViewPager.java:2519) at android.database.DataSetObservable.notifyChanged(DataSetObservable.java:37) at android.support.v4.view.PagerAdapter.notifyDataSetChanged(PagerAdapter.java:276) at com.ugglynoodle.test.testfragmentstatepageradapter.MainActivity$TestPagerAdapter.toggleState(MainActivity.java:55) ... 

    查看FragmentStatePagerAdapter的源代码,在调用第136行中的set()之前,首先检查mFragments的大小(如第113-115行)。

  2. 如果我第一次滑动到页面B,那么调用getItem(2) ,创建页面C,并且mFragments现在具有3的大小(这将防止上述崩溃在一瞬间发生)。 然后,我滑回页面A,页面C被销毁,因为它应该是(因为它是2页远,我使用默认的离屏页面限制为1)。 现在,我调用toggleState() 。 页面B现在被销毁了。 但是,页面C不重新创建! 这意味着,当我向右滑动时,我得到一个空白页面。

首先,我很高兴知道自己是否正确,而这些实际上是错误的,或者我做错了什么。 如果他们是错误,任何人都可以提出一个解决方法(除了自己调试和重建支持库)? 肯定有人必须重写getItemPosition(Object object)成功(除了将所有设置为POSITION_NONE )?

我正在使用支持库的最新版本(10)。

看着FragmentStatePagerAdapter的来源,我明白了究竟是怎么回事。 FragmentStatePagerAdapter将碎片和保存的状态缓存在ArrayList中: mFragmentsmSavedState 。 但是,当片段被重新排序时,没有机制对mFragmentsmSavedState的元素进行重新排序。 因此,适配器将向寻呼机提供错误的片段。

我已经提出了这个问题 ,并附加一个固定的实现(NewFragmentStatePagerAdapter.java)的问题。 在修复中,我已经将一个getItemId()函数添加到FragmentStatePagerAdapter。 (这反映了FragmentPagerAdapter中的重新排序实现。)通过适配器位置的itemId数组始终存储在一起。 然后,在notifyDataSetChanged() ,适配器检查itemIds数组是否已经改变。 如果有,那么mFragmentsmSavedState会相应地重新排序。 可以在destroyItem()saveState()restoreState()找到进一步的修改。

要使用这个类, getItemPosition()getItemId()必须与getItem()一致地实现。

对于我来说,是一个问题的答案之一。 答案#20#21。 链接到解决方案https://gist.github.com/ypresto/8c13cb88a0973d071a64 。 最好的解决方案,适用于更新页面和重新排序。 只有在这个解决方案中,适配器在销毁item(方法destroyItem)时不会抛出IndexOutOfBoundsExeption,这是其他解决方案的已知错误。

我已经在Kotlin中重新实现了现有的解决方案 ,使得它可以返回一个String而不是一个long的项目ID。 你可以在这里或下面找到它:

 import android.annotation.SuppressLint import android.os.Bundle import android.os.Parcelable import android.support.v4.app.Fragment import android.support.v4.app.FragmentManager import android.support.v4.app.FragmentTransaction import android.view.View import android.view.ViewGroup import java.util.HashSet import java.util.LinkedHashMap /** * A PagerAdapter that can withstand item reordering. See * https://issuetracker.google.com/issues/36956111. * * @see android.support.v4.app.FragmentStatePagerAdapter */ abstract class MovableFragmentStatePagerAdapter( private val manager: FragmentManager ) : NullablePagerAdapter() { private var currentTransaction: FragmentTransaction? = null private var currentPrimaryItem: Fragment? = null private val savedStates = LinkedHashMap<String, Fragment.SavedState>() private val fragmentsToItemIds = LinkedHashMap<Fragment, String>() private val itemIdsToFragments = LinkedHashMap<String, Fragment>() private val unusedRestoredFragments = HashSet<Fragment>() /** @see android.support.v4.app.FragmentStatePagerAdapter.getItem */ abstract fun getItem(position: Int): Fragment /** * @return a unique identifier for the item at the given position. */ abstract fun getItemId(position: Int): String /** @see android.support.v4.app.FragmentStatePagerAdapter.startUpdate */ override fun startUpdate(container: ViewGroup) { check(container.id != View.NO_ID) { "ViewPager with adapter $this requires a view id." } } /** @see android.support.v4.app.FragmentStatePagerAdapter.instantiateItem */ override fun instantiateItem(container: ViewGroup, position: Int): Any { val itemId = getItemId(position) val f = itemIdsToFragments[itemId] if (f != null) { unusedRestoredFragments.remove(f) return f } if (currentTransaction == null) { // We commit the transaction later @SuppressLint("CommitTransaction") currentTransaction = manager.beginTransaction() } val fragment = getItem(position) fragmentsToItemIds.put(fragment, itemId) itemIdsToFragments.put(itemId, fragment) val fss = savedStates[itemId] if (fss != null) { fragment.setInitialSavedState(fss) } fragment.setMenuVisibility(false) fragment.userVisibleHint = false currentTransaction!!.add(container.id, fragment) return fragment } /** @see android.support.v4.app.FragmentStatePagerAdapter.destroyItem */ override fun destroyItem(container: ViewGroup, position: Int, fragment: Any) { (fragment as Fragment).destroy() } /** @see android.support.v4.app.FragmentStatePagerAdapter.setPrimaryItem */ override fun setPrimaryItem(container: ViewGroup, position: Int, fragment: Any?) { fragment as Fragment? if (fragment !== currentPrimaryItem) { currentPrimaryItem?.let { it.setMenuVisibility(false) it.userVisibleHint = false } fragment?.setMenuVisibility(true) fragment?.userVisibleHint = true currentPrimaryItem = fragment } } /** @see android.support.v4.app.FragmentStatePagerAdapter.finishUpdate */ override fun finishUpdate(container: ViewGroup) { if (!unusedRestoredFragments.isEmpty()) { for (fragment in unusedRestoredFragments) fragment.destroy() unusedRestoredFragments.clear() } currentTransaction?.let { it.commitAllowingStateLoss() currentTransaction = null manager.executePendingTransactions() } } /** @see android.support.v4.app.FragmentStatePagerAdapter.isViewFromObject */ override fun isViewFromObject(view: View, fragment: Any): Boolean = (fragment as Fragment).view === view /** @see android.support.v4.app.FragmentStatePagerAdapter.saveState */ override fun saveState(): Parcelable? = Bundle().apply { putStringArrayList(KEY_FRAGMENT_IDS, ArrayList<String>(savedStates.keys)) putParcelableArrayList( KEY_FRAGMENT_STATES, ArrayList<Fragment.SavedState>(savedStates.values) ) for ((f, id) in fragmentsToItemIds.entries) { if (f.isAdded) { manager.putFragment(this, "$KEY_FRAGMENT_STATE$id", f) } } } /** @see android.support.v4.app.FragmentStatePagerAdapter.restoreState */ override fun restoreState(state: Parcelable?, loader: ClassLoader?) { if ((state as Bundle?)?.apply { classLoader = loader }?.isEmpty == false) { state!! fragmentsToItemIds.clear() itemIdsToFragments.clear() unusedRestoredFragments.clear() savedStates.clear() val fragmentIds: List<String> = state.getStringArrayList(KEY_FRAGMENT_IDS) val fragmentStates: List<Fragment.SavedState> = state.getParcelableArrayList(KEY_FRAGMENT_STATES) for ((index, id) in fragmentIds.withIndex()) { savedStates.put(id, fragmentStates[index]) } for (key: String in state.keySet()) { if (key.startsWith(KEY_FRAGMENT_STATE)) { val itemId = key.substring(KEY_FRAGMENT_STATE.length) manager.getFragment(state, key)?.let { it.setMenuVisibility(false) fragmentsToItemIds.put(it, itemId) itemIdsToFragments.put(itemId, it) } } } unusedRestoredFragments.addAll(fragmentsToItemIds.keys) } } private fun Fragment.destroy() { if (currentTransaction == null) { // We commit the transaction later @SuppressLint("CommitTransaction") currentTransaction = manager.beginTransaction() } val itemId = fragmentsToItemIds.remove(this) itemIdsToFragments.remove(itemId) if (itemId != null) { savedStates.put(itemId, manager.saveFragmentInstanceState(this)) } currentTransaction!!.remove(this) } private companion object { const val KEY_FRAGMENT_IDS = "fragment_keys_" const val KEY_FRAGMENT_STATES = "fragment_states_" const val KEY_FRAGMENT_STATE = "fragment_state_" } } 

而Java的一块:

 import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.view.PagerAdapter; import android.view.ViewGroup; /** * A PagerAdapter whose {@link #setPrimaryItem} is overridden with proper nullability annotations. */ public abstract class NullablePagerAdapter extends PagerAdapter { @Override public void setPrimaryItem(@NonNull ViewGroup container, int position, @Nullable Object object) { // `object` is actually nullable. It's even in the dang source code which is hilariously // ridiculous: // `mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null);` } }