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