blob: 89a1a13a6e1bc4ce1633655d43166c618fc81550 [file] [log] [blame]
/*
* 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.content.res.TypedArray;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v17.leanback.transition.TransitionHelper;
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.support.v4.app.ActivityCompat;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
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.view.ViewTreeObserver;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.LinearLayout;
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 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:
* <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>
* Clients use following helper functions to add GuidedStepFragment to Activity or FragmentManager:
* <ul>
* <li>{@link #addAsRoot(Activity, GuidedStepFragment, int)}, to be called during Activity onCreate,
* adds GuidedStepFragment as the first Fragment in activity.</li>
* <li>{@link #add(FragmentManager, GuidedStepFragment)} or {@link #add(FragmentManager,
* GuidedStepFragment, int)}, to add GuidedStepFragment on top of existing Fragments or
* replacing existing GuidedStepFragment when moving forward to next step.</li>
* </ul>
* <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>
*
* @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_guidedButtonActionsBackground
* @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.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 ENTRY_NAME_DEFAULT = "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.
* The value is initially {@link #UI_STYLE_DEFAULT} and might be changed in one of the three
* helper functions:
* <ul>
* <li>{@link #addAsRoot(Activity, GuidedStepFragment, int)}</li>
* <li>{@link #add(FragmentManager, GuidedStepFragment)} or {@link #add(FragmentManager,
* GuidedStepFragment, int)}</li>
* </ul>
* <p>
* Argument value can be either:
* <ul>
* <li>{@link #UI_STYLE_DEFAULT}</li>
* <li>{@link #UI_STYLE_ENTRANCE}</li>
* <li>{@link #UI_STYLE_ACTIVITY_ROOT}</li>
* </ul>
*/
public static final String EXTRA_UI_STYLE = "uiStyle";
/**
* Default value for argument {@link #EXTRA_UI_STYLE}. The default value is assigned
* in GuidedStepFragment constructor. This is the case that we use GuidedStepFragment to
* replace another existing GuidedStepFragment when moving forward to next step. Default
* behavior of this style is:
* <ul>
* <li> Enter transition slides in from END(right), exit transition slide out to START(left).
* </li>
* <li> No background, see {@link #onProvideBackgroundFragment()}.</li>
* </ul>
*/
public static final int UI_STYLE_DEFAULT = 0;
/**
* One possible value of argument {@link #EXTRA_UI_STYLE}. This is the case that we show
* GuidedStepFragment on top of other content. The default behavior of this style:
* <ul>
* <li>Enter transition slides in from two sides, exit transition is inherited from
* {@link #UI_STYLE_DEFAULT}. Note: Changing exit transition by UI style is not working because
* fragment transition asks for exit transition before UI style is restored in Fragment
* .onCreate().</li>
* <li> {@link #onProvideBackgroundFragment()} will create {@link GuidedStepBackgroundFragment}
* to covering underneath content. The activity must provide a container to host background
* fragment and override {@link #getContainerIdForBackground()}</li>
* </ul>
*/
public static final int UI_STYLE_ENTRANCE = 1;
/**
* One possible value of argument {@link #EXTRA_UI_STYLE}. This is the case that we show first
* GuidedStepFragment in a separate activity. The default behavior of this style:
* <ul>
* <li> Enter transition is assigned null (will rely on activity transition), exit transition is
* same as {@link #UI_STYLE_DEFAULT}. Note: Changing exit transition by UI style is not working
* because fragment transition asks for exit transition before UI style is restored in
* Fragment.onCreate().</li>
* <li> No background, see {@link #onProvideBackgroundFragment()}.
* </ul>
*/
public static final int UI_STYLE_ACTIVITY_ROOT = 2;
private static final String TAG = "GuidedStepFragment";
private static final boolean DEBUG = false;
private int mTheme;
private ContextThemeWrapper mThemeWrapper;
private GuidanceStylist mGuidanceStylist;
private GuidedActionsStylist mActionsStylist;
private GuidedActionsStylist mButtonActionsStylist;
private GuidedActionAdapter mAdapter;
private GuidedActionAdapter mButtonAdapter;
private List<GuidedAction> mActions = new ArrayList<GuidedAction>();
private List<GuidedAction> mButtonActions = new ArrayList<GuidedAction>();
private int mSelectedIndex = -1;
private int mButtonSelectedIndex = -1;
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();
mButtonActionsStylist = onCreateButtonActionsStylist();
onProvideFragmentTransitions();
}
/**
* 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();
}
/**
* Creates the presenter used to style a sided actions panel for button only.
* The default implementation returns a basic GuidedActionsStylist.
* @return The GuidedActionsStylist used in this fragment.
*/
public GuidedActionsStylist onCreateButtonActionsStylist() {
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) {
}
/**
* Fills out the set of actions shown at right available to the user. This hook is called during
* {@link #onCreate}. The default leaves the list of actions empty; subclasses may override.
* @param actions A non-null, empty list ready to be populated.
* @param savedInstanceState The saved instance state from onCreate.
*/
public void onCreateButtonActions(@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) {
}
/**
* Callback invoked when an action's title or description has been edited.
* Override {@link #onGuidedActionEditedAndProceed(GuidedAction)} instead of app wants to
* control the next action to focus on.
*/
public void onGuidedActionEdited(GuidedAction action) {
}
/**
* Callback invoked when an action's title or description has been edited. Default
* implementation calls {@link #onGuidedActionEdited(GuidedAction)} and returns
* {@link GuidedAction#ACTION_ID_NEXT}.
*
* @param action The action that has been edited.
* @return ID of the action will be focused or {@link GuidedAction#ACTION_ID_NEXT},
* {@link GuidedAction#ACTION_ID_CURRENT}.
*/
public long onGuidedActionEditedAndProceed(GuidedAction action) {
onGuidedActionEdited(action);
return GuidedAction.ACTION_ID_NEXT;
}
/**
* 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.
* <li>If current fragment on stack is GuidedStepFragment: assign {@link #UI_STYLE_DEFAULT}
* <li>If current fragment on stack is not GuidedStepFragment: assign {@link #UI_STYLE_ENTRANCE}
* <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);
}
/**
* 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.
* <li>If current fragment on stack is GuidedStepFragment: assign {@link #UI_STYLE_DEFAULT}
* <li>If current fragment on stack is not GuidedStepFragment: assign {@link #UI_STYLE_ENTRANCE}
* <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.
* @param id The id of container to add GuidedStepFragment, can be android.R.id.content.
* @return The ID returned by the call FragmentTransaction.replace.
*/
public static int add(FragmentManager fragmentManager, GuidedStepFragment fragment, int id) {
boolean inGuidedStep = getCurrentGuidedStepFragment(fragmentManager) != null;
if (IS_FRAMEWORK_FRAGMENT && Build.VERSION.SDK_INT >= 21 && Build.VERSION.SDK_INT < 23
&& !inGuidedStep && fragment.getContainerIdForBackground() != View.NO_ID) {
// workaround b/22631964 for framework fragment
fragmentManager.beginTransaction()
.replace(id, new DummyFragment(), TAG_LEAN_BACK_ACTIONS_FRAGMENT)
.replace(fragment.getContainerIdForBackground(), new DummyFragment())
.commit();
}
FragmentTransaction ft = fragmentManager.beginTransaction();
fragment.setUiStyle(inGuidedStep ? UI_STYLE_DEFAULT : UI_STYLE_ENTRANCE);
ft.addToBackStack(fragment.generateStackEntryName());
initialBackground(fragment, id, ft);
return ft.replace(id, fragment, TAG_LEAN_BACK_ACTIONS_FRAGMENT).commit();
}
/**
* 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.
*/
public 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_DEFAULT} or {@link #UI_STYLE_ENTRANCE}
* @return BackStackEntry name for the GuidedStepFragment or empty String if no entry is
* associated.
*/
public static String generateStackEntryName(int uiStyle, Class guidedStepFragmentClass) {
if (!GuidedStepFragment.class.isAssignableFrom(guidedStepFragmentClass)) {
return "";
}
switch (uiStyle) {
case UI_STYLE_DEFAULT:
return ENTRY_NAME_DEFAULT + guidedStepFragmentClass.getName();
case UI_STYLE_ENTRANCE:
return ENTRY_NAME_ENTRANCE + guidedStepFragmentClass.getName();
case UI_STYLE_ACTIVITY_ROOT:
default:
return "";
}
}
/**
* Returns true if the backstack represents GuidedStepFragment with {@link #UI_STYLE_ENTRANCE};
* false otherwise.
* @param backStackEntryName Name of BackStackEntry.
* @return True if the backstack represents GuidedStepFragment with {@link #UI_STYLE_ENTRANCE};
* false otherwise.
*/
public static boolean isUiStyleEntrance(String backStackEntryName) {
return backStackEntryName != null && backStackEntryName.startsWith(ENTRY_NAME_ENTRANCE);
}
/**
* Returns true if the backstack represents GuidedStepFragment with {@link #UI_STYLE_DEFAULT};
* false otherwise.
* @param backStackEntryName Name of BackStackEntry.
* @return True if the backstack represents GuidedStepFragment with {@link #UI_STYLE_DEFAULT};
* false otherwise.
*/
public static boolean isUiStyleDefault(String backStackEntryName) {
return backStackEntryName != null && backStackEntryName.startsWith(ENTRY_NAME_DEFAULT);
}
/**
* Extract Class name from BackStackEntry name.
* @param backStackEntryName Name of BackStackEntry.
* @return Class name of GuidedStepFragment.
*/
public static String getGuidedStepFragmentClassName(String backStackEntryName) {
if (backStackEntryName.startsWith(ENTRY_NAME_DEFAULT)) {
return backStackEntryName.substring(ENTRY_NAME_DEFAULT.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.
* {@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.replace.
*/
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();
FragmentTransaction ft = fragmentManager.beginTransaction();
fragment.setUiStyle(UI_STYLE_ACTIVITY_ROOT);
initialBackground(fragment, id, ft);
return ft.replace(id, fragment, TAG_LEAN_BACK_ACTIONS_FRAGMENT).commit();
}
static void initialBackground(GuidedStepFragment fragment, int id, FragmentTransaction ft) {
if (fragment.getContainerIdForBackground() != View.NO_ID) {
Fragment backgroundFragment = fragment.onProvideBackgroundFragment();
if (backgroundFragment != null) {
ft.replace(fragment.getContainerIdForBackground(), backgroundFragment);
}
}
}
/**
* 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;
}
/**
* @hide
*/
public static class DummyFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
final View v = new View(inflater.getContext());
v.setVisibility(View.GONE);
return v;
}
}
/**
* 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<GuidedAction> getButtonActions() {
return mButtonActions;
}
/**
* Find button GuidedAction by Id.
* @param id Id of the button action to search.
* @return GuidedAction object or null if not found.
*/
public GuidedAction findButtonActionById(long id) {
int index = findButtonActionPositionById(id);
return index >= 0 ? mButtonActions.get(index) : null;
}
/**
* Find button GuidedAction position in array by Id.
* @param id Id of the button action to search.
* @return position of GuidedAction object in array or -1 if not found.
*/
public int findButtonActionPositionById(long id) {
if (mButtonActions != null) {
for (int i = 0; i < mButtonActions.size(); i++) {
GuidedAction action = mButtonActions.get(i);
if (mButtonActions.get(i).getId() == id) {
return i;
}
}
}
return -1;
}
/**
* Returns the GuidedActionsStylist that displays the button actions the user may take.
* @return The GuidedActionsStylist for this fragment.
*/
public GuidedActionsStylist getGuidedButtonActionsStylist() {
return mButtonActionsStylist;
}
/**
* Sets the list of button GuidedActions that the user may take in this fragment.
* @param actions The list of button GuidedActions for this fragment.
*/
public void setButtonActions(List<GuidedAction> actions) {
mButtonActions = actions;
if (mButtonAdapter != null) {
mButtonAdapter.setActions(mButtonActions);
}
}
/**
* Notify an button action has changed and update its UI.
* @param position Position of the button GuidedAction in array.
*/
public void notifyButtonActionChanged(int position) {
if (mButtonAdapter != null) {
mButtonAdapter.notifyItemChanged(position);
}
}
/**
* Returns the view corresponding to the button action at the indicated position in the list of
* actions for this fragment.
* @param position The integer position of the button action of interest.
* @return The View corresponding to the button action at the indicated position, or null if
* that action is not currently onscreen.
*/
public View getButtonActionItemView(int position) {
final RecyclerView.ViewHolder holder = mButtonActionsStylist.getActionsGridView()
.findViewHolderForPosition(position);
return holder == null ? null : holder.itemView;
}
/**
* Scrolls the action list to the position indicated, selecting that button action's view.
* @param position The integer position of the button action of interest.
*/
public void setSelectedButtonActionPosition(int position) {
mButtonActionsStylist.getActionsGridView().setSelectedPosition(position);
}
/**
* Returns the position if the currently selected button GuidedAction.
* @return position The integer position of the currently selected button action.
*/
public int getSelectedButtonActionPosition() {
return mButtonActionsStylist.getActionsGridView().getSelectedPosition();
}
/**
* 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;
}
/**
* Find GuidedAction by Id.
* @param id Id of the action to search.
* @return GuidedAction object or null if not found.
*/
public GuidedAction findActionById(long id) {
int index = findActionPositionById(id);
return index >= 0 ? mActions.get(index) : null;
}
/**
* Find GuidedAction position in array by Id.
* @param id Id of the action to search.
* @return position of GuidedAction object in array or -1 if not found.
*/
public int findActionPositionById(long id) {
if (mActions != null) {
for (int i = 0; i < mActions.size(); i++) {
GuidedAction action = mActions.get(i);
if (mActions.get(i).getId() == id) {
return i;
}
}
}
return -1;
}
/**
* 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);
}
}
/**
* Notify an action has changed and update its UI.
* @param position Position of the GuidedAction in array.
*/
public void notifyActionChanged(int position) {
if (mAdapter != null) {
mAdapter.notifyItemChanged(position);
}
}
/**
* 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) {
final RecyclerView.ViewHolder holder = mActionsStylist.getActionsGridView()
.findViewHolderForPosition(position);
return holder == null ? null : holder.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) {
mActionsStylist.getActionsGridView().setSelectedPosition(position);
}
/**
* Returns the position if the currently selected GuidedAction.
* @return position The integer position of the currently selected action.
*/
public int getSelectedActionPosition() {
return mActionsStylist.getActionsGridView().getSelectedPosition();
}
/**
* Called by Constructor to provide fragment transitions. Default implementation creates
* a short slide and fade transition in code for {@link #UI_STYLE_DEFAULT} for both enter and
* exit transition. When using style {@link #UI_STYLE_ENTRANCE}, enter transition is set
* to slide from both sides. When using style {@link #UI_STYLE_ACTIVITY_ROOT}, enter
* transition is set to null and you should rely on activity transition.
* <p>
* Subclass may override and set its own fragment transition. Note that because Context is not
* available when onProvideFragmentTransitions() is called, subclass will need use a cached
* static application context to load transition from xml. 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) {
if (getUiStyle() == UI_STYLE_DEFAULT) {
Object enterTransition = TransitionHelper.createFadeAndShortSlide(Gravity.END);
TransitionHelper.exclude(enterTransition, R.id.action_fragment_background, true);
TransitionHelper.exclude(enterTransition, R.id.guided_button_actions_background,
true);
TransitionHelper.exclude(enterTransition, R.id.guidedactions_selector, true);
TransitionHelper.setEnterTransition(this, enterTransition);
Object exitTransition = TransitionHelper.createFadeAndShortSlide(Gravity.START);
TransitionHelper.exclude(exitTransition, R.id.action_fragment_background, true);
TransitionHelper.exclude(exitTransition, R.id.guided_button_actions_background,
true);
TransitionHelper.exclude(exitTransition, R.id.guidedactions_selector, true);
TransitionHelper.setExitTransition(this, exitTransition);
} else if (getUiStyle() == UI_STYLE_ENTRANCE) {
Object enterTransition = TransitionHelper.createFadeAndShortSlide(Gravity.END |
Gravity.START);
TransitionHelper.include(enterTransition, R.id.content_fragment);
TransitionHelper.include(enterTransition, R.id.action_fragment_background);
TransitionHelper.include(enterTransition, R.id.guided_button_actions_background);
TransitionHelper.include(enterTransition, R.id.guidedactions_selector);
TransitionHelper.include(enterTransition, R.id.guidedactions_list);
TransitionHelper.setEnterTransition(this, enterTransition);
// exit transition is unchanged, same as UI_STYLE_DEFAULT
} else if (getUiStyle() == UI_STYLE_ACTIVITY_ROOT) {
// for Activity root, we dont need enter transition, use activity transition
TransitionHelper.setEnterTransition(this, null);
// exit transition is unchanged, same as UI_STYLE_DEFAULT
}
}
}
/**
* Default implementation of background for covering content below GuidedStepFragment.
* It uses current theme attribute guidedStepBackground which by default is read from
* android:windowBackground.
*/
public static class GuidedStepBackgroundFragment extends Fragment {
public GuidedStepBackgroundFragment() {
onProvideFragmentTransitions();
}
/**
* Sets fragment transitions for GuidedStepBackgroundFragment. Can be overridden.
*/
protected void onProvideFragmentTransitions() {
if (Build.VERSION.SDK_INT >= 21) {
Object enterTransition = TransitionHelper.createFadeTransition(
TransitionHelper.FADE_IN|TransitionHelper.FADE_OUT);
TransitionHelper.setEnterTransition(this, enterTransition);
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Activity activity = getActivity();
Context themedContext = null;
if (!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) {
ContextThemeWrapper themeWrapper =
new ContextThemeWrapper(activity, typedValue.resourceId);
if (isGuidedStepTheme(themeWrapper)) {
themedContext = themeWrapper;
}
}
if (!found) {
Log.e(TAG, "GuidedStepFragment does not have an appropriate theme set.");
}
}
if (themedContext != null) {
inflater = inflater.cloneInContext(themedContext);
}
return inflater.inflate(R.layout.lb_guidedstep_background, container, false);
}
}
/**
* Creates a background fragment for {@link #UI_STYLE_ENTRANCE}, returns null for other cases.
* Subclass may override the default behavior, e.g. provide different backgrounds
* for {@link #UI_STYLE_DEFAULT}. Background fragment will be inserted in {@link
* #getContainerIdForBackground()}.
*
* @return fragment that will be inserted below GuidedStepFragment.
*/
protected Fragment onProvideBackgroundFragment() {
if (getUiStyle() == UI_STYLE_ENTRANCE) {
return new GuidedStepBackgroundFragment();
}
return null;
}
/**
* Returns container id for inserting {@link #onProvideBackgroundFragment()}. The id should be
* different than container id for inserting GuidedStepFragment.
* Default value is {@link View#NO_ID}. Subclass must override to host background fragment.
* @return container id for inserting {@link #onProvideBackgroundFragment()}
*/
protected int getContainerIdForBackground() {
return View.NO_ID;
}
/**
* Set UI style to fragment arguments, UI style cannot be changed after initialization.
* @param style {@link #UI_STYLE_ACTIVITY_ROOT} {@link #UI_STYLE_DEFAULT} 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.
*
* @return {@link #UI_STYLE_ACTIVITY_ROOT} {@link #UI_STYLE_DEFAULT} or
* {@link #UI_STYLE_ENTRANCE}.
*/
public int getUiStyle() {
Bundle b = getArguments();
if (b == null) return UI_STYLE_DEFAULT;
return b.getInt(EXTRA_UI_STYLE, UI_STYLE_DEFAULT);
}
/**
* {@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<GuidedAction> actions = new ArrayList<GuidedAction>();
onCreateActions(actions, savedInstanceState);
setActions(actions);
ArrayList<GuidedAction> buttonActions = new ArrayList<GuidedAction>();
onCreateButtonActions(buttonActions, savedInstanceState);
setButtonActions(buttonActions);
}
/**
* {@inheritDoc}
*/
@Override
public void onDestroyView() {
mGuidanceStylist.onDestroyView();
mActionsStylist.onDestroyView();
mButtonActionsStylist.onDestroyView();
super.onDestroyView();
}
/**
* {@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);
View buttonActionsView = mButtonActionsStylist.onCreateView(inflater, actionContainer);
actionContainer.addView(buttonActionsView);
View bg = buttonActionsView.findViewById(R.id.guided_button_actions_background);
if (bg != null) {
bg.setVisibility(View.VISIBLE);
}
GuidedActionAdapter.EditListener editListener = new GuidedActionAdapter.EditListener() {
@Override
public void onImeOpen() {
runImeAnimations(true);
}
@Override
public void onImeClose() {
runImeAnimations(false);
}
@Override
public long onGuidedActionEdited(GuidedAction action) {
return GuidedStepFragment.this.onGuidedActionEditedAndProceed(action);
}
};
mAdapter = new GuidedActionAdapter(mActions, this, this, editListener,
mActionsStylist);
mButtonAdapter = new GuidedActionAdapter(mButtonActions, this, this, editListener,
mButtonActionsStylist);
mActionsStylist.getActionsGridView().setAdapter(mAdapter);
mButtonActionsStylist.getActionsGridView().setAdapter(mButtonAdapter);
if (mButtonActions.size() == 0) {
buttonActionsView.setVisibility(View.GONE);
} else {
Context ctx = mThemeWrapper != null ? mThemeWrapper : getActivity();
TypedValue typedValue = new TypedValue();
if (ctx.getTheme().resolveAttribute(R.attr.guidedActionContentWidthWeightTwoPanels,
typedValue, true)) {
View actionsRoot = v.findViewById(R.id.action_fragment_root);
float weight = typedValue.getFloat();
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) actionsRoot
.getLayoutParams();
lp.weight = weight;
actionsRoot.setLayoutParams(lp);
}
}
int pos = (mSelectedIndex >= 0 && mSelectedIndex < mActions.size()) ?
mSelectedIndex : getFirstCheckedAction();
setSelectedActionPosition(pos);
setSelectedButtonActionPosition(0);
return v;
}
@Override
public void onResume() {
super.onResume();
mActionsStylist.getActionsGridView().requestFocus();
}
/**
* {@inheritDoc}
*/
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(EXTRA_ACTION_SELECTED_INDEX,
(mActionsStylist.getActionsGridView() != null) ?
getSelectedActionPosition() : mSelectedIndex);
}
private static 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;
}
/**
* Convenient method to close GuidedStepFragments on top of other content or finish Activity if
* GuidedStepFragments were started in a separate activity. Pops all stack entries including
* {@link #UI_STYLE_ENTRANCE}; if {@link #UI_STYLE_ENTRANCE} is not found, finish the activity.
*/
public void finishGuidedStepFragments() {
final FragmentManager fragmentManager = getFragmentManager();
final int entryCount = fragmentManager.getBackStackEntryCount();
if (entryCount > 0) {
for (int i = entryCount - 1; i >= 0; i--) {
BackStackEntry entry = fragmentManager.getBackStackEntryAt(i);
if (isUiStyleEntrance(entry.getName())) {
GuidedStepFragment top = getCurrentGuidedStepFragment(fragmentManager);
if (top != null) {
top.setUiStyle(UI_STYLE_ENTRANCE);
}
fragmentManager.popBackStack(entry.getId(),
FragmentManager.POP_BACK_STACK_INCLUSIVE);
return;
}
}
}
ActivityCompat.finishAfterTransition(getActivity());
}
/**
* Convenient method to pop to fragment with Given class.
* @param guidedStepFragmentClass Name of the Class of GuidedStepFragment to pop to.
* @param flags Either 0 or {@link FragmentManager#POP_BACK_STACK_INCLUSIVE}.
*/
public void popBackStackToGuidedStepFragment(Class guidedStepFragmentClass, int flags) {
if (!GuidedStepFragment.class.isAssignableFrom(guidedStepFragmentClass)) {
return;
}
final FragmentManager fragmentManager = getFragmentManager();
final int entryCount = fragmentManager.getBackStackEntryCount();
String className = guidedStepFragmentClass.getName();
if (entryCount > 0) {
for (int i = entryCount - 1; i >= 0; i--) {
BackStackEntry entry = fragmentManager.getBackStackEntryAt(i);
String entryClassName = getGuidedStepFragmentClassName(entry.getName());
if (className.equals(entryClassName)) {
fragmentManager.popBackStack(entry.getId(), flags);
return;
}
}
}
}
private void resolveTheme() {
// 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) {
ContextThemeWrapper themeWrapper =
new ContextThemeWrapper(activity, typedValue.resourceId);
if (isGuidedStepTheme(themeWrapper)) {
mTheme = typedValue.resourceId;
mThemeWrapper = themeWrapper;
} else {
found = false;
mThemeWrapper = null;
}
}
if (!found) {
Log.e(TAG, "GuidedStepFragment does not have an appropriate theme set.");
}
} else if (mTheme != -1) {
mThemeWrapper = new ContextThemeWrapper(activity, mTheme);
}
}
private LayoutInflater getThemeInflater(LayoutInflater inflater) {
if (mTheme == -1) {
return inflater;
} else {
return inflater.cloneInContext(mThemeWrapper);
}
}
private int getFirstCheckedAction() {
for (int i = 0, size = mActions.size(); i < size; i++) {
if (mActions.get(i).isChecked()) {
return i;
}
}
return 0;
}
private void runImeAnimations(boolean entering) {
ArrayList<Animator> animators = new ArrayList<Animator>();
if (entering) {
mGuidanceStylist.onImeAppearing(animators);
mActionsStylist.onImeAppearing(animators);
mButtonActionsStylist.onImeAppearing(animators);
} else {
mGuidanceStylist.onImeDisappearing(animators);
mActionsStylist.onImeDisappearing(animators);
mButtonActionsStylist.onImeDisappearing(animators);
}
AnimatorSet set = new AnimatorSet();
set.playTogether(animators);
set.start();
}
}