2012年6月19日火曜日

Android DialogFragment では Dialog のサイズ指定は onActivityCreated でやれ

前のエントリでダイアログのサイズを指定する方法を紹介しましたが、 (Y.A.M の 雑記帳: Android Dialog の大きさを自分で設定する -) これを DialogFragment で行う場合、ちょっと注意が必要です。

DialogFragment には表示するダイアログのインスタンスを自分で作成するための onCreateDialog() というメソッドが用意されています。
このメソッドを Override して、任意の Dialog クラスのインスタンスを返すことでオリジナルのテーマを適用したダイアログを表示することができます。
  1. public class MyDialogFragment extends DialogFragment {  
  2.       
  3.     @Override  
  4.     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {  
  5.   
  6.         // ダイアログの中に表示する View を返す  
  7.         ...  
  8.     }  
  9.       
  10.     @Override  
  11.     public Dialog onCreateDialog(Bundle savedInstanceState) {  
  12.         Dialog dialog = new Dialog(getActivity(), R.style.MyDialogTheme);  
  13.   
  14.         return dialog;  
  15.     }  
  16. }  
こうすると、MyDialogTheme を適用したダイアログになります。
  1. <style name="MyDialogTheme" parent="@android:style/Theme.Holo.Light.Dialog">  
  2.     <item name="android:windowActionBar">false</item>  
  3.     <item name="android:windowNoTitle">true</item>  
  4. </style>  
とかすれば、タイトル部分のないダイアログにすることができます。


ダイアログのサイズを指定する方法で、この MyDialogFragment の大きさを指定しようとした場合、
  1. public class MyDialogFragment extends DialogFragment {  
  2.       
  3.     @Override  
  4.     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {  
  5.   
  6.         // ダイアログの中に表示する View を返す  
  7.         ...  
  8.     }  
  9.       
  10.     @Override  
  11.     public Dialog onCreateDialog(Bundle savedInstanceState) {  
  12.         Dialog dialog = new Dialog(getActivity(), R.style.MyDialogTheme);  
  13.   
  14.         WindowManager.LayoutParams lp = dialog.getWindow().getAttributes();  
  15.           
  16.         DisplayMetrics metrics = getResources().getDisplayMetrics();  
  17.         int dialogWidth = (int) (metrics.widthPixels * 0.8);  
  18.         int dialogHeight = (int) (metrics.heightPixels * 0.8);  
  19.           
  20.         lp.width = dialogWidth;  
  21.         lp.height = dialogHeight;  
  22.         dialog.getWindow().setAttributes(lp);  
  23.           
  24.         return dialog;  
  25.     }  
  26. }  
としてもなぜか適用されません。

DialogFragment の実装をみると getLayoutInflater() でフィールドの mDialog に onCreateDialog() の戻り値を格納しています。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/DialogFragment.java#391
  1.     391     /** @hide */  
  2.     392     @Override  
  3.     393     public LayoutInflater getLayoutInflater(Bundle savedInstanceState) {  
  4.     394         if (!mShowsDialog) {  
  5.     395             return super.getLayoutInflater(savedInstanceState);  
  6.     396         }  
  7.     397   
  8.     398         mDialog = onCreateDialog(savedInstanceState);  
  9. ...  
  10.     415     }  
この getLayoutInflater() を呼び出す部分(FragmentManager)をみると

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/FragmentManager.java#795
  1. 795                             f.mView = f.onCreateView(f.getLayoutInflater(f.mSavedFragmentState),  
  2. 796                                     container, f.mSavedFragmentState);  
getLayoutInflater() を呼び出して、その結果を引数として onCreateView() を呼んでいます。

この後に DialogFragment の onActivityCreated() が呼ばれ、ここで onCreateView() で返した View がダイアログに setContentView() でセットされます。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/DialogFragment.java#456
  1. 456     @Override  
  2. 457     public void onActivityCreated(Bundle savedInstanceState) {  
  3. 458         super.onActivityCreated(savedInstanceState);  
  4. 459   
  5. 460         if (!mShowsDialog) {  
  6. 461             return;  
  7. 462         }  
  8. 463   
  9. 464         View view = getView();  
  10. 465         if (view != null) {  
  11. 466             if (view.getParent() != null) {  
  12. 467                 throw new IllegalStateException("DialogFragment can not be attached to a container view");  
  13. 468             }  
  14. 469             mDialog.setContentView(view);  
  15. 470         }  
  16.   
  17. 483     }  
