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