タブの管理とActivity
を接続するすべての詳細を実装するヘルパークラスとしてFragmentPagerAdapter
を使用しているViewPager
を介してフラグメントを相互に通信させるのに問題があります関連TabHost
。 AndroidサンプルプロジェクトSupport4Demosで提供されるのと同じように、FragmentPagerAdapter
を実装しました。
主な質問は、IDもタグも持っていないときに、どのようにしてFragmentManager
から特定のフラグメントを取得できますか? FragmentPagerAdapter
はフラグメントを作成し、IDとタグを自動生成しています。
注:この回答では、FragmentPagerAdapter
とそのソースコードを参照します。ただし、一般的な解決策はFragmentStatePagerAdapter
.にも適用する必要があります。
これを読んでいる場合は、おそらくFragmentPagerAdapter
/FragmentStatePagerAdapter
がFragments
のViewPager
を作成することを意図していることを既にご存知でしょうが、アクティビティの再作成時に(デバイスの回転またはシステムがメモリを回復するためにアプリを殺すかどうかにかかわらず)これらのFragments
は作成されません繰り返しますが、代わりに FragmentManager
から取得したインスタンス 。ここで、Activity
を操作するには、これらのFragments
への参照を取得する必要があると言います。 id
内部で設定 であるため、これらの作成されたtag
にはFragments
またはFragmentPagerAdapter
がありません。だから問題は、その情報なしでそれらへの参照を取得する方法です...
私がこれまで見た多くの解決策と同様の質問は、FragmentManager.findFragmentByTag()
を呼び出して 内部で作成されたタグ:"Android:switcher:" + viewId + ":" + id
を模倣することにより、既存のFragment
への参照を取得することに依存します 。これに関する問題は、内部ソースコードに依存していることです。これは、誰もが知っているように、永遠に同じままであるとは限りません。 GoogleのAndroidエンジニアは、既存のtag
への参照を見つけることができなくなるコードを壊すFragments
構造を変更することを簡単に決定できました。
tag
に依存しない代替ソリューション以下は、Fragments
に設定された内部FragmentPagerAdapter
に依存しないtags
によって返されるFragments
への参照を取得する方法の簡単な例です。重要なのは、 instantiateItem()
をオーバーライドして、getItem()
の instead の参照を保存することです。
public class SomeActivity extends Activity {
private FragmentA m1stFragment;
private FragmentB m2ndFragment;
// other code in your Activity...
private class CustomPagerAdapter extends FragmentPagerAdapter {
// other code in your custom FragmentPagerAdapter...
public CustomPagerAdapter(FragmentManager fm) {
super(fm);
}
@Override
public Fragment getItem(int position) {
// Do NOT try to save references to the Fragments in getItem(),
// because getItem() is not always called. If the Fragment
// was already created then it will be retrieved from the FragmentManger
// and not here (i.e. getItem() won't be called again).
switch (position) {
case 0:
return new FragmentA();
case 1:
return new FragmentB();
default:
// This should never happen. Always account for each position above
return null;
}
}
// Here we can finally safely save a reference to the created
// Fragment, no matter where it came from (either getItem() or
// FragmentManger). Simply save the returned Fragment from
// super.instantiateItem() into an appropriate reference depending
// on the ViewPager position.
@Override
public Object instantiateItem(ViewGroup container, int position) {
Fragment createdFragment = (Fragment) super.instantiateItem(container, position);
// save the appropriate reference depending on position
switch (position) {
case 0:
m1stFragment = (FragmentA) createdFragment;
break;
case 1:
m2ndFragment = (FragmentB) createdFragment;
break;
}
return createdFragment;
}
}
public void someMethod() {
// do work on the referenced Fragments, but first check if they
// even exist yet, otherwise you'll get an NPE.
if (m1stFragment != null) {
// m1stFragment.doWork();
}
if (m2ndFragment != null) {
// m2ndFragment.doSomeWorkToo();
}
}
}
またはクラスメンバー変数/ tags
への参照の代わりにFragments
を使用したい場合は、tags
によって設定されたFragmentPagerAdapter
を同じ方法で取得することもできます。注:これはFragmentStatePagerAdapter
には適用されませんtags
を作成するときにFragments
を設定しません。
@Override
public Object instantiateItem(ViewGroup container, int position) {
Fragment createdFragment = (Fragment) super.instantiateItem(container, position);
// get the tags set by FragmentPagerAdapter
switch (position) {
case 0:
String firstTag = createdFragment.getTag();
break;
case 1:
String secondTag = createdFragment.getTag();
break;
}
// ... save the tags somewhere so you can reference them later
return createdFragment;
}
このメソッドは、tag
によって設定される内部FragmentPagerAdapter
の模倣に依存せず、代わりにそれらを取得するために適切なAPIを使用することに注意してください。このようにすると、tag
の将来のバージョンでSupportLibrary
が変更されても、安全です。
忘れないでくださいActivity
の設計に応じて、作業しようとしているFragments
がまだ存在する場合と存在しない場合があるため、使用する前にnull
チェックを実行して、参照。
また、代わりにFragmentStatePagerAdapter
を使用している場合は、Fragments
へのハード参照を保持したくありません。代わりに、Fragment
参照を標準変数ではなくWeakReference
変数に保存します。このような:
WeakReference<Fragment> m1stFragment = new WeakReference<Fragment>(createdFragment);
// ...and access them like so
Fragment firstFragment = m1stFragment.get();
if (firstFragment != null) {
// reference hasn't been cleared yet; do work...
}
私は次の投稿に基づいて私の質問に対する答えを見つけました: fragmentpageradapterでフラグメントを再利用する
私が学んだことはほとんどありません:
FragmentPagerAdapter
内のgetItem(int position)
は、このメソッドが実際に行うことのやや誤解を招く名前です。既存のフラグメントを返すのではなく、新しいフラグメントを作成します。その意味で、メソッドはAndroid SDKのcreateItem(int position)
のような名前に変更する必要があります。したがって、このメソッドはフラグメントの取得には役立ちません。FragmentPagerAdapter
に任せる必要があります。つまり、フラグメントまたはそのタグへの参照はありません。ただし、フラグメントタグがある場合は、findFragmentByTag()
を呼び出すことで、FragmentManager
からそのタグへの参照を簡単に取得できます。特定のページ位置でフラグメントのタグを見つける方法が必要です。ソリューション
次のヘルパーメソッドをクラスに追加して、フラグメントタグを取得し、findFragmentByTag()
メソッドに送信します。
private String getFragmentTag(int viewPagerId, int fragmentPosition)
{
return "Android:switcher:" + viewPagerId + ":" + fragmentPosition;
}
注意!これは、FragmentPagerAdapter
が新しいフラグメントを作成するときに使用するのと同じ方法です。このリンクを参照してください http://code.google.com/p/openintents/source/browse/trunk/compatibility/AndroidSupportV2/src/Android/support/v2/app/FragmentPagerAdapter.Java#104
instantiateItem
をオーバーライドする必要も、内部makeFragmentName
メソッドでフラグメントタグを作成する互換性に依存する必要もありません。 instantiateItem
はpublicメソッドなので、呼び出すことができます(実際にはshould)アクティビティのonCreate
メソッドで、フラグメントのインスタンスへの参照を取得し、必要に応じてローカル変数に保存します。 instantiateItem
javadoc で説明されているように、一連のstartUpdate
呼び出しをfinishUpdate
およびPagerAdapter
メソッドで囲むことを忘れないでください。
PagerAdapterメソッドstartUpdate(ViewGroup)の呼び出しは、ViewPagerのコンテンツが変更されようとしていることを示します。 instantiateItem(ViewGroup、int)またはdestroyItem(ViewGroup、int、Object)への1つ以上の呼び出しが続き、finishUpdate(ViewGroup)への呼び出しによって更新の終了が通知されます。
したがって、たとえば、これはonCreate
メソッドでタブフラグメントへの参照を保存する方法です。
public class MyActivity extends AppCompatActivity {
Fragment0 tab0; Fragment1 tab1;
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.myLayout);
ViewPager viewPager = (ViewPager) findViewById(R.id.myViewPager);
MyPagerAdapter adapter = new MyPagerAdapter(getSupportFragmentManager());
viewPager.setAdapter(adapter);
((TabLayout) findViewById(R.id.tabs)).setupWithViewPager(viewPager);
adapter.startUpdate(viewPager);
tab0 = (Fragment0) adapter.instantiateItem(viewPager, 0);
tab1 = (Fragment1) adapter.instantiateItem(viewPager, 1);
adapter.finishUpdate(viewPager);
}
class MyPagerAdapter extends FragmentPagerAdapter {
public MyPagerAdapter(FragmentManager manager) {super(manager);}
@Override public int getCount() {return 2;}
@Override public Fragment getItem(int position) {
if (position == 0) return new Fragment0();
if (position == 1) return new Fragment1();
return null; // or throw some exception
}
@Override public CharSequence getPageTitle(int position) {
if (position == 0) return getString(R.string.tab0);
if (position == 1) return getString(R.string.tab1);
return null; // or throw some exception
}
}
}
instantiateItem
は、最初にFragmentManager
から既存のフラグメントインスタンスへの参照を取得しようとします。まだ存在しない場合にのみ、アダプタからgetItem
メソッドを使用して新しいものを作成し、将来の使用のためにFragmentManager
に「保存」します。
追加情報:instantiateItem
メソッドでstartUpdate
/finishUpdate
に囲まれたonCreate
メソッドを呼び出さないと、フラグメントインスタンスがcommittedto FragmentManager
:アクティビティがフォアグラウンドになると、instantiateItem
が自動的に呼び出されてフラグメントを取得しますが、startUpdate
/finishUpdate
maynot(実装の詳細に応じて)および基本的には、FragmentTransaction
を開始/コミットします。
これにより、作成されたフラグメントインスタンスへの参照が非常に迅速に失われ(画面を回転させる場合など)、必要以上に頻繁に再作成されます。フラグメントがどの程度「重い」かによって、パフォーマンスに無視できない影響が生じる場合があります。
しかし、より重要なのは、そのような場合、ローカル変数に保存されたフラグメントのインスタンスが古くなることです。Androidプラットフォームは、FragmentManager
から同じインスタンスを取得できなかったため、新しい変数を使用しますが、変数は引き続き古い変数を参照します。
私がやった方法は、WeakReferencesのHashtableを次のように定義することです。
protected Hashtable<Integer, WeakReference<Fragment>> fragmentReferences;
次に、次のようにgetItem()メソッドを作成しました。
@Override
public Fragment getItem(int position) {
Fragment fragment;
switch(position) {
case 0:
fragment = new MyFirstFragmentClass();
break;
default:
fragment = new MyOtherFragmentClass();
break;
}
fragmentReferences.put(position, new WeakReference<Fragment>(fragment));
return fragment;
}
次に、メソッドを記述できます。
public Fragment getFragment(int fragmentId) {
WeakReference<Fragment> ref = fragmentReferences.get(fragmentId);
return ref == null ? null : ref.get();
}
これはうまくいくようで、私はそれが
"Android:switcher:" + viewId + ":" + position
fragmentPagerAdapterの実装方法に依存しないため、トリック。もちろん、フラグメントがFragmentPagerAdapterによってリリースされている場合、またはまだ作成されていない場合、getFragmentはnullを返します。
誰かがこのアプローチで何か間違ったことを見つけた場合、コメントは大歓迎です。
現在のフラグメントへの参照を取得するために機能しているこのメソッドを作成しました。
public static Fragment getCurrentFragment(ViewPager pager, FragmentPagerAdapter adapter) {
try {
Method m = adapter.getClass().getSuperclass().getDeclaredMethod("makeFragmentName", int.class, long.class);
Field f = adapter.getClass().getSuperclass().getDeclaredField("mFragmentManager");
f.setAccessible(true);
FragmentManager fm = (FragmentManager) f.get(adapter);
m.setAccessible(true);
String tag = null;
tag = (String) m.invoke(null, pager.getId(), (long) pager.getCurrentItem());
return fm.findFragmentByTag(tag);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
return null;
}
フラグメントへのハンドルを取得する主な障害は、getItem()に依存できないことです。方向の変更後、フラグメントへの参照はnullになり、getItem()は再度呼び出されません。
タグを取得するためにFragmentPagerAdapterの実装に依存しないアプローチを次に示します。 getItem()から作成されたフラグメントまたはフラグメントマネージャから見つかったフラグメントを返すinstantiateItem()をオーバーライドします。
@Override
public Object instantiateItem(ViewGroup container, int position) {
Object value = super.instantiateItem(container, position);
if (position == 0) {
someFragment = (SomeFragment) value;
} else if (position == 1) {
anotherFragment = (AnotherFragment) value;
}
return value;
}
@ personne3000が提案する解決策は素晴らしいですが、1つの問題があります。アクティビティがバックグラウンドになり、システムによって(空きメモリを確保するために)強制終了され、復元されると、fragmentReferences
が空になり、 getItem
が呼び出されないためです。
以下のクラスは、このような状況を処理します。
public abstract class AbstractHolderFragmentPagerAdapter<F extends Fragment> extends FragmentPagerAdapter {
public static final String FRAGMENT_SAVE_PREFIX = "holder";
private final FragmentManager fragmentManager; // we need to store fragment manager ourselves, because parent's field is private and has no getters.
public AbstractHolderFragmentPagerAdapter(FragmentManager fm) {
super(fm);
fragmentManager = fm;
}
private SparseArray<WeakReference<F>> holder = new SparseArray<WeakReference<F>>();
protected void holdFragment(F fragment) {
holdFragment(holder.size(), fragment);
}
protected void holdFragment(int position, F fragment) {
if (fragment != null)
holder.put(position, new WeakReference<F>(fragment));
}
public F getHoldedItem(int position) {
WeakReference<F> ref = holder.get(position);
return ref == null ? null : ref.get();
}
public int getHolderCount() {
return holder.size();
}
@Override
public void restoreState(Parcelable state, ClassLoader loader) { // code inspired by Google's FragmentStatePagerAdapter implementation
super.restoreState(state, loader);
Bundle bundle = (Bundle) state;
for (String key : bundle.keySet()) {
if (key.startsWith(FRAGMENT_SAVE_PREFIX)) {
int index = Integer.parseInt(key.substring(FRAGMENT_SAVE_PREFIX.length()));
Fragment f = fragmentManager.getFragment(bundle, key);
holdFragment(index, (F) f);
}
}
}
@Override
public Parcelable saveState() {
Bundle state = (Bundle) super.saveState();
if (state == null)
state = new Bundle();
for (int i = 0; i < holder.size(); i++) {
int id = holder.keyAt(i);
final F f = getHoldedItem(i);
String key = FRAGMENT_SAVE_PREFIX + id;
fragmentManager.putFragment(state, key, f);
}
return state;
}
}
子フラグメントまたはプライマリ(現在表示されている)フラグメントにアクセスする必要がある場合は、常にこの基本クラスを使用します。上書きされたメソッドは、新しいフラグメントインスタンスが作成されたときとインスタンスがFragmentManagerから受信されたときの両方で呼び出されるため、実装の詳細に依存せず、ライフサイクルの変更に注意します。
public abstract class FragmentPagerAdapterExt extends FragmentPagerAdapter {
private final ArrayList<Fragment> mFragments;
private Fragment mPrimaryFragment;
public FragmentPagerAdapterExt(FragmentManager fm) {
super(fm);
mFragments = new ArrayList<>(getCount());
}
@Override public Object instantiateItem(ViewGroup container, int position) {
Object object = super.instantiateItem(container, position);
mFragments.add((Fragment) object);
return object;
}
@Override public void destroyItem(ViewGroup container, int position, Object object) {
mFragments.remove(object);
super.destroyItem(container, position, object);
}
@Override public void setPrimaryItem(ViewGroup container, int position, Object object) {
super.setPrimaryItem(container, position, object);
mPrimaryFragment = (Fragment) object;
}
/** Returns currently visible (primary) fragment */
public Fragment getPrimaryFragment() {
return mPrimaryFragment;
}
/** Returned list can contain null-values for not created fragments */
public List<Fragment> getFragments() {
return Collections.unmodifiableList(mFragments);
}
}
タグの代わりにIDを使用して、この問題を解決できました。 (私はカスタムフラグメントを使用するFragmentStatePagerAdapterを使用しています。このフラグメントでは、onAttachメソッドをオーバーライドし、IDをどこかに保存します。
@Override
public void onAttach(Context context){
super.onAttach(context);
MainActivity.fragId = getId();
}
そして、アクティビティ内でフラグメントに簡単にアクセスします。
Fragment f = getSupportFragmentManager.findFragmentById(fragId);
FragmentPagerAdapterからフラグメントを返す方法については、 この投稿 をご覧ください。フラグメントのインデックスを知っていることに依存していますが、これはgetItem()で設定されます(インスタンス化時のみ)
このクラスは、内部タグに依存せずにトリックを実行します。警告:フラグメントには、getItemメソッドではなくgetFragmentメソッドを使用してアクセスする必要があります。
public class ViewPagerAdapter extends FragmentPagerAdapter {
private final Map<Integer, Reference<Fragment>> fragments = new HashMap<>();
private final List<Callable0<Fragment>> initializers = new ArrayList<>();
private final List<String> titles = new ArrayList<>();
public ViewPagerAdapter(FragmentManager fm) {
super(fm);
}
void addFragment(Callable0<Fragment> initializer, String title) {
initializers.add(initializer);
titles.add(title);
}
public Optional<Fragment> getFragment(int position) {
return Optional.ofNullable(fragments.get(position).get());
}
@Override
public Fragment getItem(int position) {
Fragment fragment = initializers.get(position).execute();
return fragment;
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
Fragment fragment = (Fragment) super.instantiateItem(container, position);
fragments.put(position, new WeakReference<>(fragment));
return fragment;
}
@Override
public int getCount() {
return initializers.size();
}
@Override
public CharSequence getPageTitle(int position) {
return titles.get(position);
}
}
これが最良のアプローチであるかどうかはわかりませんが、他に何もうまくいきませんでした。 getActiveFragmentを含む他のすべてのオプションがnullを返すか、アプリがクラッシュしました。
画面の回転でフラグメントが添付されていることに気付いたので、フラグメントを使用してフラグメントをアクティビティに送り返しました。
フラグメント内:
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
mListener = (OnListInteractionListener) activity;
mListener.setListFrag(this);
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString()
+ " must implement OnFragmentInteractionListener");
}
}
次に、アクティビティで:
@Override
public void setListFrag(MyListFragment lf) {
if (mListFragment == null) {
mListFragment = lf;
}
}
最後に、アクティビティonCreate()で:
if (savedInstanceState != null) {
if (mListFragment != null)
mListFragment.setListItems(items);
}
このアプローチでは、新しいフラグメントを作成せずに、実際の表示可能なフラグメントをアクティビティに添付します。
私はJava/Androidの比較的初心者なので、私の方法がこれを行うのに正しいか最良の方法であるかはわかりませんが、うまくいきました(オブジェクト指向の原則に違反していると確信していますが、私のユースケースでは他のソリューションはうまくいきませんでした)。
FragmentStatePagerAdapterでViewPagerを使用するホスティングアクティビティがありました。 FragmentStatePagerAdapterによって作成されたFragmentsへの参照を取得するために、フラグメントクラス内にコールバックインターフェイスを作成しました。
public interface Callbacks {
public void addFragment (Fragment fragment);
public void removeFragment (Fragment fragment);
}
ホスティングアクティビティで、インターフェイスを実装し、LinkedHasSetを作成してフラグメントを追跡しました。
public class HostingActivity extends AppCompatActivity implements ViewPagerFragment.Callbacks {
private LinkedHashSet<Fragment> mFragments = new LinkedHashSet<>();
@Override
public void addFragment (Fragment fragment) {
mFragments.add(fragment);
}
@Override
public void removeFragment (Fragment fragment) {
mFragments.remove(fragment);
}
}
ViewPagerFragmentクラス内で、onAttach内のリストにフラグメントを追加し、onDetach内でそれらを削除しました。
public class ViewPagerFragment extends Fragment {
private Callbacks mCallbacks;
public interface Callbacks {
public void addFragment (Fragment fragment);
public void removeFragment (Fragment fragment);
}
@Override
public void onAttach (Context context) {
super.onAttach(context);
mCallbacks = (Callbacks) context;
// Add this fragment to the HashSet in the hosting activity
mCallbacks.addFragment(this);
}
@Override
public void onDetach() {
super.onDetach();
// Remove this fragment from the HashSet in the hosting activity
mCallbacks.removeFragment(this);
mCallbacks = null;
}
}
ホスティングアクティビティ内で、mFragmentsを使用して、FragmentStatePagerAdapterに現在存在するフラグメントを反復処理できるようになります。