http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/Fragment.java#1106
  1. 1106     public View getView() {  
  2. 1107         return mView;  
  3. 1108     }  
つまり流れとしては

getLayoutInflater()
  → onCreateDialog()

onCreateView()

onActivityCreated()

のような順番になります。onCreateDialog() が一番最初です。

onActivityCreated() で Dialog の setContentView() が呼ばれるわけですが、Dialog の setContentView() は Window の setContentView() を呼び出しているだけです。
  1.  84     Window mWindow;  
  2.   
  3. 146     Dialog(Context context, int theme, boolean createContextWrapper) {  
  4.   
  5. 155         mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);  
  6. 156         Window w = PolicyManager.makeNewWindow(mContext);  
  7. 157         mWindow = w;  
  8.   
  9. 163     }  
  10.   
  11. 466     public void setContentView(View view) {  
  12. 467         mWindow.setContentView(view);  
  13. 468     }  
Window クラスの setContentView() は abstract メソッドで、

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/view/Window.java#922
  1. 922     public abstract void setContentView(View view);  
実際は Window を継承した PhoneWindow などに実装があります。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/policy/src/com/android/internal/policy/impl/PhoneWindow.java#264
  1. 258     @Override  
  2. 259     public void setContentView(View view) {  
  3. 260         setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));  
  4. 261     }  
  5. 262   
  6. 263     @Override  
  7. 264     public void setContentView(View view, ViewGroup.LayoutParams params) {  
  8. 265         if (mContentParent == null) {  
  9. 266             installDecor();  
  10. 267         } else {  
  11. 268             mContentParent.removeAllViews();  
  12. 269         }  
  13. 270         mContentParent.addView(view, params);  
  14. 271         final Callback cb = getCallback();  
  15. 272         if (cb != null && !isDestroyed()) {  
  16. 273             cb.onContentChanged();  
  17. 274         }  
  18. 275     }  
