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
  1.     712     void moveToState(Fragment f, int newState, int transit, int transitionStyle) {  
  2. ...  
  3.     722         if (f.mState < newState) {  
  4. ...  
  5.     781                 case Fragment.CREATED:  
  6.     782                     if (newState > Fragment.CREATED) {  
  7. ...  
  8.     819                         if (f.mView != null) {  
  9.     820                             f.restoreViewState();  
  10.     821                         }  
  11. ...  
  12.     850         } else if (f.mState > newState) {  
  13.     851             switch (f.mState) {  
  14. ...  
  15.     873                 case Fragment.STOPPED:  
  16.     874                 case Fragment.ACTIVITY_CREATED:  
  17.     875                     if (newState < Fragment.ACTIVITY_CREATED) {  
  18.     876                         if (DEBUG) Log.v(TAG, "movefrom ACTIVITY_CREATED: " + f);  
  19.     877                         if (f.mView != null) {  
  20.     878                             // Need to save the current view state if not  
  21.     879                             // done already.  
  22.     880                             if (!mActivity.isFinishing() && f.mSavedViewState == null) {  
  23.     881                                 saveFragmentViewState(f);  
  24.     882                             }  
  25.     883                         }  
Fragment が View を持っている場合は saveFragmentViewState() というメソッドを呼んでいます。

このメソッドでは

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/FragmentManager.java#1435
  1. 1435     void saveFragmentViewState(Fragment f) {  
  2. 1436         if (f.mView == null) {  
  3. 1437             return;  
  4. 1438         }  
  5. 1439         if (mStateArray == null) {  
  6. 1440             mStateArray = new SparseArray<Parcelable>();  
  7. 1441         } else {  
  8. 1442             mStateArray.clear();  
  9. 1443         }  
  10. 1444         f.mView.saveHierarchyState(mStateArray);  
  11. 1445         if (mStateArray.size() > 0) {  
  12. 1446             f.mSavedViewState = mStateArray;  
  13. 1447             mStateArray = null;  
  14. 1448         }  
  15. 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
  1. 9787     public void saveHierarchyState(SparseArray<Parcelable> container) {  
  2. 9788         dispatchSaveInstanceState(container);  
  3. 9789     }  
  1. 9802     protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {  
  2. 9803         if (mID != NO_ID && (mViewFlags & SAVE_DISABLED_MASK) == 0) {  
  3. 9804             mPrivateFlags &= ~SAVE_STATE_CALLED;  
  4. 9805             Parcelable state = onSaveInstanceState();  
  5. 9806             if ((mPrivateFlags & SAVE_STATE_CALLED) == 0) {  
  6. 9807                 throw new IllegalStateException(  
  7. 9808                         "Derived class did not call super.onSaveInstanceState()");  
  8. 9809             }  
  9. 9810             if (state != null) {  
  10. 9811                 // Log.i("View", "Freezing #" + Integer.toHexString(mID)  
  11. 9812                 // + ": " + state);  
  12. 9813                 container.put(mID, state);  
  13. 9814             }  
  14. 9815         }  
  15. 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
  1.    2294     @Override  
  2.    2295     protected void dispatchSaveInstanceState(SparseArray<parcelable> container) {  
  3.    2296         super.dispatchSaveInstanceState(container);  
  4.    2297         final int count = mChildrenCount;  
  5.    2298         final View[] children = mChildren;  
  6.    2299         for (int i = 0; i < count; i++) {  
  7.    2300             View c = children[i];  
  8.    2301             if ((c.mViewFlags & PARENT_SAVE_DISABLED_MASK) != PARENT_SAVE_DISABLED) {  
  9.    2302                 c.dispatchSaveInstanceState(container);  
  10.    2303             }  
  11.    2304         }  
  12.    2305     }  
  13. </parcelable>  
自身だけでなく子 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
  1. 586     final void restoreViewState() {  
  2. 587         if (mSavedViewState != null) {  
  3. 588             mView.restoreHierarchyState(mSavedViewState);  
  4. 589             mSavedViewState = null;  
  5. 590         }  
  6. 591     }  


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





0 件のコメント:

コメントを投稿