1/* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 * in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the License 10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 * or implied. See the License for the specific language governing permissions and limitations under 12 * the License. 13 */ 14package android.support.v17.leanback.app; 15 16import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; 17 18import android.animation.Animator; 19import android.animation.AnimatorSet; 20import android.app.Activity; 21import android.app.Fragment; 22import android.app.FragmentManager; 23import android.app.FragmentManager.BackStackEntry; 24import android.app.FragmentTransaction; 25import android.content.Context; 26import android.os.Build; 27import android.os.Bundle; 28import android.support.annotation.NonNull; 29import android.support.annotation.RestrictTo; 30import android.support.v17.leanback.R; 31import android.support.v17.leanback.transition.TransitionHelper; 32import android.support.v17.leanback.widget.GuidanceStylist; 33import android.support.v17.leanback.widget.GuidanceStylist.Guidance; 34import android.support.v17.leanback.widget.GuidedAction; 35import android.support.v17.leanback.widget.GuidedActionAdapter; 36import android.support.v17.leanback.widget.GuidedActionAdapterGroup; 37import android.support.v17.leanback.widget.GuidedActionsStylist; 38import android.support.v4.app.ActivityCompat; 39import android.support.v7.widget.RecyclerView; 40import android.util.Log; 41import android.util.TypedValue; 42import android.view.ContextThemeWrapper; 43import android.view.Gravity; 44import android.view.LayoutInflater; 45import android.view.View; 46import android.view.ViewGroup; 47import android.widget.FrameLayout; 48import android.widget.LinearLayout; 49 50import java.util.ArrayList; 51import java.util.List; 52 53/** 54 * A GuidedStepFragment is used to guide the user through a decision or series of decisions. 55 * It is composed of a guidance view on the left and a view on the right containing a list of 56 * possible actions. 57 * <p> 58 * <h3>Basic Usage</h3> 59 * <p> 60 * Clients of GuidedStepFragment must create a custom subclass to attach to their Activities. 61 * This custom subclass provides the information necessary to construct the user interface and 62 * respond to user actions. At a minimum, subclasses should override: 63 * <ul> 64 * <li>{@link #onCreateGuidance}, to provide instructions to the user</li> 65 * <li>{@link #onCreateActions}, to provide a set of {@link GuidedAction}s the user can take</li> 66 * <li>{@link #onGuidedActionClicked}, to respond to those actions</li> 67 * </ul> 68 * <p> 69 * Clients use following helper functions to add GuidedStepFragment to Activity or FragmentManager: 70 * <ul> 71 * <li>{@link #addAsRoot(Activity, GuidedStepFragment, int)}, to be called during Activity onCreate, 72 * adds GuidedStepFragment as the first Fragment in activity.</li> 73 * <li>{@link #add(FragmentManager, GuidedStepFragment)} or {@link #add(FragmentManager, 74 * GuidedStepFragment, int)}, to add GuidedStepFragment on top of existing Fragments or 75 * replacing existing GuidedStepFragment when moving forward to next step.</li> 76 * <li>{@link #finishGuidedStepFragments()} can either finish the activity or pop all 77 * GuidedStepFragment from stack. 78 * <li>If app chooses not to use the helper function, it is the app's responsibility to call 79 * {@link #setUiStyle(int)} to select fragment transition and remember the stack entry where it 80 * need pops to. 81 * </ul> 82 * <h3>Theming and Stylists</h3> 83 * <p> 84 * GuidedStepFragment delegates its visual styling to classes called stylists. The {@link 85 * GuidanceStylist} is responsible for the left guidance view, while the {@link 86 * GuidedActionsStylist} is responsible for the right actions view. The stylists use theme 87 * attributes to derive values associated with the presentation, such as colors, animations, etc. 88 * Most simple visual aspects of GuidanceStylist and GuidedActionsStylist can be customized 89 * via theming; see their documentation for more information. 90 * <p> 91 * GuidedStepFragments must have access to an appropriate theme in order for the stylists to 92 * function properly. Specifically, the fragment must receive {@link 93 * android.support.v17.leanback.R.style#Theme_Leanback_GuidedStep}, or a theme whose parent is 94 * is set to that theme. Themes can be provided in one of three ways: 95 * <ul> 96 * <li>The simplest way is to set the theme for the host Activity to the GuidedStep theme or a 97 * theme that derives from it.</li> 98 * <li>If the Activity already has a theme and setting its parent theme is inconvenient, the 99 * existing Activity theme can have an entry added for the attribute {@link 100 * android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedStepTheme}. If present, 101 * this theme will be used by GuidedStepFragment as an overlay to the Activity's theme.</li> 102 * <li>Finally, custom subclasses of GuidedStepFragment may provide a theme through the {@link 103 * #onProvideTheme} method. This can be useful if a subclass is used across multiple 104 * Activities.</li> 105 * </ul> 106 * <p> 107 * If the theme is provided in multiple ways, the onProvideTheme override has priority, followed by 108 * the Activity's theme. (Themes whose parent theme is already set to the guided step theme do not 109 * need to set the guidedStepTheme attribute; if set, it will be ignored.) 110 * <p> 111 * If themes do not provide enough customizability, the stylists themselves may be subclassed and 112 * provided to the GuidedStepFragment through the {@link #onCreateGuidanceStylist} and {@link 113 * #onCreateActionsStylist} methods. The stylists have simple hooks so that subclasses 114 * may override layout files; subclasses may also have more complex logic to determine styling. 115 * <p> 116 * <h3>Guided sequences</h3> 117 * <p> 118 * GuidedStepFragments can be grouped together to provide a guided sequence. GuidedStepFragments 119 * grouped as a sequence use custom animations provided by {@link GuidanceStylist} and 120 * {@link GuidedActionsStylist} (or subclasses) during transitions between steps. Clients 121 * should use {@link #add} to place subsequent GuidedFragments onto the fragment stack so that 122 * custom animations are properly configured. (Custom animations are triggered automatically when 123 * the fragment stack is subsequently popped by any normal mechanism.) 124 * <p> 125 * <i>Note: Currently GuidedStepFragments grouped in this way must all be defined programmatically, 126 * rather than in XML. This restriction may be removed in the future.</i> 127 * 128 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedStepTheme 129 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedStepBackground 130 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionContentWidthWeight 131 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionContentWidthWeightTwoPanels 132 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsBackground 133 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsBackgroundDark 134 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsElevation 135 * @see GuidanceStylist 136 * @see GuidanceStylist.Guidance 137 * @see GuidedAction 138 * @see GuidedActionsStylist 139 */ 140public class GuidedStepFragment extends Fragment implements GuidedActionAdapter.FocusListener { 141 142 private static final String TAG_LEAN_BACK_ACTIONS_FRAGMENT = "leanBackGuidedStepFragment"; 143 private static final String EXTRA_ACTION_PREFIX = "action_"; 144 private static final String EXTRA_BUTTON_ACTION_PREFIX = "buttonaction_"; 145 146 private static final String ENTRY_NAME_REPLACE = "GuidedStepDefault"; 147 148 private static final String ENTRY_NAME_ENTRANCE = "GuidedStepEntrance"; 149 150 private static final boolean IS_FRAMEWORK_FRAGMENT = true; 151 152 /** 153 * Fragment argument name for UI style. The argument value is persisted in fragment state and 154 * used to select fragment transition. The value is initially {@link #UI_STYLE_ENTRANCE} and 155 * might be changed in one of the three helper functions: 156 * <ul> 157 * <li>{@link #addAsRoot(Activity, GuidedStepFragment, int)} sets to 158 * {@link #UI_STYLE_ACTIVITY_ROOT}</li> 159 * <li>{@link #add(FragmentManager, GuidedStepFragment)} or {@link #add(FragmentManager, 160 * GuidedStepFragment, int)} sets it to {@link #UI_STYLE_REPLACE} if there is already a 161 * GuidedStepFragment on stack.</li> 162 * <li>{@link #finishGuidedStepFragments()} changes current GuidedStepFragment to 163 * {@link #UI_STYLE_ENTRANCE} for the non activity case. This is a special case that changes 164 * the transition settings after fragment has been created, in order to force current 165 * GuidedStepFragment run a return transition of {@link #UI_STYLE_ENTRANCE}</li> 166 * </ul> 167 * <p> 168 * Argument value can be either: 169 * <ul> 170 * <li>{@link #UI_STYLE_REPLACE}</li> 171 * <li>{@link #UI_STYLE_ENTRANCE}</li> 172 * <li>{@link #UI_STYLE_ACTIVITY_ROOT}</li> 173 * </ul> 174 */ 175 public static final String EXTRA_UI_STYLE = "uiStyle"; 176 177 /** 178 * This is the case that we use GuidedStepFragment to replace another existing 179 * GuidedStepFragment when moving forward to next step. Default behavior of this style is: 180 * <ul> 181 * <li>Enter transition slides in from END(right), exit transition same as 182 * {@link #UI_STYLE_ENTRANCE}. 183 * </li> 184 * </ul> 185 */ 186 public static final int UI_STYLE_REPLACE = 0; 187 188 /** 189 * @deprecated Same value as {@link #UI_STYLE_REPLACE}. 190 */ 191 @Deprecated 192 public static final int UI_STYLE_DEFAULT = 0; 193 194 /** 195 * Default value for argument {@link #EXTRA_UI_STYLE}. The default value is assigned in 196 * GuidedStepFragment constructor. This is the case that we show GuidedStepFragment on top of 197 * other content. The default behavior of this style: 198 * <ul> 199 * <li>Enter transition slides in from two sides, exit transition slide out to START(left). 200 * Background will be faded in. Note: Changing exit transition by UI style is not working 201 * because fragment transition asks for exit transition before UI style is restored in Fragment 202 * .onCreate().</li> 203 * </ul> 204 * When popping multiple GuidedStepFragment, {@link #finishGuidedStepFragments()} also changes 205 * the top GuidedStepFragment to UI_STYLE_ENTRANCE in order to run the return transition 206 * (reverse of enter transition) of UI_STYLE_ENTRANCE. 207 */ 208 public static final int UI_STYLE_ENTRANCE = 1; 209 210 /** 211 * One possible value of argument {@link #EXTRA_UI_STYLE}. This is the case that we show first 212 * GuidedStepFragment in a separate activity. The default behavior of this style: 213 * <ul> 214 * <li>Enter transition is assigned null (will rely on activity transition), exit transition is 215 * same as {@link #UI_STYLE_ENTRANCE}. Note: Changing exit transition by UI style is not working 216 * because fragment transition asks for exit transition before UI style is restored in 217 * Fragment.onCreate().</li> 218 * </ul> 219 */ 220 public static final int UI_STYLE_ACTIVITY_ROOT = 2; 221 222 /** 223 * Animation to slide the contents from the side (left/right). 224 * @hide 225 */ 226 @RestrictTo(LIBRARY_GROUP) 227 public static final int SLIDE_FROM_SIDE = 0; 228 229 /** 230 * Animation to slide the contents from the bottom. 231 * @hide 232 */ 233 @RestrictTo(LIBRARY_GROUP) 234 public static final int SLIDE_FROM_BOTTOM = 1; 235 236 private static final String TAG = "GuidedStepFragment"; 237 private static final boolean DEBUG = false; 238 239 /** 240 * @hide 241 */ 242 @RestrictTo(LIBRARY_GROUP) 243 public static class DummyFragment extends Fragment { 244 @Override 245 public View onCreateView(LayoutInflater inflater, ViewGroup container, 246 Bundle savedInstanceState) { 247 final View v = new View(inflater.getContext()); 248 v.setVisibility(View.GONE); 249 return v; 250 } 251 } 252 253 private ContextThemeWrapper mThemeWrapper; 254 private GuidanceStylist mGuidanceStylist; 255 GuidedActionsStylist mActionsStylist; 256 private GuidedActionsStylist mButtonActionsStylist; 257 private GuidedActionAdapter mAdapter; 258 private GuidedActionAdapter mSubAdapter; 259 private GuidedActionAdapter mButtonAdapter; 260 private GuidedActionAdapterGroup mAdapterGroup; 261 private List<GuidedAction> mActions = new ArrayList<GuidedAction>(); 262 private List<GuidedAction> mButtonActions = new ArrayList<GuidedAction>(); 263 private int entranceTransitionType = SLIDE_FROM_SIDE; 264 265 public GuidedStepFragment() { 266 mGuidanceStylist = onCreateGuidanceStylist(); 267 mActionsStylist = onCreateActionsStylist(); 268 mButtonActionsStylist = onCreateButtonActionsStylist(); 269 onProvideFragmentTransitions(); 270 } 271 272 /** 273 * Creates the presenter used to style the guidance panel. The default implementation returns 274 * a basic GuidanceStylist. 275 * @return The GuidanceStylist used in this fragment. 276 */ 277 public GuidanceStylist onCreateGuidanceStylist() { 278 return new GuidanceStylist(); 279 } 280 281 /** 282 * Creates the presenter used to style the guided actions panel. The default implementation 283 * returns a basic GuidedActionsStylist. 284 * @return The GuidedActionsStylist used in this fragment. 285 */ 286 public GuidedActionsStylist onCreateActionsStylist() { 287 return new GuidedActionsStylist(); 288 } 289 290 /** 291 * Creates the presenter used to style a sided actions panel for button only. 292 * The default implementation returns a basic GuidedActionsStylist. 293 * @return The GuidedActionsStylist used in this fragment. 294 */ 295 public GuidedActionsStylist onCreateButtonActionsStylist() { 296 GuidedActionsStylist stylist = new GuidedActionsStylist(); 297 stylist.setAsButtonActions(); 298 return stylist; 299 } 300 301 /** 302 * Returns the theme used for styling the fragment. The default returns -1, indicating that the 303 * host Activity's theme should be used. 304 * @return The theme resource ID of the theme to use in this fragment, or -1 to use the 305 * host Activity's theme. 306 */ 307 public int onProvideTheme() { 308 return -1; 309 } 310 311 /** 312 * Returns the information required to provide guidance to the user. This hook is called during 313 * {@link #onCreateView}. May be overridden to return a custom subclass of {@link 314 * GuidanceStylist.Guidance} for use in a subclass of {@link GuidanceStylist}. The default 315 * returns a Guidance object with empty fields; subclasses should override. 316 * @param savedInstanceState The saved instance state from onCreateView. 317 * @return The Guidance object representing the information used to guide the user. 318 */ 319 public @NonNull Guidance onCreateGuidance(Bundle savedInstanceState) { 320 return new Guidance("", "", "", null); 321 } 322 323 /** 324 * Fills out the set of actions available to the user. This hook is called during {@link 325 * #onCreate}. The default leaves the list of actions empty; subclasses should override. 326 * @param actions A non-null, empty list ready to be populated. 327 * @param savedInstanceState The saved instance state from onCreate. 328 */ 329 public void onCreateActions(@NonNull List<GuidedAction> actions, Bundle savedInstanceState) { 330 } 331 332 /** 333 * Fills out the set of actions shown at right available to the user. This hook is called during 334 * {@link #onCreate}. The default leaves the list of actions empty; subclasses may override. 335 * @param actions A non-null, empty list ready to be populated. 336 * @param savedInstanceState The saved instance state from onCreate. 337 */ 338 public void onCreateButtonActions(@NonNull List<GuidedAction> actions, 339 Bundle savedInstanceState) { 340 } 341 342 /** 343 * Callback invoked when an action is taken by the user. Subclasses should override in 344 * order to act on the user's decisions. 345 * @param action The chosen action. 346 */ 347 public void onGuidedActionClicked(GuidedAction action) { 348 } 349 350 /** 351 * Callback invoked when an action in sub actions is taken by the user. Subclasses should 352 * override in order to act on the user's decisions. Default return value is true to close 353 * the sub actions list. 354 * @param action The chosen action. 355 * @return true to collapse the sub actions list, false to keep it expanded. 356 */ 357 public boolean onSubGuidedActionClicked(GuidedAction action) { 358 return true; 359 } 360 361 /** 362 * @return True if is current expanded including subactions list or 363 * action with {@link GuidedAction#hasEditableActivatorView()} is true. 364 */ 365 public boolean isExpanded() { 366 return mActionsStylist.isExpanded(); 367 } 368 369 /** 370 * @return True if the sub actions list is expanded, false otherwise. 371 */ 372 public boolean isSubActionsExpanded() { 373 return mActionsStylist.isSubActionsExpanded(); 374 } 375 376 /** 377 * Expand a given action's sub actions list. 378 * @param action GuidedAction to expand. 379 * @see #expandAction(GuidedAction, boolean) 380 */ 381 public void expandSubActions(GuidedAction action) { 382 if (!action.hasSubActions()) { 383 return; 384 } 385 expandAction(action, true); 386 } 387 388 /** 389 * Expand a given action with sub actions list or 390 * {@link GuidedAction#hasEditableActivatorView()} is true. The method must be called after 391 * {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)} creates fragment view. 392 * 393 * @param action GuidedAction to expand. 394 * @param withTransition True to run transition animation, false otherwise. 395 */ 396 public void expandAction(GuidedAction action, boolean withTransition) { 397 mActionsStylist.expandAction(action, withTransition); 398 } 399 400 /** 401 * Collapse sub actions list. 402 * @see GuidedAction#getSubActions() 403 */ 404 public void collapseSubActions() { 405 collapseAction(true); 406 } 407 408 /** 409 * Collapse action which either has a sub actions list or action with 410 * {@link GuidedAction#hasEditableActivatorView()} is true. 411 * 412 * @param withTransition True to run transition animation, false otherwise. 413 */ 414 public void collapseAction(boolean withTransition) { 415 if (mActionsStylist != null && mActionsStylist.getActionsGridView() != null) { 416 mActionsStylist.collapseAction(withTransition); 417 } 418 } 419 420 /** 421 * Callback invoked when an action is focused (made to be the current selection) by the user. 422 */ 423 @Override 424 public void onGuidedActionFocused(GuidedAction action) { 425 } 426 427 /** 428 * Callback invoked when an action's title or description has been edited, this happens either 429 * when user clicks confirm button in IME or user closes IME window by BACK key. 430 * @deprecated Override {@link #onGuidedActionEditedAndProceed(GuidedAction)} and/or 431 * {@link #onGuidedActionEditCanceled(GuidedAction)}. 432 */ 433 @Deprecated 434 public void onGuidedActionEdited(GuidedAction action) { 435 } 436 437 /** 438 * Callback invoked when an action has been canceled editing, for example when user closes 439 * IME window by BACK key. Default implementation calls deprecated method 440 * {@link #onGuidedActionEdited(GuidedAction)}. 441 * @param action The action which has been canceled editing. 442 */ 443 public void onGuidedActionEditCanceled(GuidedAction action) { 444 onGuidedActionEdited(action); 445 } 446 447 /** 448 * Callback invoked when an action has been edited, for example when user clicks confirm button 449 * in IME window. Default implementation calls deprecated method 450 * {@link #onGuidedActionEdited(GuidedAction)} and returns {@link GuidedAction#ACTION_ID_NEXT}. 451 * 452 * @param action The action that has been edited. 453 * @return ID of the action will be focused or {@link GuidedAction#ACTION_ID_NEXT}, 454 * {@link GuidedAction#ACTION_ID_CURRENT}. 455 */ 456 public long onGuidedActionEditedAndProceed(GuidedAction action) { 457 onGuidedActionEdited(action); 458 return GuidedAction.ACTION_ID_NEXT; 459 } 460 461 /** 462 * Adds the specified GuidedStepFragment to the fragment stack, replacing any existing 463 * GuidedStepFragments in the stack, and configuring the fragment-to-fragment custom 464 * transitions. A backstack entry is added, so the fragment will be dismissed when BACK key 465 * is pressed. 466 * <li>If current fragment on stack is GuidedStepFragment: assign {@link #UI_STYLE_REPLACE} 467 * <li>If current fragment on stack is not GuidedStepFragment: assign {@link #UI_STYLE_ENTRANCE} 468 * <p> 469 * Note: currently fragments added using this method must be created programmatically rather 470 * than via XML. 471 * @param fragmentManager The FragmentManager to be used in the transaction. 472 * @param fragment The GuidedStepFragment to be inserted into the fragment stack. 473 * @return The ID returned by the call FragmentTransaction.commit. 474 */ 475 public static int add(FragmentManager fragmentManager, GuidedStepFragment fragment) { 476 return add(fragmentManager, fragment, android.R.id.content); 477 } 478 479 /** 480 * Adds the specified GuidedStepFragment to the fragment stack, replacing any existing 481 * GuidedStepFragments in the stack, and configuring the fragment-to-fragment custom 482 * transitions. A backstack entry is added, so the fragment will be dismissed when BACK key 483 * is pressed. 484 * <li>If current fragment on stack is GuidedStepFragment: assign {@link #UI_STYLE_REPLACE} and 485 * {@link #onAddSharedElementTransition(FragmentTransaction, GuidedStepFragment)} will be called 486 * to perform shared element transition between GuidedStepFragments. 487 * <li>If current fragment on stack is not GuidedStepFragment: assign {@link #UI_STYLE_ENTRANCE} 488 * <p> 489 * Note: currently fragments added using this method must be created programmatically rather 490 * than via XML. 491 * @param fragmentManager The FragmentManager to be used in the transaction. 492 * @param fragment The GuidedStepFragment to be inserted into the fragment stack. 493 * @param id The id of container to add GuidedStepFragment, can be android.R.id.content. 494 * @return The ID returned by the call FragmentTransaction.commit. 495 */ 496 public static int add(FragmentManager fragmentManager, GuidedStepFragment fragment, int id) { 497 GuidedStepFragment current = getCurrentGuidedStepFragment(fragmentManager); 498 boolean inGuidedStep = current != null; 499 if (IS_FRAMEWORK_FRAGMENT && Build.VERSION.SDK_INT >= 21 && Build.VERSION.SDK_INT < 23 500 && !inGuidedStep) { 501 // workaround b/22631964 for framework fragment 502 fragmentManager.beginTransaction() 503 .replace(id, new DummyFragment(), TAG_LEAN_BACK_ACTIONS_FRAGMENT) 504 .commit(); 505 } 506 FragmentTransaction ft = fragmentManager.beginTransaction(); 507 508 fragment.setUiStyle(inGuidedStep ? UI_STYLE_REPLACE : UI_STYLE_ENTRANCE); 509 ft.addToBackStack(fragment.generateStackEntryName()); 510 if (current != null) { 511 fragment.onAddSharedElementTransition(ft, current); 512 } 513 return ft.replace(id, fragment, TAG_LEAN_BACK_ACTIONS_FRAGMENT).commit(); 514 } 515 516 /** 517 * Called when this fragment is added to FragmentTransaction with {@link #UI_STYLE_REPLACE} (aka 518 * when the GuidedStepFragment replacing an existing GuidedStepFragment). Default implementation 519 * establishes connections between action background views to morph action background bounds 520 * change from disappearing GuidedStepFragment into this GuidedStepFragment. The default 521 * implementation heavily relies on {@link GuidedActionsStylist}'s layout, app may override this 522 * method when modifying the default layout of {@link GuidedActionsStylist}. 523 * 524 * @see GuidedActionsStylist 525 * @see #onProvideFragmentTransitions() 526 * @param ft The FragmentTransaction to add shared element. 527 * @param disappearing The disappearing fragment. 528 */ 529 protected void onAddSharedElementTransition(FragmentTransaction ft, GuidedStepFragment 530 disappearing) { 531 View fragmentView = disappearing.getView(); 532 addNonNullSharedElementTransition(ft, fragmentView.findViewById( 533 R.id.action_fragment_root), "action_fragment_root"); 534 addNonNullSharedElementTransition(ft, fragmentView.findViewById( 535 R.id.action_fragment_background), "action_fragment_background"); 536 addNonNullSharedElementTransition(ft, fragmentView.findViewById( 537 R.id.action_fragment), "action_fragment"); 538 addNonNullSharedElementTransition(ft, fragmentView.findViewById( 539 R.id.guidedactions_root), "guidedactions_root"); 540 addNonNullSharedElementTransition(ft, fragmentView.findViewById( 541 R.id.guidedactions_content), "guidedactions_content"); 542 addNonNullSharedElementTransition(ft, fragmentView.findViewById( 543 R.id.guidedactions_list_background), "guidedactions_list_background"); 544 addNonNullSharedElementTransition(ft, fragmentView.findViewById( 545 R.id.guidedactions_root2), "guidedactions_root2"); 546 addNonNullSharedElementTransition(ft, fragmentView.findViewById( 547 R.id.guidedactions_content2), "guidedactions_content2"); 548 addNonNullSharedElementTransition(ft, fragmentView.findViewById( 549 R.id.guidedactions_list_background2), "guidedactions_list_background2"); 550 } 551 552 private static void addNonNullSharedElementTransition (FragmentTransaction ft, View subView, 553 String transitionName) 554 { 555 if (subView != null) 556 TransitionHelper.addSharedElement(ft, subView, transitionName); 557 } 558 559 /** 560 * Returns BackStackEntry name for the GuidedStepFragment or empty String if no entry is 561 * associated. Note {@link #UI_STYLE_ACTIVITY_ROOT} will return empty String. The method 562 * returns undefined value if the fragment is not in FragmentManager. 563 * @return BackStackEntry name for the GuidedStepFragment or empty String if no entry is 564 * associated. 565 */ 566 final String generateStackEntryName() { 567 return generateStackEntryName(getUiStyle(), getClass()); 568 } 569 570 /** 571 * Generates BackStackEntry name for GuidedStepFragment class or empty String if no entry is 572 * associated. Note {@link #UI_STYLE_ACTIVITY_ROOT} is not allowed and returns empty String. 573 * @param uiStyle {@link #UI_STYLE_REPLACE} or {@link #UI_STYLE_ENTRANCE} 574 * @return BackStackEntry name for the GuidedStepFragment or empty String if no entry is 575 * associated. 576 */ 577 static String generateStackEntryName(int uiStyle, Class guidedStepFragmentClass) { 578 switch (uiStyle) { 579 case UI_STYLE_REPLACE: 580 return ENTRY_NAME_REPLACE + guidedStepFragmentClass.getName(); 581 case UI_STYLE_ENTRANCE: 582 return ENTRY_NAME_ENTRANCE + guidedStepFragmentClass.getName(); 583 case UI_STYLE_ACTIVITY_ROOT: 584 default: 585 return ""; 586 } 587 } 588 589 /** 590 * Returns true if the backstack entry represents GuidedStepFragment with 591 * {@link #UI_STYLE_ENTRANCE}, i.e. this is the first GuidedStepFragment pushed to stack; false 592 * otherwise. 593 * @see #generateStackEntryName(int, Class) 594 * @param backStackEntryName Name of BackStackEntry. 595 * @return True if the backstack represents GuidedStepFragment with {@link #UI_STYLE_ENTRANCE}; 596 * false otherwise. 597 */ 598 static boolean isStackEntryUiStyleEntrance(String backStackEntryName) { 599 return backStackEntryName != null && backStackEntryName.startsWith(ENTRY_NAME_ENTRANCE); 600 } 601 602 /** 603 * Extract Class name from BackStackEntry name. 604 * @param backStackEntryName Name of BackStackEntry. 605 * @return Class name of GuidedStepFragment. 606 */ 607 static String getGuidedStepFragmentClassName(String backStackEntryName) { 608 if (backStackEntryName.startsWith(ENTRY_NAME_REPLACE)) { 609 return backStackEntryName.substring(ENTRY_NAME_REPLACE.length()); 610 } else if (backStackEntryName.startsWith(ENTRY_NAME_ENTRANCE)) { 611 return backStackEntryName.substring(ENTRY_NAME_ENTRANCE.length()); 612 } else { 613 return ""; 614 } 615 } 616 617 /** 618 * Adds the specified GuidedStepFragment as content of Activity; no backstack entry is added so 619 * the activity will be dismissed when BACK key is pressed. The method is typically called in 620 * Activity.onCreate() when savedInstanceState is null. When savedInstanceState is not null, 621 * the Activity is being restored, do not call addAsRoot() to duplicate the Fragment restored 622 * by FragmentManager. 623 * {@link #UI_STYLE_ACTIVITY_ROOT} is assigned. 624 * 625 * Note: currently fragments added using this method must be created programmatically rather 626 * than via XML. 627 * @param activity The Activity to be used to insert GuidedstepFragment. 628 * @param fragment The GuidedStepFragment to be inserted into the fragment stack. 629 * @param id The id of container to add GuidedStepFragment, can be android.R.id.content. 630 * @return The ID returned by the call FragmentTransaction.commit, or -1 there is already 631 * GuidedStepFragment. 632 */ 633 public static int addAsRoot(Activity activity, GuidedStepFragment fragment, int id) { 634 // Workaround b/23764120: call getDecorView() to force requestFeature of ActivityTransition. 635 activity.getWindow().getDecorView(); 636 FragmentManager fragmentManager = activity.getFragmentManager(); 637 if (fragmentManager.findFragmentByTag(TAG_LEAN_BACK_ACTIONS_FRAGMENT) != null) { 638 Log.w(TAG, "Fragment is already exists, likely calling " 639 + "addAsRoot() when savedInstanceState is not null in Activity.onCreate()."); 640 return -1; 641 } 642 FragmentTransaction ft = fragmentManager.beginTransaction(); 643 fragment.setUiStyle(UI_STYLE_ACTIVITY_ROOT); 644 return ft.replace(id, fragment, TAG_LEAN_BACK_ACTIONS_FRAGMENT).commit(); 645 } 646 647 /** 648 * Returns the current GuidedStepFragment on the fragment transaction stack. 649 * @return The current GuidedStepFragment, if any, on the fragment transaction stack. 650 */ 651 public static GuidedStepFragment getCurrentGuidedStepFragment(FragmentManager fm) { 652 Fragment f = fm.findFragmentByTag(TAG_LEAN_BACK_ACTIONS_FRAGMENT); 653 if (f instanceof GuidedStepFragment) { 654 return (GuidedStepFragment) f; 655 } 656 return null; 657 } 658 659 /** 660 * Returns the GuidanceStylist that displays guidance information for the user. 661 * @return The GuidanceStylist for this fragment. 662 */ 663 public GuidanceStylist getGuidanceStylist() { 664 return mGuidanceStylist; 665 } 666 667 /** 668 * Returns the GuidedActionsStylist that displays the actions the user may take. 669 * @return The GuidedActionsStylist for this fragment. 670 */ 671 public GuidedActionsStylist getGuidedActionsStylist() { 672 return mActionsStylist; 673 } 674 675 /** 676 * Returns the list of button GuidedActions that the user may take in this fragment. 677 * @return The list of button GuidedActions for this fragment. 678 */ 679 public List<GuidedAction> getButtonActions() { 680 return mButtonActions; 681 } 682 683 /** 684 * Find button GuidedAction by Id. 685 * @param id Id of the button action to search. 686 * @return GuidedAction object or null if not found. 687 */ 688 public GuidedAction findButtonActionById(long id) { 689 int index = findButtonActionPositionById(id); 690 return index >= 0 ? mButtonActions.get(index) : null; 691 } 692 693 /** 694 * Find button GuidedAction position in array by Id. 695 * @param id Id of the button action to search. 696 * @return position of GuidedAction object in array or -1 if not found. 697 */ 698 public int findButtonActionPositionById(long id) { 699 if (mButtonActions != null) { 700 for (int i = 0; i < mButtonActions.size(); i++) { 701 GuidedAction action = mButtonActions.get(i); 702 if (mButtonActions.get(i).getId() == id) { 703 return i; 704 } 705 } 706 } 707 return -1; 708 } 709 710 /** 711 * Returns the GuidedActionsStylist that displays the button actions the user may take. 712 * @return The GuidedActionsStylist for this fragment. 713 */ 714 public GuidedActionsStylist getGuidedButtonActionsStylist() { 715 return mButtonActionsStylist; 716 } 717 718 /** 719 * Sets the list of button GuidedActions that the user may take in this fragment. 720 * @param actions The list of button GuidedActions for this fragment. 721 */ 722 public void setButtonActions(List<GuidedAction> actions) { 723 mButtonActions = actions; 724 if (mButtonAdapter != null) { 725 mButtonAdapter.setActions(mButtonActions); 726 } 727 } 728 729 /** 730 * Notify an button action has changed and update its UI. 731 * @param position Position of the button GuidedAction in array. 732 */ 733 public void notifyButtonActionChanged(int position) { 734 if (mButtonAdapter != null) { 735 mButtonAdapter.notifyItemChanged(position); 736 } 737 } 738 739 /** 740 * Returns the view corresponding to the button action at the indicated position in the list of 741 * actions for this fragment. 742 * @param position The integer position of the button action of interest. 743 * @return The View corresponding to the button action at the indicated position, or null if 744 * that action is not currently onscreen. 745 */ 746 public View getButtonActionItemView(int position) { 747 final RecyclerView.ViewHolder holder = mButtonActionsStylist.getActionsGridView() 748 .findViewHolderForPosition(position); 749 return holder == null ? null : holder.itemView; 750 } 751 752 /** 753 * Scrolls the action list to the position indicated, selecting that button action's view. 754 * @param position The integer position of the button action of interest. 755 */ 756 public void setSelectedButtonActionPosition(int position) { 757 mButtonActionsStylist.getActionsGridView().setSelectedPosition(position); 758 } 759 760 /** 761 * Returns the position if the currently selected button GuidedAction. 762 * @return position The integer position of the currently selected button action. 763 */ 764 public int getSelectedButtonActionPosition() { 765 return mButtonActionsStylist.getActionsGridView().getSelectedPosition(); 766 } 767 768 /** 769 * Returns the list of GuidedActions that the user may take in this fragment. 770 * @return The list of GuidedActions for this fragment. 771 */ 772 public List<GuidedAction> getActions() { 773 return mActions; 774 } 775 776 /** 777 * Find GuidedAction by Id. 778 * @param id Id of the action to search. 779 * @return GuidedAction object or null if not found. 780 */ 781 public GuidedAction findActionById(long id) { 782 int index = findActionPositionById(id); 783 return index >= 0 ? mActions.get(index) : null; 784 } 785 786 /** 787 * Find GuidedAction position in array by Id. 788 * @param id Id of the action to search. 789 * @return position of GuidedAction object in array or -1 if not found. 790 */ 791 public int findActionPositionById(long id) { 792 if (mActions != null) { 793 for (int i = 0; i < mActions.size(); i++) { 794 GuidedAction action = mActions.get(i); 795 if (mActions.get(i).getId() == id) { 796 return i; 797 } 798 } 799 } 800 return -1; 801 } 802 803 /** 804 * Sets the list of GuidedActions that the user may take in this fragment. 805 * @param actions The list of GuidedActions for this fragment. 806 */ 807 public void setActions(List<GuidedAction> actions) { 808 mActions = actions; 809 if (mAdapter != null) { 810 mAdapter.setActions(mActions); 811 } 812 } 813 814 /** 815 * Notify an action has changed and update its UI. 816 * @param position Position of the GuidedAction in array. 817 */ 818 public void notifyActionChanged(int position) { 819 if (mAdapter != null) { 820 mAdapter.notifyItemChanged(position); 821 } 822 } 823 824 /** 825 * Returns the view corresponding to the action at the indicated position in the list of 826 * actions for this fragment. 827 * @param position The integer position of the action of interest. 828 * @return The View corresponding to the action at the indicated position, or null if that 829 * action is not currently onscreen. 830 */ 831 public View getActionItemView(int position) { 832 final RecyclerView.ViewHolder holder = mActionsStylist.getActionsGridView() 833 .findViewHolderForPosition(position); 834 return holder == null ? null : holder.itemView; 835 } 836 837 /** 838 * Scrolls the action list to the position indicated, selecting that action's view. 839 * @param position The integer position of the action of interest. 840 */ 841 public void setSelectedActionPosition(int position) { 842 mActionsStylist.getActionsGridView().setSelectedPosition(position); 843 } 844 845 /** 846 * Returns the position if the currently selected GuidedAction. 847 * @return position The integer position of the currently selected action. 848 */ 849 public int getSelectedActionPosition() { 850 return mActionsStylist.getActionsGridView().getSelectedPosition(); 851 } 852 853 /** 854 * Called by Constructor to provide fragment transitions. The default implementation assigns 855 * transitions based on {@link #getUiStyle()}: 856 * <ul> 857 * <li> {@link #UI_STYLE_REPLACE} Slide from/to end(right) for enter transition, slide from/to 858 * start(left) for exit transition, shared element enter transition is set to ChangeBounds. 859 * <li> {@link #UI_STYLE_ENTRANCE} Enter transition is set to slide from both sides, exit 860 * transition is same as {@link #UI_STYLE_REPLACE}, no shared element enter transition. 861 * <li> {@link #UI_STYLE_ACTIVITY_ROOT} Enter transition is set to null and app should rely on 862 * activity transition, exit transition is same as {@link #UI_STYLE_REPLACE}, no shared element 863 * enter transition. 864 * </ul> 865 * <p> 866 * The default implementation heavily relies on {@link GuidedActionsStylist} and 867 * {@link GuidanceStylist} layout, app may override this method when modifying the default 868 * layout of {@link GuidedActionsStylist} or {@link GuidanceStylist}. 869 * <p> 870 * TIP: because the fragment view is removed during fragment transition, in general app cannot 871 * use two Visibility transition together. Workaround is to create your own Visibility 872 * transition that controls multiple animators (e.g. slide and fade animation in one Transition 873 * class). 874 */ 875 protected void onProvideFragmentTransitions() { 876 if (Build.VERSION.SDK_INT >= 21) { 877 final int uiStyle = getUiStyle(); 878 if (uiStyle == UI_STYLE_REPLACE) { 879 Object enterTransition = TransitionHelper.createFadeAndShortSlide(Gravity.END); 880 TransitionHelper.exclude(enterTransition, R.id.guidedstep_background, true); 881 TransitionHelper.exclude(enterTransition, R.id.guidedactions_sub_list_background, 882 true); 883 TransitionHelper.setEnterTransition(this, enterTransition); 884 885 Object fade = TransitionHelper.createFadeTransition( 886 TransitionHelper.FADE_IN | TransitionHelper.FADE_OUT); 887 TransitionHelper.include(fade, R.id.guidedactions_sub_list_background); 888 Object changeBounds = TransitionHelper.createChangeBounds(false); 889 Object sharedElementTransition = TransitionHelper.createTransitionSet(false); 890 TransitionHelper.addTransition(sharedElementTransition, fade); 891 TransitionHelper.addTransition(sharedElementTransition, changeBounds); 892 TransitionHelper.setSharedElementEnterTransition(this, sharedElementTransition); 893 } else if (uiStyle == UI_STYLE_ENTRANCE) { 894 if (entranceTransitionType == SLIDE_FROM_SIDE) { 895 Object fade = TransitionHelper.createFadeTransition( 896 TransitionHelper.FADE_IN | TransitionHelper.FADE_OUT); 897 TransitionHelper.include(fade, R.id.guidedstep_background); 898 Object slideFromSide = TransitionHelper.createFadeAndShortSlide( 899 Gravity.END | Gravity.START); 900 TransitionHelper.include(slideFromSide, R.id.content_fragment); 901 TransitionHelper.include(slideFromSide, R.id.action_fragment_root); 902 Object enterTransition = TransitionHelper.createTransitionSet(false); 903 TransitionHelper.addTransition(enterTransition, fade); 904 TransitionHelper.addTransition(enterTransition, slideFromSide); 905 TransitionHelper.setEnterTransition(this, enterTransition); 906 } else { 907 Object slideFromBottom = TransitionHelper.createFadeAndShortSlide( 908 Gravity.BOTTOM); 909 TransitionHelper.include(slideFromBottom, R.id.guidedstep_background_view_root); 910 Object enterTransition = TransitionHelper.createTransitionSet(false); 911 TransitionHelper.addTransition(enterTransition, slideFromBottom); 912 TransitionHelper.setEnterTransition(this, enterTransition); 913 } 914 // No shared element transition 915 TransitionHelper.setSharedElementEnterTransition(this, null); 916 } else if (uiStyle == UI_STYLE_ACTIVITY_ROOT) { 917 // for Activity root, we don't need enter transition, use activity transition 918 TransitionHelper.setEnterTransition(this, null); 919 // No shared element transition 920 TransitionHelper.setSharedElementEnterTransition(this, null); 921 } 922 // exitTransition is same for all style 923 Object exitTransition = TransitionHelper.createFadeAndShortSlide(Gravity.START); 924 TransitionHelper.exclude(exitTransition, R.id.guidedstep_background, true); 925 TransitionHelper.exclude(exitTransition, R.id.guidedactions_sub_list_background, 926 true); 927 TransitionHelper.setExitTransition(this, exitTransition); 928 } 929 } 930 931 /** 932 * Called by onCreateView to inflate background view. Default implementation loads view 933 * from {@link R.layout#lb_guidedstep_background} which holds a reference to 934 * guidedStepBackground. 935 * @param inflater LayoutInflater to load background view. 936 * @param container Parent view of background view. 937 * @param savedInstanceState 938 * @return Created background view or null if no background. 939 */ 940 public View onCreateBackgroundView(LayoutInflater inflater, ViewGroup container, 941 Bundle savedInstanceState) { 942 return inflater.inflate(R.layout.lb_guidedstep_background, container, false); 943 } 944 945 /** 946 * Set UI style to fragment arguments. Default value is {@link #UI_STYLE_ENTRANCE} when fragment 947 * is first initialized. UI style is used to choose different fragment transition animations and 948 * determine if this is the first GuidedStepFragment on backstack. In most cases app does not 949 * directly call this method, app calls helper function 950 * {@link #add(FragmentManager, GuidedStepFragment, int)}. However if the app creates Fragment 951 * transaction and controls backstack by itself, it would need call setUiStyle() to select the 952 * fragment transition to use. 953 * 954 * @param style {@link #UI_STYLE_ACTIVITY_ROOT} {@link #UI_STYLE_REPLACE} or 955 * {@link #UI_STYLE_ENTRANCE}. 956 */ 957 public void setUiStyle(int style) { 958 int oldStyle = getUiStyle(); 959 Bundle arguments = getArguments(); 960 boolean isNew = false; 961 if (arguments == null) { 962 arguments = new Bundle(); 963 isNew = true; 964 } 965 arguments.putInt(EXTRA_UI_STYLE, style); 966 // call setArgument() will validate if the fragment is already added. 967 if (isNew) { 968 setArguments(arguments); 969 } 970 if (style != oldStyle) { 971 onProvideFragmentTransitions(); 972 } 973 } 974 975 /** 976 * Read UI style from fragment arguments. Default value is {@link #UI_STYLE_ENTRANCE} when 977 * fragment is first initialized. UI style is used to choose different fragment transition 978 * animations and determine if this is the first GuidedStepFragment on backstack. 979 * 980 * @return {@link #UI_STYLE_ACTIVITY_ROOT} {@link #UI_STYLE_REPLACE} or 981 * {@link #UI_STYLE_ENTRANCE}. 982 * @see #onProvideFragmentTransitions() 983 */ 984 public int getUiStyle() { 985 Bundle b = getArguments(); 986 if (b == null) return UI_STYLE_ENTRANCE; 987 return b.getInt(EXTRA_UI_STYLE, UI_STYLE_ENTRANCE); 988 } 989 990 /** 991 * {@inheritDoc} 992 */ 993 @Override 994 public void onCreate(Bundle savedInstanceState) { 995 super.onCreate(savedInstanceState); 996 if (DEBUG) Log.v(TAG, "onCreate"); 997 // Set correct transition from saved arguments. 998 onProvideFragmentTransitions(); 999 1000 ArrayList<GuidedAction> actions = new ArrayList<GuidedAction>(); 1001 onCreateActions(actions, savedInstanceState); 1002 if (savedInstanceState != null) { 1003 onRestoreActions(actions, savedInstanceState); 1004 } 1005 setActions(actions); 1006 ArrayList<GuidedAction> buttonActions = new ArrayList<GuidedAction>(); 1007 onCreateButtonActions(buttonActions, savedInstanceState); 1008 if (savedInstanceState != null) { 1009 onRestoreButtonActions(buttonActions, savedInstanceState); 1010 } 1011 setButtonActions(buttonActions); 1012 } 1013 1014 /** 1015 * {@inheritDoc} 1016 */ 1017 @Override 1018 public void onDestroyView() { 1019 mGuidanceStylist.onDestroyView(); 1020 mActionsStylist.onDestroyView(); 1021 mButtonActionsStylist.onDestroyView(); 1022 mAdapter = null; 1023 mSubAdapter = null; 1024 mButtonAdapter = null; 1025 mAdapterGroup = null; 1026 super.onDestroyView(); 1027 } 1028 1029 /** 1030 * {@inheritDoc} 1031 */ 1032 @Override 1033 public View onCreateView(LayoutInflater inflater, ViewGroup container, 1034 Bundle savedInstanceState) { 1035 if (DEBUG) Log.v(TAG, "onCreateView"); 1036 1037 resolveTheme(); 1038 inflater = getThemeInflater(inflater); 1039 1040 GuidedStepRootLayout root = (GuidedStepRootLayout) inflater.inflate( 1041 R.layout.lb_guidedstep_fragment, container, false); 1042 1043 root.setFocusOutStart(isFocusOutStartAllowed()); 1044 root.setFocusOutEnd(isFocusOutEndAllowed()); 1045 1046 ViewGroup guidanceContainer = (ViewGroup) root.findViewById(R.id.content_fragment); 1047 ViewGroup actionContainer = (ViewGroup) root.findViewById(R.id.action_fragment); 1048 1049 Guidance guidance = onCreateGuidance(savedInstanceState); 1050 View guidanceView = mGuidanceStylist.onCreateView(inflater, guidanceContainer, guidance); 1051 guidanceContainer.addView(guidanceView); 1052 1053 View actionsView = mActionsStylist.onCreateView(inflater, actionContainer); 1054 actionContainer.addView(actionsView); 1055 1056 View buttonActionsView = mButtonActionsStylist.onCreateView(inflater, actionContainer); 1057 actionContainer.addView(buttonActionsView); 1058 1059 GuidedActionAdapter.EditListener editListener = new GuidedActionAdapter.EditListener() { 1060 1061 @Override 1062 public void onImeOpen() { 1063 runImeAnimations(true); 1064 } 1065 1066 @Override 1067 public void onImeClose() { 1068 runImeAnimations(false); 1069 } 1070 1071 @Override 1072 public long onGuidedActionEditedAndProceed(GuidedAction action) { 1073 return GuidedStepFragment.this.onGuidedActionEditedAndProceed(action); 1074 } 1075 1076 @Override 1077 public void onGuidedActionEditCanceled(GuidedAction action) { 1078 GuidedStepFragment.this.onGuidedActionEditCanceled(action); 1079 } 1080 }; 1081 1082 mAdapter = new GuidedActionAdapter(mActions, new GuidedActionAdapter.ClickListener() { 1083 @Override 1084 public void onGuidedActionClicked(GuidedAction action) { 1085 GuidedStepFragment.this.onGuidedActionClicked(action); 1086 if (isExpanded()) { 1087 collapseAction(true); 1088 } else if (action.hasSubActions() || action.hasEditableActivatorView()) { 1089 expandAction(action, true); 1090 } 1091 } 1092 }, this, mActionsStylist, false); 1093 mButtonAdapter = 1094 new GuidedActionAdapter(mButtonActions, new GuidedActionAdapter.ClickListener() { 1095 @Override 1096 public void onGuidedActionClicked(GuidedAction action) { 1097 GuidedStepFragment.this.onGuidedActionClicked(action); 1098 } 1099 }, this, mButtonActionsStylist, false); 1100 mSubAdapter = new GuidedActionAdapter(null, new GuidedActionAdapter.ClickListener() { 1101 @Override 1102 public void onGuidedActionClicked(GuidedAction action) { 1103 if (mActionsStylist.isInExpandTransition()) { 1104 return; 1105 } 1106 if (GuidedStepFragment.this.onSubGuidedActionClicked(action)) { 1107 collapseSubActions(); 1108 } 1109 } 1110 }, this, mActionsStylist, true); 1111 mAdapterGroup = new GuidedActionAdapterGroup(); 1112 mAdapterGroup.addAdpter(mAdapter, mButtonAdapter); 1113 mAdapterGroup.addAdpter(mSubAdapter, null); 1114 mAdapterGroup.setEditListener(editListener); 1115 mActionsStylist.setEditListener(editListener); 1116 1117 mActionsStylist.getActionsGridView().setAdapter(mAdapter); 1118 if (mActionsStylist.getSubActionsGridView() != null) { 1119 mActionsStylist.getSubActionsGridView().setAdapter(mSubAdapter); 1120 } 1121 mButtonActionsStylist.getActionsGridView().setAdapter(mButtonAdapter); 1122 if (mButtonActions.size() == 0) { 1123 // when there is no button actions, we don't need show the second panel, but keep 1124 // the width zero to run ChangeBounds transition. 1125 LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) 1126 buttonActionsView.getLayoutParams(); 1127 lp.weight = 0; 1128 buttonActionsView.setLayoutParams(lp); 1129 } else { 1130 // when there are two actions panel, we need adjust the weight of action to 1131 // guidedActionContentWidthWeightTwoPanels. 1132 Context ctx = mThemeWrapper != null ? mThemeWrapper : FragmentUtil.getContext(this); 1133 TypedValue typedValue = new TypedValue(); 1134 if (ctx.getTheme().resolveAttribute(R.attr.guidedActionContentWidthWeightTwoPanels, 1135 typedValue, true)) { 1136 View actionsRoot = root.findViewById(R.id.action_fragment_root); 1137 float weight = typedValue.getFloat(); 1138 LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) actionsRoot 1139 .getLayoutParams(); 1140 lp.weight = weight; 1141 actionsRoot.setLayoutParams(lp); 1142 } 1143 } 1144 1145 // Add the background view. 1146 View backgroundView = onCreateBackgroundView(inflater, root, savedInstanceState); 1147 if (backgroundView != null) { 1148 FrameLayout backgroundViewRoot = (FrameLayout)root.findViewById( 1149 R.id.guidedstep_background_view_root); 1150 backgroundViewRoot.addView(backgroundView, 0); 1151 } 1152 1153 return root; 1154 } 1155 1156 @Override 1157 public void onResume() { 1158 super.onResume(); 1159 getView().findViewById(R.id.action_fragment).requestFocus(); 1160 } 1161 1162 /** 1163 * Get the key will be used to save GuidedAction with Fragment. 1164 * @param action GuidedAction to get key. 1165 * @return Key to save the GuidedAction. 1166 */ 1167 final String getAutoRestoreKey(GuidedAction action) { 1168 return EXTRA_ACTION_PREFIX + action.getId(); 1169 } 1170 1171 /** 1172 * Get the key will be used to save GuidedAction with Fragment. 1173 * @param action GuidedAction to get key. 1174 * @return Key to save the GuidedAction. 1175 */ 1176 final String getButtonAutoRestoreKey(GuidedAction action) { 1177 return EXTRA_BUTTON_ACTION_PREFIX + action.getId(); 1178 } 1179 1180 final static boolean isSaveEnabled(GuidedAction action) { 1181 return action.isAutoSaveRestoreEnabled() && action.getId() != GuidedAction.NO_ID; 1182 } 1183 1184 final void onRestoreActions(List<GuidedAction> actions, Bundle savedInstanceState) { 1185 for (int i = 0, size = actions.size(); i < size; i++) { 1186 GuidedAction action = actions.get(i); 1187 if (isSaveEnabled(action)) { 1188 action.onRestoreInstanceState(savedInstanceState, getAutoRestoreKey(action)); 1189 } 1190 } 1191 } 1192 1193 final void onRestoreButtonActions(List<GuidedAction> actions, Bundle savedInstanceState) { 1194 for (int i = 0, size = actions.size(); i < size; i++) { 1195 GuidedAction action = actions.get(i); 1196 if (isSaveEnabled(action)) { 1197 action.onRestoreInstanceState(savedInstanceState, getButtonAutoRestoreKey(action)); 1198 } 1199 } 1200 } 1201 1202 final void onSaveActions(List<GuidedAction> actions, Bundle outState) { 1203 for (int i = 0, size = actions.size(); i < size; i++) { 1204 GuidedAction action = actions.get(i); 1205 if (isSaveEnabled(action)) { 1206 action.onSaveInstanceState(outState, getAutoRestoreKey(action)); 1207 } 1208 } 1209 } 1210 1211 final void onSaveButtonActions(List<GuidedAction> actions, Bundle outState) { 1212 for (int i = 0, size = actions.size(); i < size; i++) { 1213 GuidedAction action = actions.get(i); 1214 if (isSaveEnabled(action)) { 1215 action.onSaveInstanceState(outState, getButtonAutoRestoreKey(action)); 1216 } 1217 } 1218 } 1219 1220 /** 1221 * {@inheritDoc} 1222 */ 1223 @Override 1224 public void onSaveInstanceState(Bundle outState) { 1225 super.onSaveInstanceState(outState); 1226 onSaveActions(mActions, outState); 1227 onSaveButtonActions(mButtonActions, outState); 1228 } 1229 1230 private static boolean isGuidedStepTheme(Context context) { 1231 int resId = R.attr.guidedStepThemeFlag; 1232 TypedValue typedValue = new TypedValue(); 1233 boolean found = context.getTheme().resolveAttribute(resId, typedValue, true); 1234 if (DEBUG) Log.v(TAG, "Found guided step theme flag? " + found); 1235 return found && typedValue.type == TypedValue.TYPE_INT_BOOLEAN && typedValue.data != 0; 1236 } 1237 1238 /** 1239 * Convenient method to close GuidedStepFragments on top of other content or finish Activity if 1240 * GuidedStepFragments were started in a separate activity. Pops all stack entries including 1241 * {@link #UI_STYLE_ENTRANCE}; if {@link #UI_STYLE_ENTRANCE} is not found, finish the activity. 1242 * Note that this method must be paired with {@link #add(FragmentManager, GuidedStepFragment, 1243 * int)} which sets up the stack entry name for finding which fragment we need to pop back to. 1244 */ 1245 public void finishGuidedStepFragments() { 1246 final FragmentManager fragmentManager = getFragmentManager(); 1247 final int entryCount = fragmentManager.getBackStackEntryCount(); 1248 if (entryCount > 0) { 1249 for (int i = entryCount - 1; i >= 0; i--) { 1250 BackStackEntry entry = fragmentManager.getBackStackEntryAt(i); 1251 if (isStackEntryUiStyleEntrance(entry.getName())) { 1252 GuidedStepFragment top = getCurrentGuidedStepFragment(fragmentManager); 1253 if (top != null) { 1254 top.setUiStyle(UI_STYLE_ENTRANCE); 1255 } 1256 fragmentManager.popBackStackImmediate(entry.getId(), 1257 FragmentManager.POP_BACK_STACK_INCLUSIVE); 1258 return; 1259 } 1260 } 1261 } 1262 ActivityCompat.finishAfterTransition(getActivity()); 1263 } 1264 1265 /** 1266 * Convenient method to pop to fragment with Given class. 1267 * @param guidedStepFragmentClass Name of the Class of GuidedStepFragment to pop to. 1268 * @param flags Either 0 or {@link FragmentManager#POP_BACK_STACK_INCLUSIVE}. 1269 */ 1270 public void popBackStackToGuidedStepFragment(Class guidedStepFragmentClass, int flags) { 1271 if (!GuidedStepFragment.class.isAssignableFrom(guidedStepFragmentClass)) { 1272 return; 1273 } 1274 final FragmentManager fragmentManager = getFragmentManager(); 1275 final int entryCount = fragmentManager.getBackStackEntryCount(); 1276 String className = guidedStepFragmentClass.getName(); 1277 if (entryCount > 0) { 1278 for (int i = entryCount - 1; i >= 0; i--) { 1279 BackStackEntry entry = fragmentManager.getBackStackEntryAt(i); 1280 String entryClassName = getGuidedStepFragmentClassName(entry.getName()); 1281 if (className.equals(entryClassName)) { 1282 fragmentManager.popBackStackImmediate(entry.getId(), flags); 1283 return; 1284 } 1285 } 1286 } 1287 } 1288 1289 /** 1290 * Returns true if allows focus out of start edge of GuidedStepFragment, false otherwise. 1291 * Default value is false, the reason is to disable FocusFinder to find focusable views 1292 * beneath content of GuidedStepFragment. Subclass may override. 1293 * @return True if allows focus out of start edge of GuidedStepFragment. 1294 */ 1295 public boolean isFocusOutStartAllowed() { 1296 return false; 1297 } 1298 1299 /** 1300 * Returns true if allows focus out of end edge of GuidedStepFragment, false otherwise. 1301 * Default value is false, the reason is to disable FocusFinder to find focusable views 1302 * beneath content of GuidedStepFragment. Subclass may override. 1303 * @return True if allows focus out of end edge of GuidedStepFragment. 1304 */ 1305 public boolean isFocusOutEndAllowed() { 1306 return false; 1307 } 1308 1309 /** 1310 * Sets the transition type to be used for {@link #UI_STYLE_ENTRANCE} animation. 1311 * Currently we provide 2 different variations for animation - slide in from 1312 * side (default) or bottom. 1313 * 1314 * Ideally we can retrieve the screen mode settings from the theme attribute 1315 * {@code Theme.Leanback.GuidedStep#guidedStepHeightWeight} and use that to 1316 * determine the transition. But the fragment context to retrieve the theme 1317 * isn't available on platform v23 or earlier. 1318 * 1319 * For now clients(subclasses) can call this method inside the constructor. 1320 * @hide 1321 */ 1322 @RestrictTo(LIBRARY_GROUP) 1323 public void setEntranceTransitionType(int transitionType) { 1324 this.entranceTransitionType = transitionType; 1325 } 1326 1327 /** 1328 * Opens the provided action in edit mode and raises ime. This can be 1329 * used to programmatically skip the extra click required to go into edit mode. This method 1330 * can be invoked in {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}. 1331 */ 1332 public void openInEditMode(GuidedAction action) { 1333 mActionsStylist.openInEditMode(action); 1334 } 1335 1336 private void resolveTheme() { 1337 // Look up the guidedStepTheme in the currently specified theme. If it exists, 1338 // replace the theme with its value. 1339 Context context = FragmentUtil.getContext(this); 1340 int theme = onProvideTheme(); 1341 if (theme == -1 && !isGuidedStepTheme(context)) { 1342 // Look up the guidedStepTheme in the activity's currently specified theme. If it 1343 // exists, replace the theme with its value. 1344 int resId = R.attr.guidedStepTheme; 1345 TypedValue typedValue = new TypedValue(); 1346 boolean found = context.getTheme().resolveAttribute(resId, typedValue, true); 1347 if (DEBUG) Log.v(TAG, "Found guided step theme reference? " + found); 1348 if (found) { 1349 ContextThemeWrapper themeWrapper = 1350 new ContextThemeWrapper(context, typedValue.resourceId); 1351 if (isGuidedStepTheme(themeWrapper)) { 1352 mThemeWrapper = themeWrapper; 1353 } else { 1354 found = false; 1355 mThemeWrapper = null; 1356 } 1357 } 1358 if (!found) { 1359 Log.e(TAG, "GuidedStepFragment does not have an appropriate theme set."); 1360 } 1361 } else if (theme != -1) { 1362 mThemeWrapper = new ContextThemeWrapper(context, theme); 1363 } 1364 } 1365 1366 private LayoutInflater getThemeInflater(LayoutInflater inflater) { 1367 if (mThemeWrapper == null) { 1368 return inflater; 1369 } else { 1370 return inflater.cloneInContext(mThemeWrapper); 1371 } 1372 } 1373 1374 private int getFirstCheckedAction() { 1375 for (int i = 0, size = mActions.size(); i < size; i++) { 1376 if (mActions.get(i).isChecked()) { 1377 return i; 1378 } 1379 } 1380 return 0; 1381 } 1382 1383 void runImeAnimations(boolean entering) { 1384 ArrayList<Animator> animators = new ArrayList<Animator>(); 1385 if (entering) { 1386 mGuidanceStylist.onImeAppearing(animators); 1387 mActionsStylist.onImeAppearing(animators); 1388 mButtonActionsStylist.onImeAppearing(animators); 1389 } else { 1390 mGuidanceStylist.onImeDisappearing(animators); 1391 mActionsStylist.onImeDisappearing(animators); 1392 mButtonActionsStylist.onImeDisappearing(animators); 1393 } 1394 AnimatorSet set = new AnimatorSet(); 1395 set.playTogether(animators); 1396 set.start(); 1397 } 1398} 1399