/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package android.support.v17.leanback.app; import android.animation.Animator; import android.animation.AnimatorSet; import android.app.Activity; import android.app.Fragment; import android.app.FragmentManager; import android.app.FragmentTransaction; import android.content.Context; import android.content.res.TypedArray; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v17.leanback.animation.UntargetableAnimatorSet; import android.support.v17.leanback.R; import android.support.v17.leanback.widget.GuidanceStylist; import android.support.v17.leanback.widget.GuidanceStylist.Guidance; import android.support.v17.leanback.widget.GuidedAction; import android.support.v17.leanback.widget.GuidedActionsStylist; import android.support.v17.leanback.widget.VerticalGridView; import android.util.AttributeSet; import android.util.Log; import android.util.TypedValue; import android.view.ContextThemeWrapper; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.widget.ImageView; import android.widget.RelativeLayout; import android.widget.TextView; import java.util.ArrayList; import java.util.List; /** * A GuidedStepFragment is used to guide the user through a decision or series of decisions. * It is composed of a guidance view on the left and a view on the right containing a list of * possible actions. * <p> * <h3>Basic Usage</h3> * <p> * Clients of GuidedStepFragment typically create a custom subclass to attach to their Activities. * This custom subclass provides the information necessary to construct the user interface and * respond to user actions. At a minimum, subclasses should override: * <ul> * <li>{@link #onCreateGuidance}, to provide instructions to the user</li> * <li>{@link #onCreateActions}, to provide a set of {@link GuidedAction}s the user can take</li> * <li>{@link #onGuidedActionClicked}, to respond to those actions</li> * </ul> * <p> * <h3>Theming and Stylists</h3> * <p> * GuidedStepFragment delegates its visual styling to classes called stylists. The {@link * GuidanceStylist} is responsible for the left guidance view, while the {@link * GuidedActionsStylist} is responsible for the right actions view. The stylists use theme * attributes to derive values associated with the presentation, such as colors, animations, etc. * Most simple visual aspects of GuidanceStylist and GuidedActionsStylist can be customized * via theming; see their documentation for more information. * <p> * GuidedStepFragments must have access to an appropriate theme in order for the stylists to * function properly. Specifically, the fragment must receive {@link * android.support.v17.leanback.R.style#Theme_Leanback_GuidedStep}, or a theme whose parent is * is set to that theme. Themes can be provided in one of three ways: * <ul> * <li>The simplest way is to set the theme for the host Activity to the GuidedStep theme or a * theme that derives from it.</li> * <li>If the Activity already has a theme and setting its parent theme is inconvenient, the * existing Activity theme can have an entry added for the attribute {@link * android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedStepTheme}. If present, * this theme will be used by GuidedStepFragment as an overlay to the Activity's theme.</li> * <li>Finally, custom subclasses of GuidedStepFragment may provide a theme through the {@link * #onProvideTheme} method. This can be useful if a subclass is used across multiple * Activities.</li> * </ul> * <p> * If the theme is provided in multiple ways, the onProvideTheme override has priority, followed by * the Activty's theme. (Themes whose parent theme is already set to the guided step theme do not * need to set the guidedStepTheme attribute; if set, it will be ignored.) * <p> * If themes do not provide enough customizability, the stylists themselves may be subclassed and * provided to the GuidedStepFragment through the {@link #onCreateGuidanceStylist} and {@link * #onCreateActionsStylist} methods. The stylists have simple hooks so that subclasses * may override layout files; subclasses may also have more complex logic to determine styling. * <p> * <h3>Guided sequences</h3> * <p> * GuidedStepFragments can be grouped together to provide a guided sequence. GuidedStepFragments * grouped as a sequence use custom animations provided by {@link GuidanceStylist} and * {@link GuidedActionsStylist} (or subclasses) during transitions between steps. Clients * should use {@link #add} to place subsequent GuidedFragments onto the fragment stack so that * custom animations are properly configured. (Custom animations are triggered automatically when * the fragment stack is subsequently popped by any normal mechanism.) * <p> * <i>Note: Currently GuidedStepFragments grouped in this way must all be defined programmatically, * rather than in XML. This restriction may be removed in the future.</i> * <p> * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedStepTheme * @see GuidanceStylist * @see GuidanceStylist.Guidance * @see GuidedAction * @see GuidedActionsStylist */ public class GuidedStepFragment extends Fragment implements GuidedActionAdapter.ClickListener, GuidedActionAdapter.FocusListener { private static final String TAG_LEAN_BACK_ACTIONS_FRAGMENT = "leanBackGuidedStepFragment"; private static final String EXTRA_ACTION_SELECTED_INDEX = "selectedIndex"; private static final String EXTRA_ACTION_ENTRY_TRANSITION_ENABLED = "entryTransitionEnabled"; private static final String EXTRA_ENTRY_TRANSITION_PERFORMED = "entryTransitionPerformed"; private static final String TAG = "GuidedStepFragment"; private static final boolean DEBUG = true; private static final int ANIMATION_FRAGMENT_ENTER = 1; private static final int ANIMATION_FRAGMENT_EXIT = 2; private static final int ANIMATION_FRAGMENT_ENTER_POP = 3; private static final int ANIMATION_FRAGMENT_EXIT_POP = 4; private int mTheme; private GuidanceStylist mGuidanceStylist; private GuidedActionsStylist mActionsStylist; private GuidedActionAdapter mAdapter; private VerticalGridView mListView; private List<GuidedAction> mActions = new ArrayList<GuidedAction>(); private int mSelectedIndex = -1; private boolean mEntryTransitionPerformed; private boolean mEntryTransitionEnabled = true; public GuidedStepFragment() { // We need to supply the theme before any potential call to onInflate in order // for the defaulting to work properly. mTheme = onProvideTheme(); mGuidanceStylist = onCreateGuidanceStylist(); mActionsStylist = onCreateActionsStylist(); } /** * Creates the presenter used to style the guidance panel. The default implementation returns * a basic GuidanceStylist. * @return The GuidanceStylist used in this fragment. */ public GuidanceStylist onCreateGuidanceStylist() { return new GuidanceStylist(); } /** * Creates the presenter used to style the guided actions panel. The default implementation * returns a basic GuidedActionsStylist. * @return The GuidedActionsStylist used in this fragment. */ public GuidedActionsStylist onCreateActionsStylist() { return new GuidedActionsStylist(); } /** * Returns the theme used for styling the fragment. The default returns -1, indicating that the * host Activity's theme should be used. * @return The theme resource ID of the theme to use in this fragment, or -1 to use the * host Activity's theme. */ public int onProvideTheme() { return -1; } /** * Returns the information required to provide guidance to the user. This hook is called during * {@link #onCreateView}. May be overridden to return a custom subclass of {@link * GuidanceStylist.Guidance} for use in a subclass of {@link GuidanceStylist}. The default * returns a Guidance object with empty fields; subclasses should override. * @param savedInstanceState The saved instance state from onCreateView. * @return The Guidance object representing the information used to guide the user. */ public @NonNull Guidance onCreateGuidance(Bundle savedInstanceState) { return new Guidance("", "", "", null); } /** * Fills out the set of actions available to the user. This hook is called during {@link * #onCreate}. The default leaves the list of actions empty; subclasses should override. * @param actions A non-null, empty list ready to be populated. * @param savedInstanceState The saved instance state from onCreate. */ public void onCreateActions(@NonNull List<GuidedAction> actions, Bundle savedInstanceState) { } /** * Callback invoked when an action is taken by the user. Subclasses should override in * order to act on the user's decisions. * @param action The chosen action. */ @Override public void onGuidedActionClicked(GuidedAction action) { } /** * Callback invoked when an action is focused (made to be the current selection) by the user. */ @Override public void onGuidedActionFocused(GuidedAction action) { } /** * Adds the specified GuidedStepFragment to the fragment stack, replacing any existing * GuidedStepFragments in the stack, and configuring the fragment-to-fragment custom animations. * <p> * Note: currently fragments added using this method must be created programmatically rather * than via XML. * @param fragmentManager The FragmentManager to be used in the transaction. * @param fragment The GuidedStepFragment to be inserted into the fragment stack. * @return The ID returned by the call FragmentTransaction.replace. */ public static int add(FragmentManager fragmentManager, GuidedStepFragment fragment) { return add(fragmentManager, fragment, android.R.id.content); } // Note, this method used to be public, but I haven't found a good way for a client // to specify an id. private static int add(FragmentManager fm, GuidedStepFragment f, int id) { boolean inGuidedStep = getCurrentGuidedStepFragment(fm) != null; FragmentTransaction ft = fm.beginTransaction(); if (inGuidedStep) { ft.setCustomAnimations(ANIMATION_FRAGMENT_ENTER, ANIMATION_FRAGMENT_EXIT, ANIMATION_FRAGMENT_ENTER_POP, ANIMATION_FRAGMENT_EXIT_POP); ft.addToBackStack(null); } return ft.replace(id, f, TAG_LEAN_BACK_ACTIONS_FRAGMENT).commit(); } /** * Returns the current GuidedStepFragment on the fragment transaction stack. * @return The current GuidedStepFragment, if any, on the fragment transaction stack. */ public static GuidedStepFragment getCurrentGuidedStepFragment(FragmentManager fm) { Fragment f = fm.findFragmentByTag(TAG_LEAN_BACK_ACTIONS_FRAGMENT); if (f instanceof GuidedStepFragment) { return (GuidedStepFragment) f; } return null; } /** * Returns the GuidanceStylist that displays guidance information for the user. * @return The GuidanceStylist for this fragment. */ public GuidanceStylist getGuidanceStylist() { return mGuidanceStylist; } /** * Returns the GuidedActionsStylist that displays the actions the user may take. * @return The GuidedActionsStylist for this fragment. */ public GuidedActionsStylist getGuidedActionsStylist() { return mActionsStylist; } /** * Returns the list of GuidedActions that the user may take in this fragment. * @return The list of GuidedActions for this fragment. */ public List<GuidedAction> getActions() { return mActions; } /** * Sets the list of GuidedActions that the user may take in this fragment. * @param actions The list of GuidedActions for this fragment. */ public void setActions(List<GuidedAction> actions) { mActions = actions; if (mAdapter != null) { mAdapter.setActions(mActions); } } /** * Returns the view corresponding to the action at the indicated position in the list of * actions for this fragment. * @param position The integer position of the action of interest. * @return The View corresponding to the action at the indicated position, or null if that * action is not currently onscreen. */ public View getActionItemView(int position) { return mListView.findViewHolderForPosition(position).itemView; } /** * Scrolls the action list to the position indicated, selecting that action's view. * @param position The integer position of the action of interest. */ public void setSelectedActionPosition(int position) { mListView.setSelectedPosition(position); } /** * Returns the position if the currently selected GuidedAction. * @return position The integer position of the currently selected action. */ public int getSelectedActionPosition() { return mListView.getSelectedPosition(); } /** * {@inheritDoc} */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (DEBUG) Log.v(TAG, "onCreate"); Bundle state = (savedInstanceState != null) ? savedInstanceState : getArguments(); if (state != null) { if (mSelectedIndex == -1) { mSelectedIndex = state.getInt(EXTRA_ACTION_SELECTED_INDEX, -1); } mEntryTransitionEnabled = state.getBoolean(EXTRA_ACTION_ENTRY_TRANSITION_ENABLED, true); mEntryTransitionPerformed = state.getBoolean(EXTRA_ENTRY_TRANSITION_PERFORMED, false); } mActions.clear(); onCreateActions(mActions, savedInstanceState); } /** * {@inheritDoc} */ @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { if (DEBUG) Log.v(TAG, "onCreateView"); resolveTheme(); inflater = getThemeInflater(inflater); View v = inflater.inflate(R.layout.lb_guidedstep_fragment, container, false); ViewGroup guidanceContainer = (ViewGroup) v.findViewById(R.id.content_fragment); ViewGroup actionContainer = (ViewGroup) v.findViewById(R.id.action_fragment); Guidance guidance = onCreateGuidance(savedInstanceState); View guidanceView = mGuidanceStylist.onCreateView(inflater, guidanceContainer, guidance); guidanceContainer.addView(guidanceView); View actionsView = mActionsStylist.onCreateView(inflater, actionContainer); actionContainer.addView(actionsView); mAdapter = new GuidedActionAdapter(mActions, this, this, mActionsStylist); mListView = mActionsStylist.getActionsGridView(); mListView.setAdapter(mAdapter); int pos = (mSelectedIndex >= 0 && mSelectedIndex < mActions.size()) ? mSelectedIndex : getFirstCheckedAction(); mListView.setSelectedPosition(pos); return v; } /** * {@inheritDoc} */ @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putInt(EXTRA_ACTION_SELECTED_INDEX, (mListView != null) ? getSelectedActionPosition() : mSelectedIndex); outState.putBoolean(EXTRA_ACTION_ENTRY_TRANSITION_ENABLED, mEntryTransitionEnabled); outState.putBoolean(EXTRA_ENTRY_TRANSITION_PERFORMED, mEntryTransitionPerformed); } /** * {@inheritDoc} */ @Override public void onStart() { if (DEBUG) Log.v(TAG, "onStart"); super.onStart(); if (isEntryTransitionEnabled() && !mEntryTransitionPerformed) { mEntryTransitionPerformed = true; performEntryTransition(); } } /** * {@inheritDoc} */ @Override public Animator onCreateAnimator(int transit, boolean enter, int nextAnim) { if (DEBUG) Log.v(TAG, "onCreateAnimator: " + transit + " " + enter + " " + nextAnim); View mainView = getView(); ArrayList<Animator> animators = new ArrayList<Animator>(); switch (nextAnim) { case ANIMATION_FRAGMENT_ENTER: mGuidanceStylist.onFragmentEnter(animators); mActionsStylist.onFragmentEnter(animators); break; case ANIMATION_FRAGMENT_EXIT: mGuidanceStylist.onFragmentExit(animators); mActionsStylist.onFragmentExit(animators); break; case ANIMATION_FRAGMENT_ENTER_POP: mGuidanceStylist.onFragmentReenter(animators); mActionsStylist.onFragmentReenter(animators); break; case ANIMATION_FRAGMENT_EXIT_POP: mGuidanceStylist.onFragmentReturn(animators); mActionsStylist.onFragmentReturn(animators); break; default: return super.onCreateAnimator(transit, enter, nextAnim); } mEntryTransitionPerformed = true; return createDummyAnimator(mainView, animators); } /** * Returns whether entry transitions are enabled for this fragment. * @return Whether entry transitions are enabled for this fragment. */ protected boolean isEntryTransitionEnabled() { return mEntryTransitionEnabled; } /** * Sets whether entry transitions are enabled for this fragment. * @param enabled Whether to enable entry transitions for this fragment. */ protected void setEntryTransitionEnabled(boolean enabled) { mEntryTransitionEnabled = enabled; } private boolean isGuidedStepTheme(Context context) { int resId = R.attr.guidedStepThemeFlag; TypedValue typedValue = new TypedValue(); boolean found = context.getTheme().resolveAttribute(resId, typedValue, true); if (DEBUG) Log.v(TAG, "Found guided step theme flag? " + found); return found && typedValue.type == TypedValue.TYPE_INT_BOOLEAN && typedValue.data != 0; } private void resolveTheme() { boolean hasThemeReference = true; // Look up the guidedStepTheme in the currently specified theme. If it exists, // replace the theme with its value. Activity activity = getActivity(); if (mTheme == -1 && !isGuidedStepTheme(activity)) { // Look up the guidedStepTheme in the activity's currently specified theme. If it // exists, replace the theme with its value. int resId = R.attr.guidedStepTheme; TypedValue typedValue = new TypedValue(); boolean found = activity.getTheme().resolveAttribute(resId, typedValue, true); if (DEBUG) Log.v(TAG, "Found guided step theme reference? " + found); if (found) { if (isGuidedStepTheme(new ContextThemeWrapper(activity, typedValue.resourceId))) { mTheme = typedValue.resourceId; } else { found = false; } } if (!found) { Log.e(TAG, "GuidedStepFragment does not have an appropriate theme set."); } } } private LayoutInflater getThemeInflater(LayoutInflater inflater) { if (mTheme == -1) { return inflater; } else { Context ctw = new ContextThemeWrapper(getActivity(), mTheme); return inflater.cloneInContext(ctw); } } private int getFirstCheckedAction() { for (int i = 0, size = mActions.size(); i < size; i++) { if (mActions.get(i).isChecked()) { return i; } } return 0; } private void performEntryTransition() { if (DEBUG) Log.v(TAG, "performEntryTransition"); final View mainView = getView(); mainView.setVisibility(View.INVISIBLE); ArrayList<Animator> animators = new ArrayList<Animator>(); mGuidanceStylist.onActivityEnter(animators); mActionsStylist.onActivityEnter(animators); final Animator animator = createDummyAnimator(mainView, animators); // We need to defer the animation until the first layout has occurred, as we don't yet // know the final locations of views. mainView.getViewTreeObserver().addOnGlobalLayoutListener( new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { mainView.getViewTreeObserver().removeOnGlobalLayoutListener(this); if (!isAdded()) { // We have been detached before this could run, // so just bail return; } mainView.setVisibility(View.VISIBLE); animator.start(); } }); } private Animator createDummyAnimator(final View v, ArrayList<Animator> animators) { final AnimatorSet animatorSet = new AnimatorSet(); animatorSet.playTogether(animators); return new UntargetableAnimatorSet(animatorSet); } }