2012年6月19日火曜日

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

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

DialogFragment には表示するダイアログのインスタンスを自分で作成するための onCreateDialog() というメソッドが用意されています。
このメソッドを Override して、任意の Dialog クラスのインスタンスを返すことでオリジナルのテーマを適用したダイアログを表示することができます。
public class MyDialogFragment extends DialogFragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // ダイアログの中に表示する View を返す ... } @Override public Dialog onCreateDialog(Bundle savedInstanceState) { Dialog dialog = new Dialog(getActivity(), R.style.MyDialogTheme); return dialog; } } こうすると、MyDialogTheme を適用したダイアログになります。
とかすれば、タイトル部分のないダイアログにすることができます。


ダイアログのサイズを指定する方法で、この MyDialogFragment の大きさを指定しようとした場合、 public class MyDialogFragment extends DialogFragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // ダイアログの中に表示する View を返す ... } @Override public Dialog onCreateDialog(Bundle savedInstanceState) { Dialog dialog = new Dialog(getActivity(), R.style.MyDialogTheme); WindowManager.LayoutParams lp = dialog.getWindow().getAttributes(); DisplayMetrics metrics = getResources().getDisplayMetrics(); int dialogWidth = (int) (metrics.widthPixels * 0.8); int dialogHeight = (int) (metrics.heightPixels * 0.8); lp.width = dialogWidth; lp.height = dialogHeight; dialog.getWindow().setAttributes(lp); return dialog; } } としてもなぜか適用されません。

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

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

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/FragmentManager.java#795 795 f.mView = f.onCreateView(f.getLayoutInflater(f.mSavedFragmentState), 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 456 @Override 457 public void onActivityCreated(Bundle savedInstanceState) { 458 super.onActivityCreated(savedInstanceState); 459 460 if (!mShowsDialog) { 461 return; 462 } 463 464 View view = getView(); 465 if (view != null) { 466 if (view.getParent() != null) { 467 throw new IllegalStateException("DialogFragment can not be attached to a container view"); 468 } 469 mDialog.setContentView(view); 470 } ... 483 } http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/Fragment.java#1106 1106 public View getView() { 1107 return mView; 1108 } つまり流れとしては

getLayoutInflater()
  → onCreateDialog()

onCreateView()

onActivityCreated()

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

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

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/view/Window.java#922 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 258 @Override 259 public void setContentView(View view) { 260 setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); 261 } 262 263 @Override 264 public void setContentView(View view, ViewGroup.LayoutParams params) { 265 if (mContentParent == null) { 266 installDecor(); 267 } else { 268 mContentParent.removeAllViews(); 269 } 270 mContentParent.addView(view, params); 271 final Callback cb = getCallback(); 272 if (cb != null && !isDestroyed()) { 273 cb.onContentChanged(); 274 } 275 } ここの setContentView() で呼ばれる installDecor() で呼ばれる generateLayout() で 2738 private void installDecor() { 2739 if (mDecor == null) { 2740 mDecor = generateDecor(); 2741 mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); 2742 mDecor.setIsRootNamespace(true); 2743 } 2744 if (mContentParent == null) { 2745 mContentParent = generateLayout(mDecor); 2746 ... 2498 protected ViewGroup generateLayout(DecorView decor) { 2499 // Apply data from current theme. 2500 2501 TypedArray a = getWindowStyle(); 2502 2503 if (false) { 2504 System.out.println("From style:"); 2505 String s = "Attrs:"; 2506 for (int i = 0; i < com.android.internal.R.styleable.Window.length; i++) { 2507 s = s + " " + Integer.toHexString(com.android.internal.R.styleable.Window[i]) + "=" 2508 + a.getString(i); 2509 } 2510 System.out.println(s); 2511 } 2512 2513 mIsFloating = a.getBoolean(com.android.internal.R.styleable.Window_windowIsFloating, false); 2514 int flagsToUpdate = (FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR) 2515 & (~getForcedWindowFlags()); 2516 if (mIsFloating) { 2517 setLayout(WRAP_CONTENT, WRAP_CONTENT); 2518 setFlags(0, flagsToUpdate); 2519 } else { 2520 setFlags(FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR, flagsToUpdate); 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 604 public void setLayout(int width, int height) { 605 final WindowManager.LayoutParams attrs = getAttributes(); 606 attrs.width = width; 607 attrs.height = height; 608 if (mCallback != null) { 609 mCallback.onWindowAttributesChanged(attrs); 610 } 611 } ありましたー。そう、Dialog の setContentView() を呼び出すと、ここで WindowManager.LayoutParams に WRAP_CONTENT, WRAP_CONTENT がセットされてしまうのです。

ということで、意図通りにするには public class MyDialogFragment extends DialogFragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // ダイアログの中に表示する View を返す ... } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); Dialog dialog = getDialog(); WindowManager.LayoutParams lp = dialog.getWindow().getAttributes(); DisplayMetrics metrics = getResources().getDisplayMetrics(); int dialogWidth = (int) (metrics.widthPixels * 0.8); int dialogHeight = (int) (metrics.heightPixels * 0.8); lp.width = dialogWidth; lp.height = dialogHeight; dialog.getWindow().setAttributes(lp); } @Override public Dialog onCreateDialog(Bundle savedInstanceState) { Dialog dialog = new Dialog(getActivity(), R.style.MyDialogTheme); return dialog; } のように super.onActivityCreated() を呼んだ後に getDialog() で Dialog のインスタンスを取得して、それに対して WindowManager.LayoutParams をセットすれば OK です!










2012年6月11日月曜日

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

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

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

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

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


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

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

public class MainActivity2 extends Activity implements MainFragment.OnOkBtnClickListener { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); MainFragment fragment = new MainFragment(); getFragmentManager().beginTransaction().add(android.R.id.content, fragment, "MainFragment").commit(); } @Override public void onOkClicked() { // TODO Auto-generated method stub } } public class MainFragment2 extends Fragment { public interface OnOkBtnClickListener { public void onOkClicked(); } private OnOkBtnClickListener mListener; @Override public void onAttach(Activity activity) { super.onAttach(activity); if (activity instanceof OnOkBtnClickListener == false) { throw new ClassCastException("activity が OnOkBtnClickListener を実装していません."); } mListener = ((OnOkBtnClickListener) activity); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { Button okBtn = new Button(inflater.getContext()); okBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (mListener != null) { mListener.onOkClicked(); } } }); return okBtn; } } この場合、Fragment がシステムから再生成されたときも onAttach() を通るので、リスナーがセットされないということにはなりません。
Fragment が Activity の実装に依存しないので、複数の Activity で Fragment を使い回すときに便利です。


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

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



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