前回のエントリで Fragment の Arguments の利点をいろいろ紹介しましたが、レイアウト内に <fragment> タグで定義して生成した Fragment には setArguments() をすることができません。
まず、Fragment.java のコードをみると Arguments を保持するフィールドである mArguments のコメントとして“生成時の引数である”と書いてあります。
http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/Fragment.java#370
- 370 // Construction arguments;
- 371 Bundle mArguments;
さらに、setArguments() の実装をみると
http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/Fragment.java#652
- 659 public void setArguments(Bundle args) {
- 660 if (mIndex >= 0) {
- 661 throw new IllegalStateException("Fragment already active");
- 662 }
- 663 mArguments = args;
- 664 }
- 665
では、mIndex (初期値は -1)はいつセットされるのかというと、FragmentManager の makeActive() メソッドで行われます。
http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/FragmentManager.java#1009
- 1009 void makeActive(Fragment f) {
- 1010 if (f.mIndex >= 0) {
- 1011 return;
- 1012 }
- 1013
- 1014 if (mAvailIndices == null || mAvailIndices.size() <= 0) {
- 1015 if (mActive == null) {
- 1016 mActive = new ArrayList<fragment>();
- 1017 }
- 1018 f.setIndex(mActive.size());
- 1019 mActive.add(f);
- 1020
- 1021 } else {
- 1022 f.setIndex(mAvailIndices.remove(mAvailIndices.size()-1));
- 1023 mActive.set(f.mIndex, f);
- 1024 }
- 1025 }
- ragment>
http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/Fragment.java#593
- 593 final void setIndex(int index) {
- 594 mIndex = index;
- 595 mWho = "android:fragment:" + mIndex;
- 596 }
http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/FragmentManager.java#1042
- 1042 public void addFragment(Fragment fragment, boolean moveToStateNow) {
- 1043 if (mAdded == null) {
- 1044 mAdded = new ArrayList<fragment>();
- 1045 }
- 1046 if (DEBUG) Log.v(TAG, "add: " + fragment);
- 1047 makeActive(fragment);
- 1048 if (!fragment.mDetached) {
- 1049 mAdded.add(fragment);
- 1050 fragment.mAdded = true;
- 1051 fragment.mRemoving = false;
- 1052 if (fragment.mHasMenu && fragment.mMenuVisible) {
- 1053 mNeedMenuInvalidate = true;
- 1054 }
- 1055 if (moveToStateNow) {
- 1056 moveToState(fragment);
- 1057 }
- 1058 }
- 1059 }
- </fragment>
http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/Activity.java#4189
- 4199 public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
- 4200 if (!"fragment".equals(name)) {
- 4201 return onCreateView(name, context, attrs);
- 4202 }
- ...
- 4223 Fragment fragment = id != View.NO_ID ? mFragments.findFragmentById(id) : null;
- ...
- 4234 if (fragment == null) {
- 4235 fragment = Fragment.instantiate(this, fname);
- 4236 fragment.mFromLayout = true;
- 4237 fragment.mFragmentId = id != 0 ? id : containerId;
- 4238 fragment.mContainerId = containerId;
- 4239 fragment.mTag = tag;
- 4240 fragment.mInLayout = true;
- 4241 fragment.mFragmentManager = mFragments;
- 4242 fragment.onInflate(this, attrs, fragment.mSavedFragmentState);
- 4243 mFragments.addFragment(fragment, true);
- ...
- 4263 }
- 4265 if (fragment.mView == null) {
- 4266 throw new IllegalStateException("Fragment " + fname
- 4267 + " did not create a view.");
- 4268 }
- 4269 if (id != 0) {
- 4270 fragment.mView.setId(id);
- 4271 }
- 4272 if (fragment.mView.getTag() == null) {
- 4273 fragment.mView.setTag(tag);
- 4274 }
- 4275 return fragment.mView;
- 4276 }
この onCreateView() は Activity 内での setContentView() をトリガーとして呼ばれます。
レイアウトから生成される Fragment に Argument をセットしないようになっているのは、必要がないからだと思います。そもそも単体で破棄される Fragment はバックスタックにある場合で、 Fragment のもっている View が Activity のレイアウトの一部になっている場合は Activity と一緒に破棄、再生成されます。 それならば setContentView() の後に FragmentManager#getFragmentById() で Fragment を取得して setter なりで値を渡せばいいわけです。
■ レイアウトから生成される Fragment はバックスタックに移動させないのが普通
実は、レイアウトに定義している Fragment に対し、単に FragmentTransaction の remove() を呼んだ場合、Fragment の保持している View のフィールドは null にセットされますが、レイアウトから View は削除されません。
FragmentManager の moveToState() メソッドを見てみましょう。
初期化の段階(#738)では、レイアウトから生成した Fragment(= mFromLayout が true)の場合この段階で onCreateView() から View を生成し、その処理については LayoutInflater にまかせています。 どういうことかというと、この段階で生成された View (mView として保持される)が Activity の onCreateView() での戻り値になるのです。
生成の段階(#781)では、レイアウトから生成していない Fragment であればコンテナ(Fragment の View の追加先の ViewGroup)の ID からコンテナのインスタンスを取得して mContainer として保持し、onCreateView() から生成した View をこのコンテナに追加しています。
破棄の段階(#874)では、Fragment の持つ View とそのコンテナが両方とも null ではない場合にコンテナから View を削除しています。レイアウトから生成した Fragment はコンテナが null のままなので、この if 文のなかには入らず View は削除されません。
http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/FragmentManager.java#712
- 712 void moveToState(Fragment f, int newState, int transit, int transitionStyle) {
- ...
- 722 if (f.mState < newState) {
- ...
- 737 switch (f.mState) {
- 738 case Fragment.INITIALIZING:
- ...
- 769 if (f.mFromLayout) {
- 770 // For fragments that are part of the content view
- 771 // layout, we need to instantiate the view immediately
- 772 // and the inflater will take care of adding it.
- 773 f.mView = f.onCreateView(f.getLayoutInflater(f.mSavedFragmentState),
- 774 null, f.mSavedFragmentState);
- 775 if (f.mView != null) {
- 776 f.mView.setSaveFromParentEnabled(false);
- 777 if (f.mHidden) f.mView.setVisibility(View.GONE);
- 778 f.onViewCreated(f.mView, f.mSavedFragmentState);
- 779 }
- 780 }
- 781 case Fragment.CREATED:
- 782 if (newState > Fragment.CREATED) {
- ...
- 784 if (!f.mFromLayout) {
- 785 ViewGroup container = null;
- 786 if (f.mContainerId != 0) {
- 787 container = (ViewGroup)mActivity.findViewById(f.mContainerId);
- ...
- 793 }
- 794 f.mContainer = container;
- 795 f.mView = f.onCreateView(f.getLayoutInflater(f.mSavedFragmentState),
- 796 container, f.mSavedFragmentState);
- 797 if (f.mView != null) {
- 798 f.mView.setSaveFromParentEnabled(false);
- 799 if (container != null) {
- ...
- 806 container.addView(f.mView);
- 807 }
- 808 if (f.mHidden) f.mView.setVisibility(View.GONE);
- 809 f.onViewCreated(f.mView, f.mSavedFragmentState);
- 810 }
- ...
- 811 }
- 812
- ...
- 849 }
- 850 } else if (f.mState > newState) {
- 851 switch (f.mState) {
- ...
- 873 case Fragment.STOPPED:
- 874 case Fragment.ACTIVITY_CREATED:
- 875 if (newState < Fragment.ACTIVITY_CREATED) {
- ...
- 890 if (f.mView != null && f.mContainer != null) {
- ...
- 918 f.mContainer.removeView(f.mView);
- 919 }
- 920 f.mContainer = null;
- 921 f.mView = null;
- 922 }
- ...
- 970 }
- 971 }
- 972
- 973 f.mState = newState;
- 974 }
- 975
例えば
- <?xml version="1.0" encoding="utf-8"?>
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- android:orientation="vertical" >
- <Button
- android:id="@+id/button"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:text="show dialog" />
- <FrameLayout
- android:id="@+id/container"
- android:layout_width="match_parent"
- android:layout_height="match_parent" >
- <fragment
- android:id="@+id/fragment"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- class="yanzm.example.dialogfragmentsample.MainActivity$MyFragment" />
- </FrameLayout>
- </LinearLayout>
- public class MainActivity extends Activity {
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.main);
- findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- switchFragment();
- }
- });
- }
- private void switchFragment() {
- Fragment fragment = new MyFragment2();
- getFragmentManager().beginTransaction().replace(R.id.container, fragment).commit();
- }
- public static class MyFragment extends Fragment {
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
- return inflater.inflate(R.layout.main2, container, false);
- }
- }
- public static class MyFragment2 extends Fragment {
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
- return inflater.inflate(R.layout.main4, container, false);
- }
- }
- }
Activity の onCreateView() をもう一度みてみましょう。
http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/Activity.java#4223
- 4223 Fragment fragment = id != View.NO_ID ? mFragments.findFragmentById(id) : null;
- 4224 if (fragment == null && tag != null) {
- 4225 fragment = mFragments.findFragmentByTag(tag);
- 4226 }
- 4227 if (fragment == null && containerId != View.NO_ID) {
- 4228 fragment = mFragments.findFragmentById(containerId);
- 4229 }
- ...
- 4234 if (fragment == null) {
- 4235 fragment = Fragment.instantiate(this, fname);
- ...
- 4263 }
- 4264
- 4265 if (fragment.mView == null) {
- 4266 throw new IllegalStateException("Fragment " + fname
- 4267 + " did not create a view.");
- 4268 }
1. android:id で指定されているID
2. 1. で見つからなかったら android:tag で指定されているタグ名
3. 2. でも見つからなかったら親 View の ID
ここで思いだして欲しいのが Fragment は id が明示的に指定されていない場合、親の id を自分の id として持つ、ということです。
上記のコードでは
- Fragment fragment = new MyFragment2(); getFragmentManager().beginTransaction().replace(R.id.container, fragment).commit();
3. 2. でも見つからなかったら親 View の ID
の段階で MyFragment2 のインスタンスが見つかってしまうということです。そのため、#4234 の if 文には入らず、Fragment はクラス名から生成されません。そのまま #4265 に行くのですが、MyFragment2 はレイアウトから生成されたわけではないので、この段階ではまだ View は生成されていません。そのため、IllegalStateException が投げられてしまうのです。
この流れについては、上記の
初期化の段階(#738)では、レイアウトから生成した Fragment(= mFromLayout が true)の場合この段階で onCreateView() から View を生成し、その処理については LayoutInflater にまかせています。 どういうことかというと、この段階で生成された View (mView として保持される)が Activity の onCreateView() での戻り値になるのです。
の部分を思い出してください。
結論としては
レイアウトから生成する Fragment は FragmentTransaction に対象にしない。FragmentTransaction で入れ替える Fragment はコードから生成する。
ということですね。
0 件のコメント:
コメントを投稿