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