ここの setContentView() で呼ばれる installDecor() で呼ばれる generateLayout() で
  1. 2738     private void installDecor() {  
  2. 2739         if (mDecor == null) {  
  3. 2740             mDecor = generateDecor();  
  4. 2741             mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);  
  5. 2742             mDecor.setIsRootNamespace(true);  
  6. 2743         }  
  7. 2744         if (mContentParent == null) {  
  8. 2745             mContentParent = generateLayout(mDecor);  
  9. 2746   
  1. 2498     protected ViewGroup generateLayout(DecorView decor) {  
  2. 2499         // Apply data from current theme.  
  3. 2500   
  4. 2501         TypedArray a = getWindowStyle();  
  5. 2502   
  6. 2503         if (false) {  
  7. 2504             System.out.println("From style:");  
  8. 2505             String s = "Attrs:";  
  9. 2506             for (int i = 0; i < com.android.internal.R.styleable.Window.length; i++) {  
  10. 2507                 s = s + " " + Integer.toHexString(com.android.internal.R.styleable.Window[i]) + "="  
  11. 2508                         + a.getString(i);  
  12. 2509             }  
  13. 2510             System.out.println(s);  
  14. 2511         }  
  15. 2512   
  16. 2513         mIsFloating = a.getBoolean(com.android.internal.R.styleable.Window_windowIsFloating, false);  
  17. 2514         int flagsToUpdate = (FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR)  
  18. 2515                 & (~getForcedWindowFlags());  
  19. 2516         if (mIsFloating) {  
  20. 2517             setLayout(WRAP_CONTENT, WRAP_CONTENT);  
  21. 2518             setFlags(0, flagsToUpdate);  
  22. 2519         } else {  
  23. 2520             setFlags(FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR, flagsToUpdate);  
  24. 2521         }  
<item name="android:windowIsFloating">true</item> がスタイルに定義されていた場合、setLayout(WRAP_CONTENT, WRAP_CONTENT); を呼んでいます。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/view/Window.java#604
  1. 604     public void setLayout(int width, int height) {  
  2. 605         final WindowManager.LayoutParams attrs = getAttributes();  
  3. 606         attrs.width = width;  
  4. 607         attrs.height = height;  
  5. 608         if (mCallback != null) {  
  6. 609             mCallback.onWindowAttributesChanged(attrs);  
  7. 610         }  
  8. 611     }  
ありましたー。そう、Dialog の setContentView() を呼び出すと、ここで WindowManager.LayoutParams に WRAP_CONTENT, WRAP_CONTENT がセットされてしまうのです。

ということで、意図通りにするには
  1. public class MyDialogFragment extends DialogFragment {  
  2.       
  3.     @Override  
  4.     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {  
  5.   
  6.         // ダイアログの中に表示する View を返す  
  7.         ...  
  8.     }  
  9.   
  10.     @Override  
  11.     public void onActivityCreated(Bundle savedInstanceState) {  
  12.         super.onActivityCreated(savedInstanceState);  
  13.           
  14.         Dialog dialog = getDialog();  
  15.           
  16.         WindowManager.LayoutParams lp = dialog.getWindow().getAttributes();  
  17.           
  18.         DisplayMetrics metrics = getResources().getDisplayMetrics();  
  19.         int dialogWidth = (int) (metrics.widthPixels * 0.8);  
  20.         int dialogHeight = (int) (metrics.heightPixels * 0.8);  
  21.           
  22.         lp.width = dialogWidth;  
  23.         lp.height = dialogHeight;  
  24.         dialog.getWindow().setAttributes(lp);  
  25.     }  
  26.   
  27.     @Override  
  28.     public Dialog onCreateDialog(Bundle savedInstanceState) {  
  29.         Dialog dialog = new Dialog(getActivity(), R.style.MyDialogTheme);  
  30.           
  31.         return dialog;  
  32.     }  
のように super.onActivityCreated() を呼んだ後に getDialog() で Dialog のインスタンスを取得して、それに対して WindowManager.LayoutParams をセットすれば OK です!










2012年6月11日月曜日

Fragment から Activity にコールバックする方法

Fragment から Activity にコールバックしたいときに、例えばこんな感じで実装することができます。

  1. public class MainActivity extends Activity {  
  2.   
  3.     @Override  
  4.     public void onCreate(Bundle savedInstanceState) {  
  5.         super.onCreate(savedInstanceState);  
  6.   
  7.         MainFragment fragment = new MainFragment();  
  8.   
  9.         fragment.setOnOkBtnClickListener(new MainFragment.OnOkBtnClickListener() {  
  10.   
  11.             @Override  
  12.             public void onOkClicked() {  
  13.                 // TODO Auto-generated method stub  
  14.   
  15.             }  
  16.         });  
  17.   
  18.         getFragmentManager().beginTransaction().add(android.R.id.content, fragment, "MainFragment").commit();  
  19.     }  
  20. }  
  1. public class MainFragment extends Fragment {  
  2.   
  3.     public interface OnOkBtnClickListener {  
  4.         public void onOkClicked();  
  5.     }  
  6.   
  7.     private OnOkBtnClickListener mListener;  
  8.       
  9.     public void setOnOkBtnClickListener(OnOkBtnClickListener l) {  
  10.         mListener = l;  
  11.     }  
  12.       
  13.     @Override  
  14.     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {  
  15.         Button okBtn = new Button(inflater.getContext());  
  16.         okBtn.setOnClickListener(new View.OnClickListener() {  
  17.               
  18.             @Override  
  19.             public void onClick(View v) {  
  20.                 if(mListener != null) {  
  21.                     mListener.onOkClicked();  
  22.                 }  
  23.             }  
  24.         });  
  25.           
  26.         return okBtn;  
  27.     }  
  28. }  
このコードには問題があります。
バックグラウンドにある Fragment はメモリが足りなくなると、システムによって破棄され、必要になったときにシステムによって再生成されることがあるため、その際 setOnOkBtnClickListener() 部分は呼ばれないのです。

そのため、システムによって再生成された場合、コールバックを受け取れなくなるということが起こりえます。

以前のエントリで書きましたが、レイアウトで定義した(<fragment> で定義した)Fragment は FragmentTransaction の対象にしてはいけないため、上記のコードの MainFragment が onCreate() 内で生成されるのではなく、レイアウトXMLファイルで定義されている場合は、問題になることは(たぶん)あまりないと思います。


ただ、いずれにしてもこの実装はさけた方が懸命です。 (レイアウトで定義した Fragment を使っているとはいえ、Android UI Cookbook で Activity 側から fragment にリスナーをセットするコードを載せてしまっているので、動的に生成した Fragment には応用しないでください。ごめんなさい。)

で、どうするかというと、Activity 自体にリスナーを実装するようにします。
(私はあまりこの書き方は好きじゃないのですが、Fragment がシステムから再生成されうるのでしょうがないです。。。)

  1. public class MainActivity2 extends Activity implements MainFragment.OnOkBtnClickListener {  
  2.   
  3.     @Override  
  4.     public void onCreate(Bundle savedInstanceState) {  
  5.         super.onCreate(savedInstanceState);  
  6.   
  7.         MainFragment fragment = new MainFragment();  
  8.         getFragmentManager().beginTransaction().add(android.R.id.content, fragment, "MainFragment").commit();  
  9.     }  
  10.   
  11.     @Override  
  12.     public void onOkClicked() {  
  13.         // TODO Auto-generated method stub  
  14.           
  15.     }  
  16. }  
  1. public class MainFragment2 extends Fragment {  
  2.   
  3.     public interface OnOkBtnClickListener {  
  4.         public void onOkClicked();  
  5.     }  
  6.   
  7.     private OnOkBtnClickListener mListener;  
  8.   
  9.     @Override  
  10.     public void onAttach(Activity activity) {  
  11.         super.onAttach(activity);  
  12.   
  13.         if (activity instanceof OnOkBtnClickListener == false) {  
  14.             throw new ClassCastException("activity が OnOkBtnClickListener を実装していません.");  
  15.         }  
  16.           
  17.         mListener = ((OnOkBtnClickListener) activity);  
  18.     }  
  19.   
  20.     @Override  
  21.     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {  
  22.         Button okBtn = new Button(inflater.getContext());  
  23.         okBtn.setOnClickListener(new View.OnClickListener() {  
  24.   
  25.             @Override  
  26.             public void onClick(View v) {  
  27.                 if (mListener != null) {  
  28.                     mListener.onOkClicked();  
  29.                 }  
  30.             }  
  31.         });  
  32.   
  33.         return okBtn;  
  34.     }  
  35. }  
この場合、Fragment がシステムから再生成されたときも onAttach() を通るので、リスナーがセットされないということにはなりません。
Fragment が Activity の実装に依存しないので、複数の Activity で Fragment を使い回すときに便利です。


Fragment が必ず特定の Activity にしかアタッチされない場合は、次の方法も使えます。

  1. public class MainActivity3 extends Activity {  
  2.   
  3.     @Override  
  4.     public void onCreate(Bundle savedInstanceState) {  
  5.         super.onCreate(savedInstanceState);  
  6.   
  7.         MainFragment fragment = new MainFragment();  
  8.         getFragmentManager().beginTransaction().add(android.R.id.content, fragment, "MainFragment").commit();  
  9.     }  
  10.   
  11.     public void onOkClicked() {  
  12.         // TODO Auto-generated method stub  
  13.           
  14.     }  
  15. }  
  1. public class MainFragment3 extends Fragment {  
  2.   
  3.     @Override  
  4.     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {  
  5.         Button okBtn = new Button(inflater.getContext());  
  6.         okBtn.setOnClickListener(new View.OnClickListener() {  
  7.   
  8.             @Override  
  9.             public void onClick(View v) {  
  10.                 try {  
  11.                     MainActivity3 activity = (MainActivity3) getActivity();  
  12.                     activity.onOkClicked();  
  13.                 } catch (ClassCastException e) {  
  14.                     throw new ClassCastException("activity が OnOkBtnClickListener を実装していません.");  
  15.                 }  
  16.             }  
  17.         });  
  18.   
  19.         return okBtn;  
  20.     }  
  21. }  
これが一番コードが短くてすみます。



これがベスト!という実装方法がないんですよねー。。。そもそも Fragment のコンストラクタは空コンストラクタでないとダメだよとかって制限があるのに、作れてしまったりするというがフレームワーク的にどうなのよ、と。
しかもシステムに再生成されない限りは普通に動いちゃうので、ぜったいはまるだろこれ、って感じなのです。