/* * 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.FragmentManager.BackStackEntry; import android.app.FragmentTransaction; import android.content.Context; import android.os.Build; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.RestrictTo; import android.support.v17.leanback.R; import android.support.v17.leanback.transition.TransitionHelper; 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.GuidedActionAdapter; import android.support.v17.leanback.widget.GuidedActionAdapterGroup; import android.support.v17.leanback.widget.GuidedActionsStylist; import android.support.v17.leanback.widget.ViewHolderTask; import android.support.v4.app.ActivityCompat; import android.support.v7.widget.RecyclerView; import android.util.Log; import android.util.TypedValue; import android.view.ContextThemeWrapper; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.LinearLayout; import java.util.ArrayList; import java.util.List; import static android.support.annotation.RestrictTo.Scope.GROUP_ID; /** * 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. *
*
* Clients of GuidedStepFragment must 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: *
* Clients use following helper functions to add GuidedStepFragment to Activity or FragmentManager: *
* 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. *
* 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: *
* If the theme is provided in multiple ways, the onProvideTheme override has priority, followed by * the Activity'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.) *
* 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. *
*
* 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.) *
* Note: Currently GuidedStepFragments grouped in this way must all be defined programmatically, * rather than in XML. This restriction may be removed in the future. * * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedStepTheme * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedStepBackground * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionContentWidthWeight * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionContentWidthWeightTwoPanels * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsBackground * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsBackgroundDark * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsElevation * @see GuidanceStylist * @see GuidanceStylist.Guidance * @see GuidedAction * @see GuidedActionsStylist */ public class GuidedStepFragment extends Fragment implements 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_PREFIX = "action_"; private static final String EXTRA_BUTTON_ACTION_PREFIX = "buttonaction_"; private static final String ENTRY_NAME_REPLACE = "GuidedStepDefault"; private static final String ENTRY_NAME_ENTRANCE = "GuidedStepEntrance"; private static final boolean IS_FRAMEWORK_FRAGMENT = true; /** * Fragment argument name for UI style. The argument value is persisted in fragment state and * used to select fragment transition. The value is initially {@link #UI_STYLE_ENTRANCE} and * might be changed in one of the three helper functions: *
* Argument value can be either: *
* 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.commit. */ public static int add(FragmentManager fragmentManager, GuidedStepFragment fragment) { return add(fragmentManager, fragment, android.R.id.content); } /** * Adds the specified GuidedStepFragment to the fragment stack, replacing any existing * GuidedStepFragments in the stack, and configuring the fragment-to-fragment custom * transitions. A backstack entry is added, so the fragment will be dismissed when BACK key * is pressed. *
* 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.
* @param id The id of container to add GuidedStepFragment, can be android.R.id.content.
* @return The ID returned by the call FragmentTransaction.commit.
*/
public static int add(FragmentManager fragmentManager, GuidedStepFragment fragment, int id) {
GuidedStepFragment current = getCurrentGuidedStepFragment(fragmentManager);
boolean inGuidedStep = current != null;
if (IS_FRAMEWORK_FRAGMENT && Build.VERSION.SDK_INT >= 21 && Build.VERSION.SDK_INT < 23
&& !inGuidedStep) {
// workaround b/22631964 for framework fragment
fragmentManager.beginTransaction()
.replace(id, new DummyFragment(), TAG_LEAN_BACK_ACTIONS_FRAGMENT)
.commit();
}
FragmentTransaction ft = fragmentManager.beginTransaction();
fragment.setUiStyle(inGuidedStep ? UI_STYLE_REPLACE : UI_STYLE_ENTRANCE);
ft.addToBackStack(fragment.generateStackEntryName());
if (current != null) {
fragment.onAddSharedElementTransition(ft, current);
}
return ft.replace(id, fragment, TAG_LEAN_BACK_ACTIONS_FRAGMENT).commit();
}
/**
* Called when this fragment is added to FragmentTransaction with {@link #UI_STYLE_REPLACE} (aka
* when the GuidedStepFragment replacing an existing GuidedStepFragment). Default implementation
* establishes connections between action background views to morph action background bounds
* change from disappearing GuidedStepFragment into this GuidedStepFragment. The default
* implementation heavily relies on {@link GuidedActionsStylist}'s layout, app may override this
* method when modifying the default layout of {@link GuidedActionsStylist}.
*
* @see GuidedActionsStylist
* @see #onProvideFragmentTransitions()
* @param ft The FragmentTransaction to add shared element.
* @param disappearing The disappearing fragment.
*/
protected void onAddSharedElementTransition(FragmentTransaction ft, GuidedStepFragment
disappearing) {
View fragmentView = disappearing.getView();
addNonNullSharedElementTransition(ft, fragmentView.findViewById(
R.id.action_fragment_root), "action_fragment_root");
addNonNullSharedElementTransition(ft, fragmentView.findViewById(
R.id.action_fragment_background), "action_fragment_background");
addNonNullSharedElementTransition(ft, fragmentView.findViewById(
R.id.action_fragment), "action_fragment");
addNonNullSharedElementTransition(ft, fragmentView.findViewById(
R.id.guidedactions_root), "guidedactions_root");
addNonNullSharedElementTransition(ft, fragmentView.findViewById(
R.id.guidedactions_content), "guidedactions_content");
addNonNullSharedElementTransition(ft, fragmentView.findViewById(
R.id.guidedactions_list_background), "guidedactions_list_background");
addNonNullSharedElementTransition(ft, fragmentView.findViewById(
R.id.guidedactions_root2), "guidedactions_root2");
addNonNullSharedElementTransition(ft, fragmentView.findViewById(
R.id.guidedactions_content2), "guidedactions_content2");
addNonNullSharedElementTransition(ft, fragmentView.findViewById(
R.id.guidedactions_list_background2), "guidedactions_list_background2");
}
private static void addNonNullSharedElementTransition (FragmentTransaction ft, View subView,
String transitionName)
{
if (subView != null)
TransitionHelper.addSharedElement(ft, subView, transitionName);
}
/**
* Returns BackStackEntry name for the GuidedStepFragment or empty String if no entry is
* associated. Note {@link #UI_STYLE_ACTIVITY_ROOT} will return empty String. The method
* returns undefined value if the fragment is not in FragmentManager.
* @return BackStackEntry name for the GuidedStepFragment or empty String if no entry is
* associated.
*/
String generateStackEntryName() {
return generateStackEntryName(getUiStyle(), getClass());
}
/**
* Generates BackStackEntry name for GuidedStepFragment class or empty String if no entry is
* associated. Note {@link #UI_STYLE_ACTIVITY_ROOT} is not allowed and returns empty String.
* @param uiStyle {@link #UI_STYLE_REPLACE} or {@link #UI_STYLE_ENTRANCE}
* @return BackStackEntry name for the GuidedStepFragment or empty String if no entry is
* associated.
*/
static String generateStackEntryName(int uiStyle, Class guidedStepFragmentClass) {
if (!GuidedStepFragment.class.isAssignableFrom(guidedStepFragmentClass)) {
return "";
}
switch (uiStyle) {
case UI_STYLE_REPLACE:
return ENTRY_NAME_REPLACE + guidedStepFragmentClass.getName();
case UI_STYLE_ENTRANCE:
return ENTRY_NAME_ENTRANCE + guidedStepFragmentClass.getName();
case UI_STYLE_ACTIVITY_ROOT:
default:
return "";
}
}
/**
* Returns true if the backstack entry represents GuidedStepFragment with
* {@link #UI_STYLE_ENTRANCE}, i.e. this is the first GuidedStepFragment pushed to stack; false
* otherwise.
* @see #generateStackEntryName(int, Class)
* @param backStackEntryName Name of BackStackEntry.
* @return True if the backstack represents GuidedStepFragment with {@link #UI_STYLE_ENTRANCE};
* false otherwise.
*/
static boolean isStackEntryUiStyleEntrance(String backStackEntryName) {
return backStackEntryName != null && backStackEntryName.startsWith(ENTRY_NAME_ENTRANCE);
}
/**
* Extract Class name from BackStackEntry name.
* @param backStackEntryName Name of BackStackEntry.
* @return Class name of GuidedStepFragment.
*/
static String getGuidedStepFragmentClassName(String backStackEntryName) {
if (backStackEntryName.startsWith(ENTRY_NAME_REPLACE)) {
return backStackEntryName.substring(ENTRY_NAME_REPLACE.length());
} else if (backStackEntryName.startsWith(ENTRY_NAME_ENTRANCE)) {
return backStackEntryName.substring(ENTRY_NAME_ENTRANCE.length());
} else {
return "";
}
}
/**
* Adds the specified GuidedStepFragment as content of Activity; no backstack entry is added so
* the activity will be dismissed when BACK key is pressed. The method is typically called in
* Activity.onCreate() when savedInstanceState is null. When savedInstanceState is not null,
* the Activity is being restored, do not call addAsRoot() to duplicate the Fragment restored
* by FragmentManager.
* {@link #UI_STYLE_ACTIVITY_ROOT} is assigned.
*
* Note: currently fragments added using this method must be created programmatically rather
* than via XML.
* @param activity The Activity to be used to insert GuidedstepFragment.
* @param fragment The GuidedStepFragment to be inserted into the fragment stack.
* @param id The id of container to add GuidedStepFragment, can be android.R.id.content.
* @return The ID returned by the call FragmentTransaction.commit, or -1 there is already
* GuidedStepFragment.
*/
public static int addAsRoot(Activity activity, GuidedStepFragment fragment, int id) {
// Workaround b/23764120: call getDecorView() to force requestFeature of ActivityTransition.
activity.getWindow().getDecorView();
FragmentManager fragmentManager = activity.getFragmentManager();
if (fragmentManager.findFragmentByTag(TAG_LEAN_BACK_ACTIONS_FRAGMENT) != null) {
Log.w(TAG, "Fragment is already exists, likely calling " +
"addAsRoot() when savedInstanceState is not null in Activity.onCreate().");
return -1;
}
FragmentTransaction ft = fragmentManager.beginTransaction();
fragment.setUiStyle(UI_STYLE_ACTIVITY_ROOT);
return ft.replace(id, fragment, 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 button GuidedActions that the user may take in this fragment.
* @return The list of button GuidedActions for this fragment.
*/
public List
* The default implementation heavily relies on {@link GuidedActionsStylist} and
* {@link GuidanceStylist} layout, app may override this method when modifying the default
* layout of {@link GuidedActionsStylist} or {@link GuidanceStylist}.
*
* TIP: because the fragment view is removed during fragment transition, in general app cannot
* use two Visibility transition together. Workaround is to create your own Visibility
* transition that controls multiple animators (e.g. slide and fade animation in one Transition
* class).
*/
protected void onProvideFragmentTransitions() {
if (Build.VERSION.SDK_INT >= 21) {
final int uiStyle = getUiStyle();
if (uiStyle == UI_STYLE_REPLACE) {
Object enterTransition = TransitionHelper.createFadeAndShortSlide(Gravity.END);
TransitionHelper.exclude(enterTransition, R.id.guidedstep_background, true);
TransitionHelper.exclude(enterTransition, R.id.guidedactions_sub_list_background,
true);
TransitionHelper.setEnterTransition(this, enterTransition);
Object fade = TransitionHelper.createFadeTransition(TransitionHelper.FADE_IN |
TransitionHelper.FADE_OUT);
TransitionHelper.include(fade, R.id.guidedactions_sub_list_background);
Object changeBounds = TransitionHelper.createChangeBounds(false);
Object sharedElementTransition = TransitionHelper.createTransitionSet(false);
TransitionHelper.addTransition(sharedElementTransition, fade);
TransitionHelper.addTransition(sharedElementTransition, changeBounds);
TransitionHelper.setSharedElementEnterTransition(this, sharedElementTransition);
} else if (uiStyle == UI_STYLE_ENTRANCE) {
if (entranceTransitionType == SLIDE_FROM_SIDE) {
Object fade = TransitionHelper.createFadeTransition(TransitionHelper.FADE_IN |
TransitionHelper.FADE_OUT);
TransitionHelper.include(fade, R.id.guidedstep_background);
Object slideFromSide = TransitionHelper.createFadeAndShortSlide(Gravity.END |
Gravity.START);
TransitionHelper.include(slideFromSide, R.id.content_fragment);
TransitionHelper.include(slideFromSide, R.id.action_fragment_root);
Object enterTransition = TransitionHelper.createTransitionSet(false);
TransitionHelper.addTransition(enterTransition, fade);
TransitionHelper.addTransition(enterTransition, slideFromSide);
TransitionHelper.setEnterTransition(this, enterTransition);
} else {
Object slideFromBottom = TransitionHelper.createFadeAndShortSlide(
Gravity.BOTTOM);
TransitionHelper.include(slideFromBottom, R.id.guidedstep_background_view_root);
Object enterTransition = TransitionHelper.createTransitionSet(false);
TransitionHelper.addTransition(enterTransition, slideFromBottom);
TransitionHelper.setEnterTransition(this, enterTransition);
}
// No shared element transition
TransitionHelper.setSharedElementEnterTransition(this, null);
} else if (uiStyle == UI_STYLE_ACTIVITY_ROOT) {
// for Activity root, we don't need enter transition, use activity transition
TransitionHelper.setEnterTransition(this, null);
// No shared element transition
TransitionHelper.setSharedElementEnterTransition(this, null);
}
// exitTransition is same for all style
Object exitTransition = TransitionHelper.createFadeAndShortSlide(Gravity.START);
TransitionHelper.exclude(exitTransition, R.id.guidedstep_background, true);
TransitionHelper.exclude(exitTransition, R.id.guidedactions_sub_list_background,
true);
TransitionHelper.setExitTransition(this, exitTransition);
}
}
/**
* Called by onCreateView to inflate background view. Default implementation loads view
* from {@link R.layout#lb_guidedstep_background} which holds a reference to
* guidedStepBackground.
* @param inflater LayoutInflater to load background view.
* @param container Parent view of background view.
* @param savedInstanceState
* @return Created background view or null if no background.
*/
public View onCreateBackgroundView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.lb_guidedstep_background, container, false);
}
/**
* Set UI style to fragment arguments. Default value is {@link #UI_STYLE_ENTRANCE} when fragment
* is first initialized. UI style is used to choose different fragment transition animations and
* determine if this is the first GuidedStepFragment on backstack. In most cases app does not
* directly call this method, app calls helper function
* {@link #add(FragmentManager, GuidedStepFragment, int)}. However if the app creates Fragment
* transaction and controls backstack by itself, it would need call setUiStyle() to select the
* fragment transition to use.
*
* @param style {@link #UI_STYLE_ACTIVITY_ROOT} {@link #UI_STYLE_REPLACE} or
* {@link #UI_STYLE_ENTRANCE}.
*/
public void setUiStyle(int style) {
int oldStyle = getUiStyle();
Bundle arguments = getArguments();
boolean isNew = false;
if (arguments == null) {
arguments = new Bundle();
isNew = true;
}
arguments.putInt(EXTRA_UI_STYLE, style);
// call setArgument() will validate if the fragment is already added.
if (isNew) {
setArguments(arguments);
}
if (style != oldStyle) {
onProvideFragmentTransitions();
}
}
/**
* Read UI style from fragment arguments. Default value is {@link #UI_STYLE_ENTRANCE} when
* fragment is first initialized. UI style is used to choose different fragment transition
* animations and determine if this is the first GuidedStepFragment on backstack.
*
* @return {@link #UI_STYLE_ACTIVITY_ROOT} {@link #UI_STYLE_REPLACE} or
* {@link #UI_STYLE_ENTRANCE}.
* @see #onProvideFragmentTransitions()
*/
public int getUiStyle() {
Bundle b = getArguments();
if (b == null) return UI_STYLE_ENTRANCE;
return b.getInt(EXTRA_UI_STYLE, UI_STYLE_ENTRANCE);
}
/**
* {@inheritDoc}
*/
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (DEBUG) Log.v(TAG, "onCreate");
// Set correct transition from saved arguments.
onProvideFragmentTransitions();
Bundle state = (savedInstanceState != null) ? savedInstanceState : getArguments();
if (state != null) {
if (mSelectedIndex == -1) {
mSelectedIndex = state.getInt(EXTRA_ACTION_SELECTED_INDEX, -1);
}
}
ArrayList
*
*