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