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