2012年4月18日水曜日

Android Fragment は破棄時に保持している View の状態を保存させている

前々回(Android Fragment で setArguments() してるサンプルが多いのはなぜ?)に Argument を使えば再生成時に値を引き継げることを書きました。
前回(Android レイアウトから生成した Fragment は FragmentTransaction の対象にしてはいけない)はレイアウトから生成した Fragment には setArguments() できないことを書きました。

では、レイアウトから生成した Fragment で再生成時に以前の状態を引き継ぐにはどうしたらいいのか、ということなんですが、Activity と同じように onSaveInstanceState(Bundle outState) 時に引数で渡される Bundle に入れておけば onCreate(Bundle), onCreateView(LayoutInflater, ViewGroup, Bundle), onActivityCreated(Bundle) のときに渡される Bundle から取り出すことができます。

自分の Fragment が持つ状態(例えばフィールドの値など)はこの方法で引き継ぐのが普通ですが、独自の View を使っていて、その View の状態(つまりカスタムビュークラスのフィールド値など)を引き継ぐには、View の onSaveInstanceState() で引き継ぎたい値を格納した Parcelable を返すのが普通です。

例えば、TextView では現在のカーソルの位置を、ListView では現在選択されているアイテムのIDなどを Parcelable として格納しています。

この View の onSaveInstanceState() は saveHierarchyState(SparseArray<Parcelable> container) をトリガーとして呼ばれます。

Fragment が破棄される部分のコード(#873以後)を見てみると

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/FragmentManager.java#874 712 void moveToState(Fragment f, int newState, int transit, int transitionStyle) { ... 722 if (f.mState < newState) { ... 781 case Fragment.CREATED: 782 if (newState > Fragment.CREATED) { ... 819 if (f.mView != null) { 820 f.restoreViewState(); 821 } ... 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) { 876 if (DEBUG) Log.v(TAG, "movefrom ACTIVITY_CREATED: " + f); 877 if (f.mView != null) { 878 // Need to save the current view state if not 879 // done already. 880 if (!mActivity.isFinishing() && f.mSavedViewState == null) { 881 saveFragmentViewState(f); 882 } 883 } Fragment が View を持っている場合は saveFragmentViewState() というメソッドを呼んでいます。

このメソッドでは

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/FragmentManager.java#1435 1435 void saveFragmentViewState(Fragment f) { 1436 if (f.mView == null) { 1437 return; 1438 } 1439 if (mStateArray == null) { 1440 mStateArray = new SparseArray<Parcelable>(); 1441 } else { 1442 mStateArray.clear(); 1443 } 1444 f.mView.saveHierarchyState(mStateArray); 1445 if (mStateArray.size() > 0) { 1446 f.mSavedViewState = mStateArray; 1447 mStateArray = null; 1448 } 1449 } このように、View の saveHierarchyState() が呼ばれていることがわかります。

View の saveHierarchyState() では

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/view/View.java#9787 9787 public void saveHierarchyState(SparseArray<Parcelable> container) { 9788 dispatchSaveInstanceState(container); 9789 } 9802 protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) { 9803 if (mID != NO_ID && (mViewFlags & SAVE_DISABLED_MASK) == 0) { 9804 mPrivateFlags &= ~SAVE_STATE_CALLED; 9805 Parcelable state = onSaveInstanceState(); 9806 if ((mPrivateFlags & SAVE_STATE_CALLED) == 0) { 9807 throw new IllegalStateException( 9808 "Derived class did not call super.onSaveInstanceState()"); 9809 } 9810 if (state != null) { 9811 // Log.i("View", "Freezing #" + Integer.toHexString(mID) 9812 // + ": " + state); 9813 container.put(mID, state); 9814 } 9815 } 9816 } このように、onSaveInstanceState() を呼んで返ってきた Parcelable を引数の SparseArray に格納しています。

ViewGroup ではこの dispatchSaveInstanceState() が Override されており

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/view/ViewGroup.java#2294 2294 @Override 2295 protected void dispatchSaveInstanceState(SparseArray container) { 2296 super.dispatchSaveInstanceState(container); 2297 final int count = mChildrenCount; 2298 final View[] children = mChildren; 2299 for (int i = 0; i < count; i++) { 2300 View c = children[i]; 2301 if ((c.mViewFlags & PARENT_SAVE_DISABLED_MASK) != PARENT_SAVE_DISABLED) { 2302 c.dispatchSaveInstanceState(container); 2303 } 2304 } 2305 } 自身だけでなく子 View の dispatchSaveInstanceState() も呼ぶようになっています。

onSavedInstanceState() で返した Parcelable は onRestoreInstanceState(Parcelable state) で取り出すことができます。このメソッドは restoreHierarchyState(SparseArray<Parcelable> container) をトリガーとして呼ばれます。そして、Fragment が生成されるときに呼ばれる restoreViewState() メソッド内で restoreHierarchyState() を呼んでいます。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/Fragment.java#586 586 final void restoreViewState() { 587 if (mSavedViewState != null) { 588 mView.restoreHierarchyState(mSavedViewState); 589 mSavedViewState = null; 590 } 591 }

つまり、カスタムビューを作る場合、その中だけで引き継ぎが完結する状態(private なフィールド値など)は View の onSaveInstanceState() を利用しましょう。そうすることで Fragment が View のフィールド値をわざわざ取り出して引き継ぐような依存性を回避できます。





0 件のコメント:

コメントを投稿