DialogFragment.java revision 327fbd2c8fa294b919475feb4c74a74ee1981e02
1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.app;
18
19import android.content.DialogInterface;
20import android.os.Bundle;
21import android.view.LayoutInflater;
22import android.view.View;
23import android.view.ViewGroup;
24import android.view.Window;
25import android.view.WindowManager;
26
27/**
28 * A fragment that displays a dialog window, floating on top of its
29 * activity's window.  This fragment contains a Dialog object, which it
30 * displays as appropriate based on the fragment's state.  Control of
31 * the dialog (deciding when to show, hide, dismiss it) should be done through
32 * the API here, not with direct calls on the dialog.
33 *
34 * <p>Implementations should override this class and implement
35 * {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)} to supply the
36 * content of the dialog.  Alternatively, they can override
37 * {@link #onCreateDialog(Bundle)} to create an entirely custom dialog, such
38 * as an AlertDialog, with its own content.
39 *
40 * <p>Topics covered here:
41 * <ol>
42 * <li><a href="#Lifecycle">Lifecycle</a>
43 * <li><a href="#BasicDialog">Basic Dialog</a>
44 * <li><a href="#AlertDialog">Alert Dialog</a>
45 * <li><a href="#DialogOrEmbed">Selecting Between Dialog or Embedding</a>
46 * </ol>
47 *
48 * <a name="Lifecycle"></a>
49 * <h3>Lifecycle</h3>
50 *
51 * <p>DialogFragment does various things to keep the fragment's lifecycle
52 * driving it, instead of the Dialog.  Note that dialogs are generally
53 * autonomous entities -- they are their own window, receiving their own
54 * input events, and often deciding on their own when to disappear (by
55 * receiving a back key event or the user clicking on a button).
56 *
57 * <p>DialogFragment needs to ensure that what is happening with the Fragment
58 * and Dialog states remains consistent.  To do this, it watches for dismiss
59 * events from the dialog and takes are of removing its own state when they
60 * happen.  This means you should use {@link #show(FragmentManager, String)}
61 * or {@link #show(FragmentTransaction, String)} to add an instance of
62 * DialogFragment to your UI, as these keep track of how DialogFragment should
63 * remove itself when the dialog is dismissed.
64 *
65 * <a name="BasicDialog"></a>
66 * <h3>Basic Dialog</h3>
67 *
68 * <p>The simplest use of DialogFragment is as a floating container for the
69 * fragment's view hierarchy.  A simple implementation may look like this:
70 *
71 * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/FragmentDialog.java
72 *      dialog}
73 *
74 * <p>An example showDialog() method on the Activity could be:
75 *
76 * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/FragmentDialog.java
77 *      add_dialog}
78 *
79 * <p>This removes any currently shown dialog, creates a new DialogFragment
80 * with an argument, and shows it as a new state on the back stack.  When the
81 * transaction is popped, the current DialogFragment and its Dialog will be
82 * destroyed, and the previous one (if any) re-shown.  Note that in this case
83 * DialogFragment will take care of popping the transaction of the Dialog
84 * is dismissed separately from it.
85 *
86 * <a name="AlertDialog"></a>
87 * <h3>Alert Dialog</h3>
88 *
89 * <p>Instead of (or in addition to) implementing {@link #onCreateView} to
90 * generate the view hierarchy inside of a dialog, you may implement
91 * {@link #onCreateDialog(Bundle)} to create your own custom Dialog object.
92 *
93 * <p>This is most useful for creating an {@link AlertDialog}, allowing you
94 * to display standard alerts to the user that are managed by a fragment.
95 * A simple example implementation of this is:
96 *
97 * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/FragmentAlertDialog.java
98 *      dialog}
99 *
100 * <p>The activity creating this fragment may have the following methods to
101 * show the dialog and receive results from it:
102 *
103 * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/FragmentAlertDialog.java
104 *      activity}
105 *
106 * <p>Note that in this case the fragment is not placed on the back stack, it
107 * is just added as an indefinitely running fragment.  Because dialogs normally
108 * are modal, this will still operate as a back stack, since the dialog will
109 * capture user input until it is dismissed.  When it is dismissed, DialogFragment
110 * will take care of removing itself from its fragment manager.
111 *
112 * <a name="DialogOrEmbed"></a>
113 * <h3>Selecting Between Dialog or Embedding</h3>
114 *
115 * <p>A DialogFragment can still optionally be used as a normal fragment, if
116 * desired.  This is useful if you have a fragment that in some cases should
117 * be shown as a dialog and others embedded in a larger UI.  This behavior
118 * will normally be automatically selected for you based on how you are using
119 * the fragment, but can be customized with {@link #setShowsDialog(boolean)}.
120 *
121 * <p>For example, here is a simple dialog fragment:
122 *
123 * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/FragmentDialogOrActivity.java
124 *      dialog}
125 *
126 * <p>An instance of this fragment can be created and shown as a dialog:
127 *
128 * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/FragmentDialogOrActivity.java
129 *      show_dialog}
130 *
131 * <p>It can also be added as content in a view hierarchy:
132 *
133 * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/FragmentDialogOrActivity.java
134 *      embed}
135 */
136public class DialogFragment extends Fragment
137        implements DialogInterface.OnCancelListener, DialogInterface.OnDismissListener {
138
139    /**
140     * Style for {@link #setStyle(int, int)}: a basic,
141     * normal dialog.
142     */
143    public static final int STYLE_NORMAL = 0;
144
145    /**
146     * Style for {@link #setStyle(int, int)}: don't include
147     * a title area.
148     */
149    public static final int STYLE_NO_TITLE = 1;
150
151    /**
152     * Style for {@link #setStyle(int, int)}: don't draw
153     * any frame at all; the view hierarchy returned by {@link #onCreateView}
154     * is entirely responsible for drawing the dialog.
155     */
156    public static final int STYLE_NO_FRAME = 2;
157
158    /**
159     * Style for {@link #setStyle(int, int)}: like
160     * {@link #STYLE_NO_FRAME}, but also disables all input to the dialog.
161     * The user can not touch it, and its window will not receive input focus.
162     */
163    public static final int STYLE_NO_INPUT = 3;
164
165    private static final String SAVED_DIALOG_STATE_TAG = "android:savedDialogState";
166    private static final String SAVED_STYLE = "android:style";
167    private static final String SAVED_THEME = "android:theme";
168    private static final String SAVED_CANCELABLE = "android:cancelable";
169    private static final String SAVED_SHOWS_DIALOG = "android:showsDialog";
170    private static final String SAVED_BACK_STACK_ID = "android:backStackId";
171
172    int mStyle = STYLE_NORMAL;
173    int mTheme = 0;
174    boolean mCancelable = true;
175    boolean mShowsDialog = true;
176    int mBackStackId = -1;
177
178    Dialog mDialog;
179    boolean mDestroyed;
180    boolean mRemoved;
181
182    public DialogFragment() {
183    }
184
185    /**
186     * Call to customize the basic appearance and behavior of the
187     * fragment's dialog.  This can be used for some common dialog behaviors,
188     * taking care of selecting flags, theme, and other options for you.  The
189     * same effect can be achieve by manually setting Dialog and Window
190     * attributes yourself.  Calling this after the fragment's Dialog is
191     * created will have no effect.
192     *
193     * @param style Selects a standard style: may be {@link #STYLE_NORMAL},
194     * {@link #STYLE_NO_TITLE}, {@link #STYLE_NO_FRAME}, or
195     * {@link #STYLE_NO_INPUT}.
196     * @param theme Optional custom theme.  If 0, an appropriate theme (based
197     * on the style) will be selected for you.
198     */
199    public void setStyle(int style, int theme) {
200        mStyle = style;
201        if (mStyle == STYLE_NO_FRAME || mStyle == STYLE_NO_INPUT) {
202            mTheme = com.android.internal.R.style.Theme_Holo_Dialog_NoFrame;
203        }
204        if (theme != 0) {
205            mTheme = theme;
206        }
207    }
208
209    /**
210     * Display the dialog, adding the fragment to the given FragmentManager.  This
211     * is a convenience for explicitly creating a transaction, adding the
212     * fragment to it with the given tag, and committing it.  This does
213     * <em>not</em> add the transaction to the back stack.  When the fragment
214     * is dismissed, a new transaction will be executed to remove it from
215     * the activity.
216     * @param manager The FragmentManager this fragment will be added to.
217     * @param tag The tag for this fragment, as per
218     * {@link FragmentTransaction#add(Fragment, String) FragmentTransaction.add}.
219     */
220    public void show(FragmentManager manager, String tag) {
221        FragmentTransaction ft = manager.beginTransaction();
222        ft.add(this, tag);
223        ft.commit();
224    }
225
226    /**
227     * Display the dialog, adding the fragment using an existing transaction
228     * and then committing the transaction.
229     * @param transaction An existing transaction in which to add the fragment.
230     * @param tag The tag for this fragment, as per
231     * {@link FragmentTransaction#add(Fragment, String) FragmentTransaction.add}.
232     * @return Returns the identifier of the committed transaction, as per
233     * {@link FragmentTransaction#commit() FragmentTransaction.commit()}.
234     */
235    public int show(FragmentTransaction transaction, String tag) {
236        transaction.add(this, tag);
237        mRemoved = false;
238        mBackStackId = transaction.commit();
239        return mBackStackId;
240    }
241
242    /**
243     * Dismiss the fragment and its dialog.  If the fragment was added to the
244     * back stack, all back stack state up to and including this entry will
245     * be popped.  Otherwise, a new transaction will be committed to remove
246     * the fragment.
247     */
248    public void dismiss() {
249        dismissInternal(false);
250    }
251
252    void dismissInternal(boolean allowStateLoss) {
253        if (mDialog != null) {
254            mDialog.dismiss();
255            mDialog = null;
256        }
257        mRemoved = true;
258        if (mBackStackId >= 0) {
259            getFragmentManager().popBackStack(mBackStackId,
260                    FragmentManager.POP_BACK_STACK_INCLUSIVE);
261            mBackStackId = -1;
262        } else {
263            FragmentTransaction ft = getFragmentManager().beginTransaction();
264            ft.remove(this);
265            if (allowStateLoss) {
266                ft.commitAllowingStateLoss();
267            } else {
268                ft.commit();
269            }
270        }
271    }
272
273    public Dialog getDialog() {
274        return mDialog;
275    }
276
277    public int getTheme() {
278        return mTheme;
279    }
280
281    /**
282     * Control whether the shown Dialog is cancelable.  Use this instead of
283     * directly calling {@link Dialog#setCancelable(boolean)
284     * Dialog.setCancelable(boolean)}, because DialogFragment needs to change
285     * its behavior based on this.
286     *
287     * @param cancelable If true, the dialog is cancelable.  The default
288     * is true.
289     */
290    public void setCancelable(boolean cancelable) {
291        mCancelable = cancelable;
292        if (mDialog != null) mDialog.setCancelable(cancelable);
293    }
294
295    /**
296     * Return the current value of {@link #setCancelable(boolean)}.
297     */
298    public boolean isCancelable() {
299        return mCancelable;
300    }
301
302    /**
303     * Controls whether this fragment should be shown in a dialog.  If not
304     * set, no Dialog will be created in {@link #onActivityCreated(Bundle)},
305     * and the fragment's view hierarchy will thus not be added to it.  This
306     * allows you to instead use it as a normal fragment (embedded inside of
307     * its activity).
308     *
309     * <p>This is normally set for you based on whether the fragment is
310     * associated with a container view ID passed to
311     * {@link FragmentTransaction#add(int, Fragment) FragmentTransaction.add(int, Fragment)}.
312     * If the fragment was added with a container, setShowsDialog will be
313     * initialized to false; otherwise, it will be true.
314     *
315     * @param showsDialog If true, the fragment will be displayed in a Dialog.
316     * If false, no Dialog will be created and the fragment's view hierarchly
317     * left undisturbed.
318     */
319    public void setShowsDialog(boolean showsDialog) {
320        mShowsDialog = showsDialog;
321    }
322
323    /**
324     * Return the current value of {@link #setShowsDialog(boolean)}.
325     */
326    public boolean getShowsDialog() {
327        return mShowsDialog;
328    }
329
330    @Override
331    public void onCreate(Bundle savedInstanceState) {
332        super.onCreate(savedInstanceState);
333
334        mShowsDialog = mContainerId == 0;
335
336        if (savedInstanceState != null) {
337            mStyle = savedInstanceState.getInt(SAVED_STYLE, STYLE_NORMAL);
338            mTheme = savedInstanceState.getInt(SAVED_THEME, 0);
339            mCancelable = savedInstanceState.getBoolean(SAVED_CANCELABLE, true);
340            mShowsDialog = savedInstanceState.getBoolean(SAVED_SHOWS_DIALOG, mShowsDialog);
341            mBackStackId = savedInstanceState.getInt(SAVED_BACK_STACK_ID, -1);
342        }
343    }
344
345    public Dialog onCreateDialog(Bundle savedInstanceState) {
346        return new Dialog(getActivity(), getTheme());
347    }
348
349    public void onCancel(DialogInterface dialog) {
350    }
351
352    public void onDismiss(DialogInterface dialog) {
353        if (!mRemoved) {
354            // Note: we need to use allowStateLoss, because the dialog
355            // dispatches this asynchronously so we can receive the call
356            // after the activity is paused.  Worst case, when the user comes
357            // back to the activity they see the dialog again.
358            dismissInternal(true);
359        }
360    }
361
362    @Override
363    public void onActivityCreated(Bundle savedInstanceState) {
364        super.onActivityCreated(savedInstanceState);
365
366        if (!mShowsDialog) {
367            return;
368        }
369
370        mDialog = onCreateDialog(savedInstanceState);
371        mDestroyed = false;
372        switch (mStyle) {
373            case STYLE_NO_INPUT:
374                mDialog.getWindow().addFlags(
375                        WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
376                        WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE);
377                // fall through...
378            case STYLE_NO_FRAME:
379            case STYLE_NO_TITLE:
380                mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
381        }
382        View view = getView();
383        if (view != null) {
384            if (view.getParent() != null) {
385                throw new IllegalStateException("DialogFragment can not be attached to a container view");
386            }
387            mDialog.setContentView(view);
388        }
389        mDialog.setOwnerActivity(getActivity());
390        mDialog.setCancelable(mCancelable);
391        mDialog.setOnCancelListener(this);
392        mDialog.setOnDismissListener(this);
393        if (savedInstanceState != null) {
394            Bundle dialogState = savedInstanceState.getBundle(SAVED_DIALOG_STATE_TAG);
395            if (dialogState != null) {
396                mDialog.onRestoreInstanceState(dialogState);
397            }
398        }
399    }
400
401    @Override
402    public void onStart() {
403        super.onStart();
404        if (mDialog != null) {
405            mRemoved = false;
406            mDialog.show();
407        }
408    }
409
410    @Override
411    public void onSaveInstanceState(Bundle outState) {
412        super.onSaveInstanceState(outState);
413        if (mDialog != null) {
414            Bundle dialogState = mDialog.onSaveInstanceState();
415            if (dialogState != null) {
416                outState.putBundle(SAVED_DIALOG_STATE_TAG, dialogState);
417            }
418        }
419        if (mStyle != STYLE_NORMAL) {
420            outState.putInt(SAVED_STYLE, mStyle);
421        }
422        if (mTheme != 0) {
423            outState.putInt(SAVED_THEME, mTheme);
424        }
425        if (!mCancelable) {
426            outState.putBoolean(SAVED_CANCELABLE, mCancelable);
427        }
428        if (!mShowsDialog) {
429            outState.putBoolean(SAVED_SHOWS_DIALOG, mShowsDialog);
430        }
431        if (mBackStackId != -1) {
432            outState.putInt(SAVED_BACK_STACK_ID, mBackStackId);
433        }
434    }
435
436    @Override
437    public void onStop() {
438        super.onStop();
439        if (mDialog != null) {
440            mDialog.hide();
441        }
442    }
443
444    /**
445     * Remove dialog.
446     */
447    @Override
448    public void onDestroyView() {
449        super.onDestroyView();
450        mDestroyed = true;
451        if (mDialog != null) {
452            // Set removed here because this dismissal is just to hide
453            // the dialog -- we don't want this to cause the fragment to
454            // actually be removed.
455            mRemoved = true;
456            mDialog.dismiss();
457            mDialog = null;
458        }
459    }
460}
461