1 /* 2 * Copyright (C) 2011 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 17 package android.support.v4.app; 18 19 import android.app.Activity; 20 import android.app.Dialog; 21 import android.content.Context; 22 import android.content.DialogInterface; 23 import android.os.Bundle; 24 import android.support.annotation.IntDef; 25 import android.support.annotation.NonNull; 26 import android.support.annotation.StyleRes; 27 import android.view.LayoutInflater; 28 import android.view.View; 29 import android.view.ViewGroup; 30 import android.view.Window; 31 import android.view.WindowManager; 32 33 import java.lang.annotation.Retention; 34 import java.lang.annotation.RetentionPolicy; 35 36 /** 37 * Static library support version of the framework's {@link android.app.DialogFragment}. 38 * Used to write apps that run on platforms prior to Android 3.0. When running 39 * on Android 3.0 or above, this implementation is still used; it does not try 40 * to switch to the framework's implementation. See the framework SDK 41 * documentation for a class overview. 42 */ 43 public class DialogFragment extends Fragment 44 implements DialogInterface.OnCancelListener, DialogInterface.OnDismissListener { 45 46 /** @hide */ 47 @IntDef({STYLE_NORMAL, STYLE_NO_TITLE, STYLE_NO_FRAME, STYLE_NO_INPUT}) 48 @Retention(RetentionPolicy.SOURCE) 49 private @interface DialogStyle {} 50 51 /** 52 * Style for {@link #setStyle(int, int)}: a basic, 53 * normal dialog. 54 */ 55 public static final int STYLE_NORMAL = 0; 56 57 /** 58 * Style for {@link #setStyle(int, int)}: don't include 59 * a title area. 60 */ 61 public static final int STYLE_NO_TITLE = 1; 62 63 /** 64 * Style for {@link #setStyle(int, int)}: don't draw 65 * any frame at all; the view hierarchy returned by {@link #onCreateView} 66 * is entirely responsible for drawing the dialog. 67 */ 68 public static final int STYLE_NO_FRAME = 2; 69 70 /** 71 * Style for {@link #setStyle(int, int)}: like 72 * {@link #STYLE_NO_FRAME}, but also disables all input to the dialog. 73 * The user can not touch it, and its window will not receive input focus. 74 */ 75 public static final int STYLE_NO_INPUT = 3; 76 77 private static final String SAVED_DIALOG_STATE_TAG = "android:savedDialogState"; 78 private static final String SAVED_STYLE = "android:style"; 79 private static final String SAVED_THEME = "android:theme"; 80 private static final String SAVED_CANCELABLE = "android:cancelable"; 81 private static final String SAVED_SHOWS_DIALOG = "android:showsDialog"; 82 private static final String SAVED_BACK_STACK_ID = "android:backStackId"; 83 84 int mStyle = STYLE_NORMAL; 85 int mTheme = 0; 86 boolean mCancelable = true; 87 boolean mShowsDialog = true; 88 int mBackStackId = -1; 89 90 Dialog mDialog; 91 boolean mViewDestroyed; 92 boolean mDismissed; 93 boolean mShownByMe; 94 DialogFragment()95 public DialogFragment() { 96 } 97 98 /** 99 * Call to customize the basic appearance and behavior of the 100 * fragment's dialog. This can be used for some common dialog behaviors, 101 * taking care of selecting flags, theme, and other options for you. The 102 * same effect can be achieve by manually setting Dialog and Window 103 * attributes yourself. Calling this after the fragment's Dialog is 104 * created will have no effect. 105 * 106 * @param style Selects a standard style: may be {@link #STYLE_NORMAL}, 107 * {@link #STYLE_NO_TITLE}, {@link #STYLE_NO_FRAME}, or 108 * {@link #STYLE_NO_INPUT}. 109 * @param theme Optional custom theme. If 0, an appropriate theme (based 110 * on the style) will be selected for you. 111 */ setStyle(@ialogStyle int style, @StyleRes int theme)112 public void setStyle(@DialogStyle int style, @StyleRes int theme) { 113 mStyle = style; 114 if (mStyle == STYLE_NO_FRAME || mStyle == STYLE_NO_INPUT) { 115 mTheme = android.R.style.Theme_Panel; 116 } 117 if (theme != 0) { 118 mTheme = theme; 119 } 120 } 121 122 /** 123 * Display the dialog, adding the fragment to the given FragmentManager. This 124 * is a convenience for explicitly creating a transaction, adding the 125 * fragment to it with the given tag, and committing it. This does 126 * <em>not</em> add the transaction to the back stack. When the fragment 127 * is dismissed, a new transaction will be executed to remove it from 128 * the activity. 129 * @param manager The FragmentManager this fragment will be added to. 130 * @param tag The tag for this fragment, as per 131 * {@link FragmentTransaction#add(Fragment, String) FragmentTransaction.add}. 132 */ show(FragmentManager manager, String tag)133 public void show(FragmentManager manager, String tag) { 134 mDismissed = false; 135 mShownByMe = true; 136 FragmentTransaction ft = manager.beginTransaction(); 137 ft.add(this, tag); 138 ft.commit(); 139 } 140 141 /** 142 * Display the dialog, adding the fragment using an existing transaction 143 * and then committing the transaction. 144 * @param transaction An existing transaction in which to add the fragment. 145 * @param tag The tag for this fragment, as per 146 * {@link FragmentTransaction#add(Fragment, String) FragmentTransaction.add}. 147 * @return Returns the identifier of the committed transaction, as per 148 * {@link FragmentTransaction#commit() FragmentTransaction.commit()}. 149 */ show(FragmentTransaction transaction, String tag)150 public int show(FragmentTransaction transaction, String tag) { 151 mDismissed = false; 152 mShownByMe = true; 153 transaction.add(this, tag); 154 mViewDestroyed = false; 155 mBackStackId = transaction.commit(); 156 return mBackStackId; 157 } 158 159 /** 160 * Dismiss the fragment and its dialog. If the fragment was added to the 161 * back stack, all back stack state up to and including this entry will 162 * be popped. Otherwise, a new transaction will be committed to remove 163 * the fragment. 164 */ dismiss()165 public void dismiss() { 166 dismissInternal(false); 167 } 168 169 /** 170 * Version of {@link #dismiss()} that uses 171 * {@link FragmentTransaction#commitAllowingStateLoss() 172 * FragmentTransaction.commitAllowingStateLoss()}. See linked 173 * documentation for further details. 174 */ dismissAllowingStateLoss()175 public void dismissAllowingStateLoss() { 176 dismissInternal(true); 177 } 178 dismissInternal(boolean allowStateLoss)179 void dismissInternal(boolean allowStateLoss) { 180 if (mDismissed) { 181 return; 182 } 183 mDismissed = true; 184 mShownByMe = false; 185 if (mDialog != null) { 186 mDialog.dismiss(); 187 mDialog = null; 188 } 189 mViewDestroyed = true; 190 if (mBackStackId >= 0) { 191 getFragmentManager().popBackStack(mBackStackId, 192 FragmentManager.POP_BACK_STACK_INCLUSIVE); 193 mBackStackId = -1; 194 } else { 195 FragmentTransaction ft = getFragmentManager().beginTransaction(); 196 ft.remove(this); 197 if (allowStateLoss) { 198 ft.commitAllowingStateLoss(); 199 } else { 200 ft.commit(); 201 } 202 } 203 } 204 getDialog()205 public Dialog getDialog() { 206 return mDialog; 207 } 208 209 @StyleRes getTheme()210 public int getTheme() { 211 return mTheme; 212 } 213 214 /** 215 * Control whether the shown Dialog is cancelable. Use this instead of 216 * directly calling {@link Dialog#setCancelable(boolean) 217 * Dialog.setCancelable(boolean)}, because DialogFragment needs to change 218 * its behavior based on this. 219 * 220 * @param cancelable If true, the dialog is cancelable. The default 221 * is true. 222 */ setCancelable(boolean cancelable)223 public void setCancelable(boolean cancelable) { 224 mCancelable = cancelable; 225 if (mDialog != null) mDialog.setCancelable(cancelable); 226 } 227 228 /** 229 * Return the current value of {@link #setCancelable(boolean)}. 230 */ isCancelable()231 public boolean isCancelable() { 232 return mCancelable; 233 } 234 235 /** 236 * Controls whether this fragment should be shown in a dialog. If not 237 * set, no Dialog will be created in {@link #onActivityCreated(Bundle)}, 238 * and the fragment's view hierarchy will thus not be added to it. This 239 * allows you to instead use it as a normal fragment (embedded inside of 240 * its activity). 241 * 242 * <p>This is normally set for you based on whether the fragment is 243 * associated with a container view ID passed to 244 * {@link FragmentTransaction#add(int, Fragment) FragmentTransaction.add(int, Fragment)}. 245 * If the fragment was added with a container, setShowsDialog will be 246 * initialized to false; otherwise, it will be true. 247 * 248 * @param showsDialog If true, the fragment will be displayed in a Dialog. 249 * If false, no Dialog will be created and the fragment's view hierarchly 250 * left undisturbed. 251 */ setShowsDialog(boolean showsDialog)252 public void setShowsDialog(boolean showsDialog) { 253 mShowsDialog = showsDialog; 254 } 255 256 /** 257 * Return the current value of {@link #setShowsDialog(boolean)}. 258 */ getShowsDialog()259 public boolean getShowsDialog() { 260 return mShowsDialog; 261 } 262 263 @Override onAttach(Activity activity)264 public void onAttach(Activity activity) { 265 super.onAttach(activity); 266 if (!mShownByMe) { 267 // If not explicitly shown through our API, take this as an 268 // indication that the dialog is no longer dismissed. 269 mDismissed = false; 270 } 271 } 272 273 @Override onDetach()274 public void onDetach() { 275 super.onDetach(); 276 if (!mShownByMe && !mDismissed) { 277 // The fragment was not shown by a direct call here, it is not 278 // dismissed, and now it is being detached... well, okay, thou 279 // art now dismissed. Have fun. 280 mDismissed = true; 281 } 282 } 283 284 @Override onCreate(Bundle savedInstanceState)285 public void onCreate(Bundle savedInstanceState) { 286 super.onCreate(savedInstanceState); 287 288 mShowsDialog = mContainerId == 0; 289 290 if (savedInstanceState != null) { 291 mStyle = savedInstanceState.getInt(SAVED_STYLE, STYLE_NORMAL); 292 mTheme = savedInstanceState.getInt(SAVED_THEME, 0); 293 mCancelable = savedInstanceState.getBoolean(SAVED_CANCELABLE, true); 294 mShowsDialog = savedInstanceState.getBoolean(SAVED_SHOWS_DIALOG, mShowsDialog); 295 mBackStackId = savedInstanceState.getInt(SAVED_BACK_STACK_ID, -1); 296 } 297 298 } 299 300 /** @hide */ 301 @Override getLayoutInflater(Bundle savedInstanceState)302 public LayoutInflater getLayoutInflater(Bundle savedInstanceState) { 303 if (!mShowsDialog) { 304 return super.getLayoutInflater(savedInstanceState); 305 } 306 307 mDialog = onCreateDialog(savedInstanceState); 308 switch (mStyle) { 309 case STYLE_NO_INPUT: 310 mDialog.getWindow().addFlags( 311 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | 312 WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE); 313 // fall through... 314 case STYLE_NO_FRAME: 315 case STYLE_NO_TITLE: 316 mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE); 317 } 318 if (mDialog != null) { 319 return (LayoutInflater) mDialog.getContext().getSystemService( 320 Context.LAYOUT_INFLATER_SERVICE); 321 } 322 return (LayoutInflater) mActivity.getSystemService( 323 Context.LAYOUT_INFLATER_SERVICE); 324 } 325 326 /** 327 * Override to build your own custom Dialog container. This is typically 328 * used to show an AlertDialog instead of a generic Dialog; when doing so, 329 * {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)} does not need 330 * to be implemented since the AlertDialog takes care of its own content. 331 * 332 * <p>This method will be called after {@link #onCreate(Bundle)} and 333 * before {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}. The 334 * default implementation simply instantiates and returns a {@link Dialog} 335 * class. 336 * 337 * <p><em>Note: DialogFragment own the {@link Dialog#setOnCancelListener 338 * Dialog.setOnCancelListener} and {@link Dialog#setOnDismissListener 339 * Dialog.setOnDismissListener} callbacks. You must not set them yourself.</em> 340 * To find out about these events, override {@link #onCancel(DialogInterface)} 341 * and {@link #onDismiss(DialogInterface)}.</p> 342 * 343 * @param savedInstanceState The last saved instance state of the Fragment, 344 * or null if this is a freshly created Fragment. 345 * 346 * @return Return a new Dialog instance to be displayed by the Fragment. 347 */ 348 @NonNull onCreateDialog(Bundle savedInstanceState)349 public Dialog onCreateDialog(Bundle savedInstanceState) { 350 return new Dialog(getActivity(), getTheme()); 351 } 352 onCancel(DialogInterface dialog)353 public void onCancel(DialogInterface dialog) { 354 } 355 onDismiss(DialogInterface dialog)356 public void onDismiss(DialogInterface dialog) { 357 if (!mViewDestroyed) { 358 // Note: we need to use allowStateLoss, because the dialog 359 // dispatches this asynchronously so we can receive the call 360 // after the activity is paused. Worst case, when the user comes 361 // back to the activity they see the dialog again. 362 dismissInternal(true); 363 } 364 } 365 366 @Override onActivityCreated(Bundle savedInstanceState)367 public void onActivityCreated(Bundle savedInstanceState) { 368 super.onActivityCreated(savedInstanceState); 369 370 if (!mShowsDialog) { 371 return; 372 } 373 374 View view = getView(); 375 if (view != null) { 376 if (view.getParent() != null) { 377 throw new IllegalStateException("DialogFragment can not be attached to a container view"); 378 } 379 mDialog.setContentView(view); 380 } 381 mDialog.setOwnerActivity(getActivity()); 382 mDialog.setCancelable(mCancelable); 383 mDialog.setOnCancelListener(this); 384 mDialog.setOnDismissListener(this); 385 if (savedInstanceState != null) { 386 Bundle dialogState = savedInstanceState.getBundle(SAVED_DIALOG_STATE_TAG); 387 if (dialogState != null) { 388 mDialog.onRestoreInstanceState(dialogState); 389 } 390 } 391 } 392 393 @Override onStart()394 public void onStart() { 395 super.onStart(); 396 if (mDialog != null) { 397 mViewDestroyed = false; 398 mDialog.show(); 399 } 400 } 401 402 @Override onSaveInstanceState(Bundle outState)403 public void onSaveInstanceState(Bundle outState) { 404 super.onSaveInstanceState(outState); 405 if (mDialog != null) { 406 Bundle dialogState = mDialog.onSaveInstanceState(); 407 if (dialogState != null) { 408 outState.putBundle(SAVED_DIALOG_STATE_TAG, dialogState); 409 } 410 } 411 if (mStyle != STYLE_NORMAL) { 412 outState.putInt(SAVED_STYLE, mStyle); 413 } 414 if (mTheme != 0) { 415 outState.putInt(SAVED_THEME, mTheme); 416 } 417 if (!mCancelable) { 418 outState.putBoolean(SAVED_CANCELABLE, mCancelable); 419 } 420 if (!mShowsDialog) { 421 outState.putBoolean(SAVED_SHOWS_DIALOG, mShowsDialog); 422 } 423 if (mBackStackId != -1) { 424 outState.putInt(SAVED_BACK_STACK_ID, mBackStackId); 425 } 426 } 427 428 @Override onStop()429 public void onStop() { 430 super.onStop(); 431 if (mDialog != null) { 432 mDialog.hide(); 433 } 434 } 435 436 /** 437 * Remove dialog. 438 */ 439 @Override onDestroyView()440 public void onDestroyView() { 441 super.onDestroyView(); 442 if (mDialog != null) { 443 // Set removed here because this dismissal is just to hide 444 // the dialog -- we don't want this to cause the fragment to 445 // actually be removed. 446 mViewDestroyed = true; 447 mDialog.dismiss(); 448 mDialog = null; 449 } 450 } 451 } 452