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