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.FragmentTransaction; 22import android.content.Context; 23import android.content.res.TypedArray; 24import android.os.Bundle; 25import android.support.annotation.NonNull; 26import android.support.v17.leanback.animation.UntargetableAnimatorSet; 27import android.support.v17.leanback.R; 28import android.support.v17.leanback.widget.GuidanceStylist; 29import android.support.v17.leanback.widget.GuidanceStylist.Guidance; 30import android.support.v17.leanback.widget.GuidedAction; 31import android.support.v17.leanback.widget.GuidedActionsStylist; 32import android.support.v17.leanback.widget.VerticalGridView; 33import android.util.AttributeSet; 34import android.util.Log; 35import android.util.TypedValue; 36import android.view.ContextThemeWrapper; 37import android.view.LayoutInflater; 38import android.view.View; 39import android.view.ViewGroup; 40import android.view.ViewTreeObserver; 41import android.widget.ImageView; 42import android.widget.RelativeLayout; 43import android.widget.TextView; 44 45import java.util.ArrayList; 46import java.util.List; 47 48/** 49 * A GuidedStepFragment is used to guide the user through a decision or series of decisions. 50 * It is composed of a guidance view on the left and a view on the right containing a list of 51 * possible actions. 52 * <p> 53 * <h3>Basic Usage</h3> 54 * <p> 55 * Clients of GuidedStepFragment typically create a custom subclass to attach to their Activities. 56 * This custom subclass provides the information necessary to construct the user interface and 57 * respond to user actions. At a minimum, subclasses should override: 58 * <ul> 59 * <li>{@link #onCreateGuidance}, to provide instructions to the user</li> 60 * <li>{@link #onCreateActions}, to provide a set of {@link GuidedAction}s the user can take</li> 61 * <li>{@link #onGuidedActionClicked}, to respond to those actions</li> 62 * </ul> 63 * <p> 64 * <h3>Theming and Stylists</h3> 65 * <p> 66 * GuidedStepFragment delegates its visual styling to classes called stylists. The {@link 67 * GuidanceStylist} is responsible for the left guidance view, while the {@link 68 * GuidedActionsStylist} is responsible for the right actions view. The stylists use theme 69 * attributes to derive values associated with the presentation, such as colors, animations, etc. 70 * Most simple visual aspects of GuidanceStylist and GuidedActionsStylist can be customized 71 * via theming; see their documentation for more information. 72 * <p> 73 * GuidedStepFragments must have access to an appropriate theme in order for the stylists to 74 * function properly. Specifically, the fragment must receive {@link 75 * android.support.v17.leanback.R.style#Theme_Leanback_GuidedStep}, or a theme whose parent is 76 * is set to that theme. Themes can be provided in one of three ways: 77 * <ul> 78 * <li>The simplest way is to set the theme for the host Activity to the GuidedStep theme or a 79 * theme that derives from it.</li> 80 * <li>If the Activity already has a theme and setting its parent theme is inconvenient, the 81 * existing Activity theme can have an entry added for the attribute {@link 82 * android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedStepTheme}. If present, 83 * this theme will be used by GuidedStepFragment as an overlay to the Activity's theme.</li> 84 * <li>Finally, custom subclasses of GuidedStepFragment may provide a theme through the {@link 85 * #onProvideTheme} method. This can be useful if a subclass is used across multiple 86 * Activities.</li> 87 * </ul> 88 * <p> 89 * If the theme is provided in multiple ways, the onProvideTheme override has priority, followed by 90 * the Activty's theme. (Themes whose parent theme is already set to the guided step theme do not 91 * need to set the guidedStepTheme attribute; if set, it will be ignored.) 92 * <p> 93 * If themes do not provide enough customizability, the stylists themselves may be subclassed and 94 * provided to the GuidedStepFragment through the {@link #onCreateGuidanceStylist} and {@link 95 * #onCreateActionsStylist} methods. The stylists have simple hooks so that subclasses 96 * may override layout files; subclasses may also have more complex logic to determine styling. 97 * <p> 98 * <h3>Guided sequences</h3> 99 * <p> 100 * GuidedStepFragments can be grouped together to provide a guided sequence. GuidedStepFragments 101 * grouped as a sequence use custom animations provided by {@link GuidanceStylist} and 102 * {@link GuidedActionsStylist} (or subclasses) during transitions between steps. Clients 103 * should use {@link #add} to place subsequent GuidedFragments onto the fragment stack so that 104 * custom animations are properly configured. (Custom animations are triggered automatically when 105 * the fragment stack is subsequently popped by any normal mechanism.) 106 * <p> 107 * <i>Note: Currently GuidedStepFragments grouped in this way must all be defined programmatically, 108 * rather than in XML. This restriction may be removed in the future.</i> 109 * <p> 110 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedStepTheme 111 * @see GuidanceStylist 112 * @see GuidanceStylist.Guidance 113 * @see GuidedAction 114 * @see GuidedActionsStylist 115 */ 116public class GuidedStepFragment extends Fragment implements GuidedActionAdapter.ClickListener, 117 GuidedActionAdapter.FocusListener { 118 119 private static final String TAG_LEAN_BACK_ACTIONS_FRAGMENT = "leanBackGuidedStepFragment"; 120 private static final String EXTRA_ACTION_SELECTED_INDEX = "selectedIndex"; 121 private static final String EXTRA_ACTION_ENTRY_TRANSITION_ENABLED = "entryTransitionEnabled"; 122 private static final String EXTRA_ENTRY_TRANSITION_PERFORMED = "entryTransitionPerformed"; 123 private static final String TAG = "GuidedStepFragment"; 124 private static final boolean DEBUG = true; 125 private static final int ANIMATION_FRAGMENT_ENTER = 1; 126 private static final int ANIMATION_FRAGMENT_EXIT = 2; 127 private static final int ANIMATION_FRAGMENT_ENTER_POP = 3; 128 private static final int ANIMATION_FRAGMENT_EXIT_POP = 4; 129 130 private int mTheme; 131 private GuidanceStylist mGuidanceStylist; 132 private GuidedActionsStylist mActionsStylist; 133 private GuidedActionAdapter mAdapter; 134 private VerticalGridView mListView; 135 private List<GuidedAction> mActions = new ArrayList<GuidedAction>(); 136 private int mSelectedIndex = -1; 137 private boolean mEntryTransitionPerformed; 138 private boolean mEntryTransitionEnabled = true; 139 140 public GuidedStepFragment() { 141 // We need to supply the theme before any potential call to onInflate in order 142 // for the defaulting to work properly. 143 mTheme = onProvideTheme(); 144 mGuidanceStylist = onCreateGuidanceStylist(); 145 mActionsStylist = onCreateActionsStylist(); 146 } 147 148 /** 149 * Creates the presenter used to style the guidance panel. The default implementation returns 150 * a basic GuidanceStylist. 151 * @return The GuidanceStylist used in this fragment. 152 */ 153 public GuidanceStylist onCreateGuidanceStylist() { 154 return new GuidanceStylist(); 155 } 156 157 /** 158 * Creates the presenter used to style the guided actions panel. The default implementation 159 * returns a basic GuidedActionsStylist. 160 * @return The GuidedActionsStylist used in this fragment. 161 */ 162 public GuidedActionsStylist onCreateActionsStylist() { 163 return new GuidedActionsStylist(); 164 } 165 166 /** 167 * Returns the theme used for styling the fragment. The default returns -1, indicating that the 168 * host Activity's theme should be used. 169 * @return The theme resource ID of the theme to use in this fragment, or -1 to use the 170 * host Activity's theme. 171 */ 172 public int onProvideTheme() { 173 return -1; 174 } 175 176 /** 177 * Returns the information required to provide guidance to the user. This hook is called during 178 * {@link #onCreateView}. May be overridden to return a custom subclass of {@link 179 * GuidanceStylist.Guidance} for use in a subclass of {@link GuidanceStylist}. The default 180 * returns a Guidance object with empty fields; subclasses should override. 181 * @param savedInstanceState The saved instance state from onCreateView. 182 * @return The Guidance object representing the information used to guide the user. 183 */ 184 public @NonNull Guidance onCreateGuidance(Bundle savedInstanceState) { 185 return new Guidance("", "", "", null); 186 } 187 188 /** 189 * Fills out the set of actions available to the user. This hook is called during {@link 190 * #onCreate}. The default leaves the list of actions empty; subclasses should override. 191 * @param actions A non-null, empty list ready to be populated. 192 * @param savedInstanceState The saved instance state from onCreate. 193 */ 194 public void onCreateActions(@NonNull List<GuidedAction> actions, Bundle savedInstanceState) { 195 } 196 197 /** 198 * Callback invoked when an action is taken by the user. Subclasses should override in 199 * order to act on the user's decisions. 200 * @param action The chosen action. 201 */ 202 @Override 203 public void onGuidedActionClicked(GuidedAction action) { 204 } 205 206 /** 207 * Callback invoked when an action is focused (made to be the current selection) by the user. 208 */ 209 @Override 210 public void onGuidedActionFocused(GuidedAction action) { 211 } 212 213 /** 214 * Adds the specified GuidedStepFragment to the fragment stack, replacing any existing 215 * GuidedStepFragments in the stack, and configuring the fragment-to-fragment custom animations. 216 * <p> 217 * Note: currently fragments added using this method must be created programmatically rather 218 * than via XML. 219 * @param fragmentManager The FragmentManager to be used in the transaction. 220 * @param fragment The GuidedStepFragment to be inserted into the fragment stack. 221 * @return The ID returned by the call FragmentTransaction.replace. 222 */ 223 public static int add(FragmentManager fragmentManager, GuidedStepFragment fragment) { 224 return add(fragmentManager, fragment, android.R.id.content); 225 } 226 227 // Note, this method used to be public, but I haven't found a good way for a client 228 // to specify an id. 229 private static int add(FragmentManager fm, GuidedStepFragment f, int id) { 230 boolean inGuidedStep = getCurrentGuidedStepFragment(fm) != null; 231 FragmentTransaction ft = fm.beginTransaction(); 232 233 if (inGuidedStep) { 234 ft.setCustomAnimations(ANIMATION_FRAGMENT_ENTER, 235 ANIMATION_FRAGMENT_EXIT, ANIMATION_FRAGMENT_ENTER_POP, 236 ANIMATION_FRAGMENT_EXIT_POP); 237 ft.addToBackStack(null); 238 } 239 return ft.replace(id, f, TAG_LEAN_BACK_ACTIONS_FRAGMENT).commit(); 240 } 241 242 /** 243 * Returns the current GuidedStepFragment on the fragment transaction stack. 244 * @return The current GuidedStepFragment, if any, on the fragment transaction stack. 245 */ 246 public static GuidedStepFragment getCurrentGuidedStepFragment(FragmentManager fm) { 247 Fragment f = fm.findFragmentByTag(TAG_LEAN_BACK_ACTIONS_FRAGMENT); 248 if (f instanceof GuidedStepFragment) { 249 return (GuidedStepFragment) f; 250 } 251 return null; 252 } 253 254 /** 255 * Returns the GuidanceStylist that displays guidance information for the user. 256 * @return The GuidanceStylist for this fragment. 257 */ 258 public GuidanceStylist getGuidanceStylist() { 259 return mGuidanceStylist; 260 } 261 262 /** 263 * Returns the GuidedActionsStylist that displays the actions the user may take. 264 * @return The GuidedActionsStylist for this fragment. 265 */ 266 public GuidedActionsStylist getGuidedActionsStylist() { 267 return mActionsStylist; 268 } 269 270 /** 271 * Returns the list of GuidedActions that the user may take in this fragment. 272 * @return The list of GuidedActions for this fragment. 273 */ 274 public List<GuidedAction> getActions() { 275 return mActions; 276 } 277 278 /** 279 * Sets the list of GuidedActions that the user may take in this fragment. 280 * @param actions The list of GuidedActions for this fragment. 281 */ 282 public void setActions(List<GuidedAction> actions) { 283 mActions = actions; 284 if (mAdapter != null) { 285 mAdapter.setActions(mActions); 286 } 287 } 288 289 /** 290 * Returns the view corresponding to the action at the indicated position in the list of 291 * actions for this fragment. 292 * @param position The integer position of the action of interest. 293 * @return The View corresponding to the action at the indicated position, or null if that 294 * action is not currently onscreen. 295 */ 296 public View getActionItemView(int position) { 297 return mListView.findViewHolderForPosition(position).itemView; 298 } 299 300 /** 301 * Scrolls the action list to the position indicated, selecting that action's view. 302 * @param position The integer position of the action of interest. 303 */ 304 public void setSelectedActionPosition(int position) { 305 mListView.setSelectedPosition(position); 306 } 307 308 /** 309 * Returns the position if the currently selected GuidedAction. 310 * @return position The integer position of the currently selected action. 311 */ 312 public int getSelectedActionPosition() { 313 return mListView.getSelectedPosition(); 314 } 315 316 /** 317 * {@inheritDoc} 318 */ 319 @Override 320 public void onCreate(Bundle savedInstanceState) { 321 super.onCreate(savedInstanceState); 322 if (DEBUG) Log.v(TAG, "onCreate"); 323 Bundle state = (savedInstanceState != null) ? savedInstanceState : getArguments(); 324 if (state != null) { 325 if (mSelectedIndex == -1) { 326 mSelectedIndex = state.getInt(EXTRA_ACTION_SELECTED_INDEX, -1); 327 } 328 mEntryTransitionEnabled = state.getBoolean(EXTRA_ACTION_ENTRY_TRANSITION_ENABLED, true); 329 mEntryTransitionPerformed = state.getBoolean(EXTRA_ENTRY_TRANSITION_PERFORMED, false); 330 } 331 mActions.clear(); 332 onCreateActions(mActions, savedInstanceState); 333 } 334 335 /** 336 * {@inheritDoc} 337 */ 338 @Override 339 public View onCreateView(LayoutInflater inflater, ViewGroup container, 340 Bundle savedInstanceState) { 341 if (DEBUG) Log.v(TAG, "onCreateView"); 342 343 resolveTheme(); 344 inflater = getThemeInflater(inflater); 345 346 View v = inflater.inflate(R.layout.lb_guidedstep_fragment, container, false); 347 ViewGroup guidanceContainer = (ViewGroup) v.findViewById(R.id.content_fragment); 348 ViewGroup actionContainer = (ViewGroup) v.findViewById(R.id.action_fragment); 349 350 Guidance guidance = onCreateGuidance(savedInstanceState); 351 View guidanceView = mGuidanceStylist.onCreateView(inflater, guidanceContainer, guidance); 352 guidanceContainer.addView(guidanceView); 353 354 View actionsView = mActionsStylist.onCreateView(inflater, actionContainer); 355 actionContainer.addView(actionsView); 356 357 mAdapter = new GuidedActionAdapter(mActions, this, this, mActionsStylist); 358 359 mListView = mActionsStylist.getActionsGridView(); 360 mListView.setAdapter(mAdapter); 361 int pos = (mSelectedIndex >= 0 && mSelectedIndex < mActions.size()) ? 362 mSelectedIndex : getFirstCheckedAction(); 363 mListView.setSelectedPosition(pos); 364 365 return v; 366 } 367 368 /** 369 * {@inheritDoc} 370 */ 371 @Override 372 public void onSaveInstanceState(Bundle outState) { 373 super.onSaveInstanceState(outState); 374 outState.putInt(EXTRA_ACTION_SELECTED_INDEX, 375 (mListView != null) ? getSelectedActionPosition() : mSelectedIndex); 376 outState.putBoolean(EXTRA_ACTION_ENTRY_TRANSITION_ENABLED, mEntryTransitionEnabled); 377 outState.putBoolean(EXTRA_ENTRY_TRANSITION_PERFORMED, mEntryTransitionPerformed); 378 } 379 380 /** 381 * {@inheritDoc} 382 */ 383 @Override 384 public void onStart() { 385 if (DEBUG) Log.v(TAG, "onStart"); 386 super.onStart(); 387 if (isEntryTransitionEnabled() && !mEntryTransitionPerformed) { 388 mEntryTransitionPerformed = true; 389 performEntryTransition(); 390 } 391 } 392 393 /** 394 * {@inheritDoc} 395 */ 396 @Override 397 public Animator onCreateAnimator(int transit, boolean enter, int nextAnim) { 398 if (DEBUG) Log.v(TAG, "onCreateAnimator: " + transit + " " + enter + " " + nextAnim); 399 View mainView = getView(); 400 401 ArrayList<Animator> animators = new ArrayList<Animator>(); 402 switch (nextAnim) { 403 case ANIMATION_FRAGMENT_ENTER: 404 mGuidanceStylist.onFragmentEnter(animators); 405 mActionsStylist.onFragmentEnter(animators); 406 break; 407 case ANIMATION_FRAGMENT_EXIT: 408 mGuidanceStylist.onFragmentExit(animators); 409 mActionsStylist.onFragmentExit(animators); 410 break; 411 case ANIMATION_FRAGMENT_ENTER_POP: 412 mGuidanceStylist.onFragmentReenter(animators); 413 mActionsStylist.onFragmentReenter(animators); 414 break; 415 case ANIMATION_FRAGMENT_EXIT_POP: 416 mGuidanceStylist.onFragmentReturn(animators); 417 mActionsStylist.onFragmentReturn(animators); 418 break; 419 default: 420 return super.onCreateAnimator(transit, enter, nextAnim); 421 } 422 423 mEntryTransitionPerformed = true; 424 return createDummyAnimator(mainView, animators); 425 } 426 427 /** 428 * Returns whether entry transitions are enabled for this fragment. 429 * @return Whether entry transitions are enabled for this fragment. 430 */ 431 protected boolean isEntryTransitionEnabled() { 432 return mEntryTransitionEnabled; 433 } 434 435 /** 436 * Sets whether entry transitions are enabled for this fragment. 437 * @param enabled Whether to enable entry transitions for this fragment. 438 */ 439 protected void setEntryTransitionEnabled(boolean enabled) { 440 mEntryTransitionEnabled = enabled; 441 } 442 443 private boolean isGuidedStepTheme(Context context) { 444 int resId = R.attr.guidedStepThemeFlag; 445 TypedValue typedValue = new TypedValue(); 446 boolean found = context.getTheme().resolveAttribute(resId, typedValue, true); 447 if (DEBUG) Log.v(TAG, "Found guided step theme flag? " + found); 448 return found && typedValue.type == TypedValue.TYPE_INT_BOOLEAN && typedValue.data != 0; 449 } 450 451 private void resolveTheme() { 452 boolean hasThemeReference = true; 453 // Look up the guidedStepTheme in the currently specified theme. If it exists, 454 // replace the theme with its value. 455 Activity activity = getActivity(); 456 if (mTheme == -1 && !isGuidedStepTheme(activity)) { 457 // Look up the guidedStepTheme in the activity's currently specified theme. If it 458 // exists, replace the theme with its value. 459 int resId = R.attr.guidedStepTheme; 460 TypedValue typedValue = new TypedValue(); 461 boolean found = activity.getTheme().resolveAttribute(resId, typedValue, true); 462 if (DEBUG) Log.v(TAG, "Found guided step theme reference? " + found); 463 if (found) { 464 if (isGuidedStepTheme(new ContextThemeWrapper(activity, typedValue.resourceId))) { 465 mTheme = typedValue.resourceId; 466 } else { 467 found = false; 468 } 469 } 470 if (!found) { 471 Log.e(TAG, "GuidedStepFragment does not have an appropriate theme set."); 472 } 473 } 474 } 475 476 private LayoutInflater getThemeInflater(LayoutInflater inflater) { 477 if (mTheme == -1) { 478 return inflater; 479 } else { 480 Context ctw = new ContextThemeWrapper(getActivity(), mTheme); 481 return inflater.cloneInContext(ctw); 482 } 483 } 484 485 private int getFirstCheckedAction() { 486 for (int i = 0, size = mActions.size(); i < size; i++) { 487 if (mActions.get(i).isChecked()) { 488 return i; 489 } 490 } 491 return 0; 492 } 493 494 private void performEntryTransition() { 495 if (DEBUG) Log.v(TAG, "performEntryTransition"); 496 final View mainView = getView(); 497 498 mainView.setVisibility(View.INVISIBLE); 499 500 ArrayList<Animator> animators = new ArrayList<Animator>(); 501 mGuidanceStylist.onActivityEnter(animators); 502 mActionsStylist.onActivityEnter(animators); 503 504 final Animator animator = createDummyAnimator(mainView, animators); 505 506 // We need to defer the animation until the first layout has occurred, as we don't yet 507 // know the final locations of views. 508 mainView.getViewTreeObserver().addOnGlobalLayoutListener( 509 new ViewTreeObserver.OnGlobalLayoutListener() { 510 @Override 511 public void onGlobalLayout() { 512 mainView.getViewTreeObserver().removeOnGlobalLayoutListener(this); 513 if (!isAdded()) { 514 // We have been detached before this could run, 515 // so just bail 516 return; 517 } 518 519 mainView.setVisibility(View.VISIBLE); 520 animator.start(); 521 } 522 }); 523 } 524 525 private Animator createDummyAnimator(final View v, ArrayList<Animator> animators) { 526 final AnimatorSet animatorSet = new AnimatorSet(); 527 animatorSet.playTogether(animators); 528 return new UntargetableAnimatorSet(animatorSet); 529 } 530 531} 532