ページの並べ替えを目的として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));
}
}
_
正しくないように見える2つの別々の動作に遭遇しました。
すぐに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行目)。
最初にページBにスワイプすると、getItem(2)
が呼び出され、ページCが作成され、mFragments
のサイズが3になります(これにより、上記のクラッシュがすぐに発生するのを防ぐことができます) 。次に、スワイプしてページAに戻ると、ページCが破棄されるはずです(2ページ離れていて、デフォルトのオフスクリーンページ制限1を使用しているため)。ここで、toggleState()
を呼び出します。ページBが破棄されました。ただし、ページCは再作成されません。つまり、右にスワイプすると、空のページが表示されます。
まず、私が正しく、これらが実際にバグであるかどうか、または私が何か間違ったことをしているのかどうかを知ることは素晴らしいことです。それらがバグである場合、誰かが回避策を提案できますか(自分でサポートライブラリをデバッグして再構築する以外)?確かに誰かがgetItemPosition(Object object)
を正常にオーバーライドしたに違いありません(すべてを_POSITION_NONE
_に設定することは別として)?
サポートライブラリの現在のリビジョン(10)を使用しています。
FragmentStatePagerAdapterのソースを見て、何が問題になっているのかを正確に把握しました。 FragmentStatePagerAdapterは、フラグメントと保存された状態をArrayListsにキャッシュします:mFragments
およびmSavedState
。ただし、フラグメントが並べ替えられる場合、mFragments
およびmSavedState
の要素を並べ替えるメカニズムはありません。したがって、アダプタはポケットベルに間違ったフラグメントを提供します。
私はこれについて 問題 を提出し、問題に修正された実装(NewFragmentStatePagerAdapter.Java)を添付しました。修正では、FragmentStatePagerAdapterにgetItemId()
関数を追加しました。 (これはFragmentPagerAdapterの並べ替えの実装を反映しています。)アダプターの位置によるitemIdの配列は常に保存されます。次に、notifyDataSetChanged()
で、アダプターはitemIds配列が変更されたかどうかを確認します。ある場合は、それに応じてmFragments
とmSavedState
が並べ替えられます。さらなる変更は、destroyItem()
、saveState()
、およびrestoreState()
にあります。
このクラスを使用するには、getItemPosition()
およびgetItemId()
をgetItem()
と一貫して実装する必要があります。
私にとっては 問題 の答えの1つを働いた。回答#20#21。ソリューションへのリンク https://Gist.github.com/ypresto/8c13cb88a0973d071a64 。最良の解決策は、ページの更新と並べ替えに役立ちます。このソリューションでのみ、アダプターはアイテムを破棄するときにIndexOutOfBoundsExeptionをスローしませんでした(メソッドdestroyItemで)。これは、他のソリューションの既知のバグです。
Kotlinで 既存のソリューション を再実装し、アイテムIDにString
ではなくlong
を返すことができるようにしました。あなたはそれを見つけることができます ここ または以下:
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);`
}
}
さて、私は解決策を見つけました。これにより、新しいタブを動的に作成/変更する場合のビューページャーフラグメントの並べ替えの問題が修正されます。
FragmentStatePagerAdapter.Javaの代わりにこのクラスを使用します
package Android.support.v4.app;
import Android.os.Bundle;
import Android.os.Parcelable;
import Android.support.v4.view.PagerAdapter;
import Android.util.Log;
import Android.view.View;
import Android.view.ViewGroup;
import Java.util.ArrayList;
public abstract class NewFragmentStatePagerAdapter extends PagerAdapter {
private static final String TAG = "FragmentStatePagerAdapt";
private static final boolean DEBUG = false;
private final FragmentManager mFragmentManager;
private FragmentTransaction mCurTransaction = null;
private ArrayList<Fragment.SavedState> mSavedState = new ArrayList<Fragment.SavedState>();
private ArrayList<Fragment> mFragments = new ArrayList<Fragment>();
private Fragment mCurrentPrimaryItem = null;
public NewFragmentStatePagerAdapter(FragmentManager fm) {
mFragmentManager = fm;
}
/**
* Return the Fragment associated with a specified position.
*/
public abstract Fragment getItem(int position);
@Override
public void startUpdate(ViewGroup container) {
if (container.getId() == View.NO_ID) {
throw new IllegalStateException("ViewPager with adapter " + this
+ " requires a view id");
}
}
public void destroyItemState(int position) {
mFragments.remove(position);
mSavedState.remove(position);
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
// If we already have this item instantiated, there is nothing
// to do. This can happen when we are restoring the entire pager
// from its saved state, where the fragment manager has already
// taken care of restoring the fragments we previously had instantiated.
if (mFragments.size() > position) {
Fragment f = mFragments.get(position);
if (f != null) {
return f;
}
}
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
Fragment fragment = getItem(position);
if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
if (mSavedState.size() > position) {
Fragment.SavedState fss = mSavedState.get(position);
if (fss != null) {
fragment.setInitialSavedState(fss);
}
}
while (mFragments.size() <= position) {
mFragments.add(null);
}
fragment.setMenuVisibility(false);
fragment.setUserVisibleHint(false);
mFragments.set(position, fragment);
mCurTransaction.add(container.getId(), fragment);
return fragment;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
Fragment fragment = (Fragment) object;
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
if (DEBUG) Log.v(TAG, "Removing item #" + position + ": f=" + object
+ " v=" + ((Fragment)object).getView());
while (mSavedState.size() <= position) {
mSavedState.add(null);
}
mSavedState.set(position, fragment.isAdded()
? mFragmentManager.saveFragmentInstanceState(fragment) : null);
mFragments.set(position, null);
mCurTransaction.remove(fragment);
}
@Override
@SuppressWarnings("ReferenceEquality")
public void setPrimaryItem(ViewGroup container, int position, Object object) {
Fragment fragment = (Fragment)object;
if (fragment != mCurrentPrimaryItem) {
if (mCurrentPrimaryItem != null) {
mCurrentPrimaryItem.setMenuVisibility(false);
mCurrentPrimaryItem.setUserVisibleHint(false);
}
if (fragment != null) {
fragment.setMenuVisibility(true);
fragment.setUserVisibleHint(true);
}
mCurrentPrimaryItem = fragment;
}
}
@Override
public void finishUpdate(ViewGroup container) {
if (mCurTransaction != null) {
mCurTransaction.commitNowAllowingStateLoss();
mCurTransaction = null;
}
}
@Override
public boolean isViewFromObject(View view, Object object) {
return ((Fragment)object).getView() == view;
}
@Override
public Parcelable saveState() {
Bundle state = null;
if (mSavedState.size() > 0) {
state = new Bundle();
Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()];
mSavedState.toArray(fss);
state.putParcelableArray("states", fss);
}
for (int i=0; i<mFragments.size(); i++) {
Fragment f = mFragments.get(i);
if (f != null && f.isAdded()) {
if (state == null) {
state = new Bundle();
}
String key = "f" + i;
mFragmentManager.putFragment(state, key, f);
}
}
return state;
}
@Override
public void restoreState(Parcelable state, ClassLoader loader) {
if (state != null) {
Bundle bundle = (Bundle)state;
bundle.setClassLoader(loader);
Parcelable[] fss = bundle.getParcelableArray("states");
mSavedState.clear();
mFragments.clear();
if (fss != null) {
for (int i=0; i<fss.length; i++) {
mSavedState.add((Fragment.SavedState)fss[i]);
}
}
Iterable<String> keys = bundle.keySet();
for (String key: keys) {
if (key.startsWith("f")) {
int index = Integer.parseInt(key.substring(1));
Fragment f = mFragmentManager.getFragment(bundle, key);
if (f != null) {
while (mFragments.size() <= index) {
mFragments.add(null);
}
f.setMenuVisibility(false);
mFragments.set(index, f);
} else {
Log.w(TAG, "Bad fragment at key " + key);
}
}
}
}
}
}
これを使用してメソッドをオーバーライドします
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
super.destroyItem(container, position, object);
if (getItemPosition(object) == POSITION_NONE) {
destroyItemState(position);
}
}