GuidedStepSupportFragment.java revision a97810e4e2ec2552f8247ebdadf323dae70d9e3f
1/* This file is auto-generated from GuidedStepFragment.java.  DO NOT MODIFY. */
2
3/*
4 * Copyright (C) 2015 The Android Open Source Project
5 *
6 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
7 * in compliance with the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software distributed under the License
12 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
13 * or implied. See the License for the specific language governing permissions and limitations under
14 * the License.
15 */
16package android.support.v17.leanback.app;
17
18import android.animation.Animator;
19import android.animation.AnimatorSet;
20import android.support.v4.app.FragmentActivity;
21import android.support.v4.app.Fragment;
22import android.support.v4.app.FragmentManager;
23import android.support.v4.app.FragmentManager.BackStackEntry;
24import android.support.v4.app.FragmentTransaction;
25import android.content.Context;
26import android.content.res.TypedArray;
27import android.os.Build;
28import android.os.Bundle;
29import android.support.annotation.NonNull;
30import android.support.v17.leanback.transition.TransitionHelper;
31import android.support.v17.leanback.R;
32import android.support.v17.leanback.widget.GuidanceStylist;
33import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
34import android.support.v17.leanback.widget.GuidedAction;
35import android.support.v17.leanback.widget.GuidedActionsStylist;
36import android.support.v17.leanback.widget.VerticalGridView;
37import android.support.v4.app.ActivityCompat;
38import android.support.v7.widget.RecyclerView;
39import android.util.AttributeSet;
40import android.util.Log;
41import android.util.TypedValue;
42import android.view.ContextThemeWrapper;
43import android.view.Gravity;
44import android.view.LayoutInflater;
45import android.view.View;
46import android.view.ViewGroup;
47import android.view.ViewTreeObserver;
48import android.widget.ImageView;
49import android.widget.RelativeLayout;
50import android.widget.LinearLayout;
51import android.widget.TextView;
52
53import java.util.ArrayList;
54import 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 * </ul>
80 * <h3>Theming and Stylists</h3>
81 * <p>
82 * GuidedStepSupportFragment 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 * GuidedStepSupportFragments 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 GuidedStepSupportFragment as an overlay to the Activity's theme.</li>
100 * <li>Finally, custom subclasses of GuidedStepSupportFragment 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 Activty'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 GuidedStepSupportFragment 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 * GuidedStepSupportFragments can be grouped together to provide a guided sequence. GuidedStepSupportFragments
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 GuidedStepSupportFragments 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_guidedButtonActionsBackground
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 */
138public class GuidedStepSupportFragment extends Fragment implements GuidedActionAdapter.ClickListener,
139        GuidedActionAdapter.FocusListener {
140
141    private static final String TAG_LEAN_BACK_ACTIONS_FRAGMENT = "leanBackGuidedStepSupportFragment";
142    private static final String EXTRA_ACTION_SELECTED_INDEX = "selectedIndex";
143
144    private static final String ENTRY_NAME_DEFAULT = "GuidedStepDefault";
145
146    private static final String ENTRY_NAME_ENTRANCE = "GuidedStepEntrance";
147
148    private static final boolean IS_FRAMEWORK_FRAGMENT = false;
149
150    /**
151     * Fragment argument name for UI style.  The argument value is persisted in fragment state.
152     * The value is initially {@link #UI_STYLE_DEFAULT} and might be changed in one of the three
153     * helper functions:
154     * <ul>
155     * <li>{@link #addAsRoot(FragmentActivity, GuidedStepSupportFragment, int)}</li>
156     * <li>{@link #add(FragmentManager, GuidedStepSupportFragment)} or {@link #add(FragmentManager,
157     * GuidedStepSupportFragment, int)}</li>
158     * </ul>
159     * <p>
160     * Argument value can be either:
161     * <ul>
162     * <li>{@link #UI_STYLE_DEFAULT}</li>
163     * <li>{@link #UI_STYLE_ENTRANCE}</li>
164     * <li>{@link #UI_STYLE_ACTIVITY_ROOT}</li>
165     * </ul>
166     */
167    public static final String EXTRA_UI_STYLE = "uiStyle";
168
169    /**
170     * Default value for argument {@link #EXTRA_UI_STYLE}.  The default value is assigned
171     * in GuidedStepSupportFragment constructor.  This is the case that we use GuidedStepSupportFragment to
172     * replace another existing GuidedStepSupportFragment when moving forward to next step. Default
173     * behavior of this style is:
174     * <ul>
175     * <li> Enter transition slides in from END(right), exit transition slide out to START(left).
176     * </li>
177     * <li> No background, see {@link #onProvideBackgroundSupportFragment()}.</li>
178     * </ul>
179     */
180    public static final int UI_STYLE_DEFAULT = 0;
181
182    /**
183     * One possible value of argument {@link #EXTRA_UI_STYLE}.  This is the case that we show
184     * GuidedStepSupportFragment on top of other content.  The default behavior of this style:
185     * <ul>
186     * <li>Enter transition slides in from two sides, exit transition is inherited from
187     * {@link #UI_STYLE_DEFAULT}.  Note: Changing exit transition by UI style is not working because
188     * fragment transition asks for exit transition before UI style is restored in Fragment
189     * .onCreate().</li>
190     * <li> {@link #onProvideBackgroundSupportFragment()} will create {@link GuidedStepBackgroundSupportFragment}
191     * to covering underneath content. The activity must provide a container to host background
192     * fragment and override {@link #getContainerIdForBackground()}</li>
193     * </ul>
194     */
195    public static final int UI_STYLE_ENTRANCE = 1;
196
197    /**
198     * One possible value of argument {@link #EXTRA_UI_STYLE}.  This is the case that we show first
199     * GuidedStepSupportFragment in a separate activity.  The default behavior of this style:
200     * <ul>
201     * <li> Enter transition is assigned null (will rely on activity transition), exit transition is
202     * same as {@link #UI_STYLE_DEFAULT}.  Note: Changing exit transition by UI style is not working
203     * because fragment transition asks for exit transition before UI style is restored in
204     * Fragment.onCreate().</li>
205     * <li> No background, see {@link #onProvideBackgroundSupportFragment()}.
206     * </ul>
207     */
208    public static final int UI_STYLE_ACTIVITY_ROOT = 2;
209
210    private static final String TAG = "GuidedStepSupportFragment";
211    private static final boolean DEBUG = false;
212
213    private int mTheme;
214    private ContextThemeWrapper mThemeWrapper;
215    private GuidanceStylist mGuidanceStylist;
216    private GuidedActionsStylist mActionsStylist;
217    private GuidedActionsStylist mButtonActionsStylist;
218    private GuidedActionAdapter mAdapter;
219    private GuidedActionAdapter mButtonAdapter;
220    private List<GuidedAction> mActions = new ArrayList<GuidedAction>();
221    private List<GuidedAction> mButtonActions = new ArrayList<GuidedAction>();
222    private int mSelectedIndex = -1;
223    private int mButtonSelectedIndex = -1;
224
225    public GuidedStepSupportFragment() {
226        // We need to supply the theme before any potential call to onInflate in order
227        // for the defaulting to work properly.
228        mTheme = onProvideTheme();
229        mGuidanceStylist = onCreateGuidanceStylist();
230        mActionsStylist = onCreateActionsStylist();
231        mButtonActionsStylist = onCreateButtonActionsStylist();
232        onProvideFragmentTransitions();
233    }
234
235    /**
236     * Creates the presenter used to style the guidance panel. The default implementation returns
237     * a basic GuidanceStylist.
238     * @return The GuidanceStylist used in this fragment.
239     */
240    public GuidanceStylist onCreateGuidanceStylist() {
241        return new GuidanceStylist();
242    }
243
244    /**
245     * Creates the presenter used to style the guided actions panel. The default implementation
246     * returns a basic GuidedActionsStylist.
247     * @return The GuidedActionsStylist used in this fragment.
248     */
249    public GuidedActionsStylist onCreateActionsStylist() {
250        return new GuidedActionsStylist();
251    }
252
253    /**
254     * Creates the presenter used to style a sided actions panel for button only.
255     * The default implementation returns a basic GuidedActionsStylist.
256     * @return The GuidedActionsStylist used in this fragment.
257     */
258    public GuidedActionsStylist onCreateButtonActionsStylist() {
259        return new GuidedActionsStylist();
260    }
261
262    /**
263     * Returns the theme used for styling the fragment. The default returns -1, indicating that the
264     * host Activity's theme should be used.
265     * @return The theme resource ID of the theme to use in this fragment, or -1 to use the
266     * host Activity's theme.
267     */
268    public int onProvideTheme() {
269        return -1;
270    }
271
272    /**
273     * Returns the information required to provide guidance to the user. This hook is called during
274     * {@link #onCreateView}.  May be overridden to return a custom subclass of {@link
275     * GuidanceStylist.Guidance} for use in a subclass of {@link GuidanceStylist}. The default
276     * returns a Guidance object with empty fields; subclasses should override.
277     * @param savedInstanceState The saved instance state from onCreateView.
278     * @return The Guidance object representing the information used to guide the user.
279     */
280    public @NonNull Guidance onCreateGuidance(Bundle savedInstanceState) {
281        return new Guidance("", "", "", null);
282    }
283
284    /**
285     * Fills out the set of actions available to the user. This hook is called during {@link
286     * #onCreate}. The default leaves the list of actions empty; subclasses should override.
287     * @param actions A non-null, empty list ready to be populated.
288     * @param savedInstanceState The saved instance state from onCreate.
289     */
290    public void onCreateActions(@NonNull List<GuidedAction> actions, Bundle savedInstanceState) {
291    }
292
293    /**
294     * Fills out the set of actions shown at right available to the user. This hook is called during
295     * {@link #onCreate}. The default leaves the list of actions empty; subclasses may override.
296     * @param actions A non-null, empty list ready to be populated.
297     * @param savedInstanceState The saved instance state from onCreate.
298     */
299    public void onCreateButtonActions(@NonNull List<GuidedAction> actions,
300            Bundle savedInstanceState) {
301    }
302
303    /**
304     * Callback invoked when an action is taken by the user. Subclasses should override in
305     * order to act on the user's decisions.
306     * @param action The chosen action.
307     */
308    @Override
309    public void onGuidedActionClicked(GuidedAction action) {
310    }
311
312    /**
313     * Callback invoked when an action is focused (made to be the current selection) by the user.
314     */
315    @Override
316    public void onGuidedActionFocused(GuidedAction action) {
317    }
318
319    /**
320     * Callback invoked when an action's title or description has been edited.
321     * Override {@link #onGuidedActionEditedAndProceed(GuidedAction)} instead of app wants to
322     * control the next action to focus on.
323     */
324    public void onGuidedActionEdited(GuidedAction action) {
325    }
326
327    /**
328     * Callback invoked when an action's title or description has been edited.  Default
329     * implementation calls {@link #onGuidedActionEdited(GuidedAction)} and returns
330     * {@link GuidedAction#ACTION_ID_NEXT}.
331     *
332     * @param action The action that has been edited.
333     * @return ID of the action will be focused or {@link GuidedAction#ACTION_ID_NEXT},
334     * {@link GuidedAction#ACTION_ID_CURRENT}.
335     */
336    public long onGuidedActionEditedAndProceed(GuidedAction action) {
337        onGuidedActionEdited(action);
338        return GuidedAction.ACTION_ID_NEXT;
339    }
340
341    /**
342     * Adds the specified GuidedStepSupportFragment to the fragment stack, replacing any existing
343     * GuidedStepSupportFragments in the stack, and configuring the fragment-to-fragment custom
344     * transitions.  A backstack entry is added, so the fragment will be dismissed when BACK key
345     * is pressed.
346     * <li>If current fragment on stack is GuidedStepSupportFragment: assign {@link #UI_STYLE_DEFAULT}
347     * <li>If current fragment on stack is not GuidedStepSupportFragment: assign {@link #UI_STYLE_ENTRANCE}
348     * <p>
349     * Note: currently fragments added using this method must be created programmatically rather
350     * than via XML.
351     * @param fragmentManager The FragmentManager to be used in the transaction.
352     * @param fragment The GuidedStepSupportFragment to be inserted into the fragment stack.
353     * @return The ID returned by the call FragmentTransaction.replace.
354     */
355    public static int add(FragmentManager fragmentManager, GuidedStepSupportFragment fragment) {
356        return add(fragmentManager, fragment, android.R.id.content);
357    }
358
359    /**
360     * Adds the specified GuidedStepSupportFragment to the fragment stack, replacing any existing
361     * GuidedStepSupportFragments in the stack, and configuring the fragment-to-fragment custom
362     * transitions.  A backstack entry is added, so the fragment will be dismissed when BACK key
363     * is pressed.
364     * <li>If current fragment on stack is GuidedStepSupportFragment: assign {@link #UI_STYLE_DEFAULT}
365     * <li>If current fragment on stack is not GuidedStepSupportFragment: assign {@link #UI_STYLE_ENTRANCE}
366     * <p>
367     * Note: currently fragments added using this method must be created programmatically rather
368     * than via XML.
369     * @param fragmentManager The FragmentManager to be used in the transaction.
370     * @param fragment The GuidedStepSupportFragment to be inserted into the fragment stack.
371     * @param id The id of container to add GuidedStepSupportFragment, can be android.R.id.content.
372     * @return The ID returned by the call FragmentTransaction.replace.
373     */
374    public static int add(FragmentManager fragmentManager, GuidedStepSupportFragment fragment, int id) {
375        boolean inGuidedStep = getCurrentGuidedStepSupportFragment(fragmentManager) != null;
376        if (IS_FRAMEWORK_FRAGMENT && Build.VERSION.SDK_INT >= 21 && Build.VERSION.SDK_INT < 23
377                && !inGuidedStep && fragment.getContainerIdForBackground() != View.NO_ID) {
378            // workaround b/22631964 for framework fragment
379            fragmentManager.beginTransaction()
380                .replace(id, new DummyFragment(), TAG_LEAN_BACK_ACTIONS_FRAGMENT)
381                .replace(fragment.getContainerIdForBackground(), new DummyFragment())
382                .commit();
383        }
384        FragmentTransaction ft = fragmentManager.beginTransaction();
385
386        fragment.setUiStyle(inGuidedStep ? UI_STYLE_DEFAULT : UI_STYLE_ENTRANCE);
387        ft.addToBackStack(fragment.generateStackEntryName());
388        initialBackground(fragment, id, ft);
389        return ft.replace(id, fragment, TAG_LEAN_BACK_ACTIONS_FRAGMENT).commit();
390    }
391
392    /**
393     * Returns BackStackEntry name for the GuidedStepSupportFragment or empty String if no entry is
394     * associated.  Note {@link #UI_STYLE_ACTIVITY_ROOT} will return empty String.  The method
395     * returns undefined value if the fragment is not in FragmentManager.
396     * @return BackStackEntry name for the GuidedStepSupportFragment or empty String if no entry is
397     * associated.
398     */
399    public String generateStackEntryName() {
400        return generateStackEntryName(getUiStyle(), getClass());
401    }
402
403    /**
404     * Generates BackStackEntry name for GuidedStepSupportFragment class or empty String if no entry is
405     * associated.  Note {@link #UI_STYLE_ACTIVITY_ROOT} is not allowed and returns empty String.
406     * @param uiStyle {@link #UI_STYLE_DEFAULT} or {@link #UI_STYLE_ENTRANCE}
407     * @return BackStackEntry name for the GuidedStepSupportFragment or empty String if no entry is
408     * associated.
409     */
410    public static String generateStackEntryName(int uiStyle, Class guidedStepFragmentClass) {
411        if (!GuidedStepSupportFragment.class.isAssignableFrom(guidedStepFragmentClass)) {
412            return "";
413        }
414        switch (uiStyle) {
415        case UI_STYLE_DEFAULT:
416            return ENTRY_NAME_DEFAULT + guidedStepFragmentClass.getName();
417        case UI_STYLE_ENTRANCE:
418            return ENTRY_NAME_ENTRANCE + guidedStepFragmentClass.getName();
419        case UI_STYLE_ACTIVITY_ROOT:
420        default:
421            return "";
422        }
423    }
424
425    /**
426     * Returns true if the backstack represents GuidedStepSupportFragment with {@link #UI_STYLE_ENTRANCE};
427     * false otherwise.
428     * @param backStackEntryName Name of BackStackEntry.
429     * @return True if the backstack represents GuidedStepSupportFragment with {@link #UI_STYLE_ENTRANCE};
430     * false otherwise.
431     */
432    public static boolean isUiStyleEntrance(String backStackEntryName) {
433        return backStackEntryName != null && backStackEntryName.startsWith(ENTRY_NAME_ENTRANCE);
434    }
435
436    /**
437     * Returns true if the backstack represents GuidedStepSupportFragment with {@link #UI_STYLE_DEFAULT};
438     * false otherwise.
439     * @param backStackEntryName Name of BackStackEntry.
440     * @return True if the backstack represents GuidedStepSupportFragment with {@link #UI_STYLE_DEFAULT};
441     * false otherwise.
442     */
443    public static boolean isUiStyleDefault(String backStackEntryName) {
444        return backStackEntryName != null && backStackEntryName.startsWith(ENTRY_NAME_DEFAULT);
445    }
446
447    /**
448     * Extract Class name from BackStackEntry name.
449     * @param backStackEntryName Name of BackStackEntry.
450     * @return Class name of GuidedStepSupportFragment.
451     */
452    public static String getGuidedStepSupportFragmentClassName(String backStackEntryName) {
453        if (backStackEntryName.startsWith(ENTRY_NAME_DEFAULT)) {
454            return backStackEntryName.substring(ENTRY_NAME_DEFAULT.length());
455        } else if (backStackEntryName.startsWith(ENTRY_NAME_ENTRANCE)) {
456            return backStackEntryName.substring(ENTRY_NAME_ENTRANCE.length());
457        } else {
458            return "";
459        }
460    }
461
462    /**
463     * Adds the specified GuidedStepSupportFragment as content of Activity; no backstack entry is added so
464     * the activity will be dismissed when BACK key is pressed.
465     * {@link #UI_STYLE_ACTIVITY_ROOT} is assigned.
466     *
467     * Note: currently fragments added using this method must be created programmatically rather
468     * than via XML.
469     * @param activity The Activity to be used to insert GuidedstepFragment.
470     * @param fragment The GuidedStepSupportFragment to be inserted into the fragment stack.
471     * @param id The id of container to add GuidedStepSupportFragment, can be android.R.id.content.
472     * @return The ID returned by the call FragmentTransaction.replace.
473     */
474    public static int addAsRoot(FragmentActivity activity, GuidedStepSupportFragment fragment, int id) {
475        // Workaround b/23764120: call getDecorView() to force requestFeature of ActivityTransition.
476        activity.getWindow().getDecorView();
477
478        FragmentManager fragmentManager = activity.getSupportFragmentManager();
479        FragmentTransaction ft = fragmentManager.beginTransaction();
480        fragment.setUiStyle(UI_STYLE_ACTIVITY_ROOT);
481        initialBackground(fragment, id, ft);
482        return ft.replace(id, fragment, TAG_LEAN_BACK_ACTIONS_FRAGMENT).commit();
483    }
484
485    static void initialBackground(GuidedStepSupportFragment fragment, int id, FragmentTransaction ft) {
486        if (fragment.getContainerIdForBackground() != View.NO_ID) {
487            Fragment backgroundFragment = fragment.onProvideBackgroundSupportFragment();
488            if (backgroundFragment != null) {
489                ft.replace(fragment.getContainerIdForBackground(), backgroundFragment);
490            }
491        }
492    }
493
494    /**
495     * Returns the current GuidedStepSupportFragment on the fragment transaction stack.
496     * @return The current GuidedStepSupportFragment, if any, on the fragment transaction stack.
497     */
498    public static GuidedStepSupportFragment getCurrentGuidedStepSupportFragment(FragmentManager fm) {
499        Fragment f = fm.findFragmentByTag(TAG_LEAN_BACK_ACTIONS_FRAGMENT);
500        if (f instanceof GuidedStepSupportFragment) {
501            return (GuidedStepSupportFragment) f;
502        }
503        return null;
504    }
505
506    /**
507     * @hide
508     */
509    public static class DummyFragment extends Fragment {
510
511        @Override
512        public View onCreateView(LayoutInflater inflater, ViewGroup container,
513                Bundle savedInstanceState) {
514            final View v = new View(inflater.getContext());
515            v.setVisibility(View.GONE);
516            return v;
517        }
518    }
519
520    /**
521     * Returns the GuidanceStylist that displays guidance information for the user.
522     * @return The GuidanceStylist for this fragment.
523     */
524    public GuidanceStylist getGuidanceStylist() {
525        return mGuidanceStylist;
526    }
527
528    /**
529     * Returns the GuidedActionsStylist that displays the actions the user may take.
530     * @return The GuidedActionsStylist for this fragment.
531     */
532    public GuidedActionsStylist getGuidedActionsStylist() {
533        return mActionsStylist;
534    }
535
536    /**
537     * Returns the list of button GuidedActions that the user may take in this fragment.
538     * @return The list of button GuidedActions for this fragment.
539     */
540    public List<GuidedAction> getButtonActions() {
541        return mButtonActions;
542    }
543
544    /**
545     * Find button GuidedAction by Id.
546     * @param id  Id of the button action to search.
547     * @return  GuidedAction object or null if not found.
548     */
549    public GuidedAction findButtonActionById(long id) {
550        int index = findButtonActionPositionById(id);
551        return index >= 0 ? mButtonActions.get(index) : null;
552    }
553
554    /**
555     * Find button GuidedAction position in array by Id.
556     * @param id  Id of the button action to search.
557     * @return  position of GuidedAction object in array or -1 if not found.
558     */
559    public int findButtonActionPositionById(long id) {
560        if (mButtonActions != null) {
561            for (int i = 0; i < mButtonActions.size(); i++) {
562                GuidedAction action = mButtonActions.get(i);
563                if (mButtonActions.get(i).getId() == id) {
564                    return i;
565                }
566            }
567        }
568        return -1;
569    }
570
571    /**
572     * Returns the GuidedActionsStylist that displays the button actions the user may take.
573     * @return The GuidedActionsStylist for this fragment.
574     */
575    public GuidedActionsStylist getGuidedButtonActionsStylist() {
576        return mButtonActionsStylist;
577    }
578
579    /**
580     * Sets the list of button GuidedActions that the user may take in this fragment.
581     * @param actions The list of button GuidedActions for this fragment.
582     */
583    public void setButtonActions(List<GuidedAction> actions) {
584        mButtonActions = actions;
585        if (mButtonAdapter != null) {
586            mButtonAdapter.setActions(mButtonActions);
587        }
588    }
589
590    /**
591     * Notify an button action has changed and update its UI.
592     * @param position Position of the button GuidedAction in array.
593     */
594    public void notifyButtonActionChanged(int position) {
595        if (mButtonAdapter != null) {
596            mButtonAdapter.notifyItemChanged(position);
597        }
598    }
599
600    /**
601     * Returns the view corresponding to the button action at the indicated position in the list of
602     * actions for this fragment.
603     * @param position The integer position of the button action of interest.
604     * @return The View corresponding to the button action at the indicated position, or null if
605     * that action is not currently onscreen.
606     */
607    public View getButtonActionItemView(int position) {
608        final RecyclerView.ViewHolder holder = mButtonActionsStylist.getActionsGridView()
609                    .findViewHolderForPosition(position);
610        return holder == null ? null : holder.itemView;
611    }
612
613    /**
614     * Scrolls the action list to the position indicated, selecting that button action's view.
615     * @param position The integer position of the button action of interest.
616     */
617    public void setSelectedButtonActionPosition(int position) {
618        mButtonActionsStylist.getActionsGridView().setSelectedPosition(position);
619    }
620
621    /**
622     * Returns the position if the currently selected button GuidedAction.
623     * @return position The integer position of the currently selected button action.
624     */
625    public int getSelectedButtonActionPosition() {
626        return mButtonActionsStylist.getActionsGridView().getSelectedPosition();
627    }
628
629    /**
630     * Returns the list of GuidedActions that the user may take in this fragment.
631     * @return The list of GuidedActions for this fragment.
632     */
633    public List<GuidedAction> getActions() {
634        return mActions;
635    }
636
637    /**
638     * Find GuidedAction by Id.
639     * @param id  Id of the action to search.
640     * @return  GuidedAction object or null if not found.
641     */
642    public GuidedAction findActionById(long id) {
643        int index = findActionPositionById(id);
644        return index >= 0 ? mActions.get(index) : null;
645    }
646
647    /**
648     * Find GuidedAction position in array by Id.
649     * @param id  Id of the action to search.
650     * @return  position of GuidedAction object in array or -1 if not found.
651     */
652    public int findActionPositionById(long id) {
653        if (mActions != null) {
654            for (int i = 0; i < mActions.size(); i++) {
655                GuidedAction action = mActions.get(i);
656                if (mActions.get(i).getId() == id) {
657                    return i;
658                }
659            }
660        }
661        return -1;
662    }
663
664    /**
665     * Sets the list of GuidedActions that the user may take in this fragment.
666     * @param actions The list of GuidedActions for this fragment.
667     */
668    public void setActions(List<GuidedAction> actions) {
669        mActions = actions;
670        if (mAdapter != null) {
671            mAdapter.setActions(mActions);
672        }
673    }
674
675    /**
676     * Notify an action has changed and update its UI.
677     * @param position Position of the GuidedAction in array.
678     */
679    public void notifyActionChanged(int position) {
680        if (mAdapter != null) {
681            mAdapter.notifyItemChanged(position);
682        }
683    }
684
685    /**
686     * Returns the view corresponding to the action at the indicated position in the list of
687     * actions for this fragment.
688     * @param position The integer position of the action of interest.
689     * @return The View corresponding to the action at the indicated position, or null if that
690     * action is not currently onscreen.
691     */
692    public View getActionItemView(int position) {
693        final RecyclerView.ViewHolder holder = mActionsStylist.getActionsGridView()
694                    .findViewHolderForPosition(position);
695        return holder == null ? null : holder.itemView;
696    }
697
698    /**
699     * Scrolls the action list to the position indicated, selecting that action's view.
700     * @param position The integer position of the action of interest.
701     */
702    public void setSelectedActionPosition(int position) {
703        mActionsStylist.getActionsGridView().setSelectedPosition(position);
704    }
705
706    /**
707     * Returns the position if the currently selected GuidedAction.
708     * @return position The integer position of the currently selected action.
709     */
710    public int getSelectedActionPosition() {
711        return mActionsStylist.getActionsGridView().getSelectedPosition();
712    }
713
714    /**
715     * Called by Constructor to provide fragment transitions.  Default implementation creates
716     * a short slide and fade transition in code for {@link #UI_STYLE_DEFAULT} for both enter and
717     * exit transition.  When using style {@link #UI_STYLE_ENTRANCE}, enter transition is set
718     * to slide from both sides.  When using style {@link #UI_STYLE_ACTIVITY_ROOT}, enter
719     * transition is set to null and you should rely on activity transition.
720     * <p>
721     * Subclass may override and set its own fragment transition.  Note that because Context is not
722     * available when onProvideFragmentTransitions() is called, subclass will need use a cached
723     * static application context to load transition from xml.  Because the fragment view is
724     * removed during fragment transition, in general app cannot use two Visibility transition
725     * together.  Workaround is to create your own Visibility transition that controls multiple
726     * animators (e.g. slide and fade animation in one Transition class).
727     */
728    protected void onProvideFragmentTransitions() {
729        if (Build.VERSION.SDK_INT >= 21) {
730            if (getUiStyle() == UI_STYLE_DEFAULT) {
731                Object enterTransition = TransitionHelper.createFadeAndShortSlide(Gravity.END);
732                TransitionHelper.exclude(enterTransition, R.id.action_fragment_background, true);
733                TransitionHelper.exclude(enterTransition, R.id.guided_button_actions_background,
734                        true);
735                TransitionHelper.exclude(enterTransition, R.id.guidedactions_selector, true);
736                TransitionHelper.setEnterTransition(this, enterTransition);
737                Object exitTransition = TransitionHelper.createFadeAndShortSlide(Gravity.START);
738                TransitionHelper.exclude(exitTransition, R.id.action_fragment_background, true);
739                TransitionHelper.exclude(exitTransition, R.id.guided_button_actions_background,
740                        true);
741                TransitionHelper.exclude(exitTransition, R.id.guidedactions_selector, true);
742                TransitionHelper.setExitTransition(this, exitTransition);
743            } else if (getUiStyle() == UI_STYLE_ENTRANCE) {
744                Object enterTransition = TransitionHelper.createFadeAndShortSlide(Gravity.END |
745                        Gravity.START);
746                TransitionHelper.include(enterTransition, R.id.content_fragment);
747                TransitionHelper.include(enterTransition, R.id.action_fragment_background);
748                TransitionHelper.include(enterTransition, R.id.guided_button_actions_background);
749                TransitionHelper.include(enterTransition, R.id.guidedactions_selector);
750                TransitionHelper.include(enterTransition, R.id.guidedactions_list);
751                TransitionHelper.setEnterTransition(this, enterTransition);
752                // exit transition is unchanged, same as UI_STYLE_DEFAULT
753            } else if (getUiStyle() == UI_STYLE_ACTIVITY_ROOT) {
754                // for Activity root, we dont need enter transition, use activity transition
755                TransitionHelper.setEnterTransition(this, null);
756                // exit transition is unchanged, same as UI_STYLE_DEFAULT
757            }
758        }
759    }
760
761    /**
762     * Default implementation of background for covering content below GuidedStepSupportFragment.
763     * It uses current theme attribute guidedStepBackground which by default is read from
764     * android:windowBackground.
765     */
766    public static class GuidedStepBackgroundSupportFragment extends Fragment {
767        public GuidedStepBackgroundSupportFragment() {
768            onProvideFragmentTransitions();
769        }
770
771        /**
772         * Sets fragment transitions for GuidedStepBackgroundSupportFragment.  Can be overridden.
773         */
774        protected void onProvideFragmentTransitions() {
775            if (Build.VERSION.SDK_INT >= 21) {
776                Object enterTransition = TransitionHelper.createFadeTransition(
777                        TransitionHelper.FADE_IN|TransitionHelper.FADE_OUT);
778                TransitionHelper.setEnterTransition(this, enterTransition);
779            }
780        }
781
782        @Override
783        public View onCreateView(LayoutInflater inflater, ViewGroup container,
784                Bundle savedInstanceState) {
785            FragmentActivity activity = getActivity();
786            Context themedContext = null;
787            if (!isGuidedStepTheme(activity)) {
788                // Look up the guidedStepTheme in the activity's currently specified theme.  If it
789                // exists, replace the theme with its value.
790                int resId = R.attr.guidedStepTheme;
791                TypedValue typedValue = new TypedValue();
792                boolean found = activity.getTheme().resolveAttribute(resId, typedValue, true);
793                if (DEBUG) Log.v(TAG, "Found guided step theme reference? " + found);
794                if (found) {
795                    ContextThemeWrapper themeWrapper =
796                            new ContextThemeWrapper(activity, typedValue.resourceId);
797                    if (isGuidedStepTheme(themeWrapper)) {
798                        themedContext = themeWrapper;
799                    }
800                }
801                if (!found) {
802                    Log.e(TAG, "GuidedStepSupportFragment does not have an appropriate theme set.");
803                }
804            }
805
806            if (themedContext != null) {
807                inflater = inflater.cloneInContext(themedContext);
808            }
809
810            return inflater.inflate(R.layout.lb_guidedstep_background, container, false);
811        }
812    }
813
814    /**
815     * Creates a background fragment for {@link #UI_STYLE_ENTRANCE}, returns null for other cases.
816     * Subclass may override the default behavior, e.g. provide different backgrounds
817     * for {@link #UI_STYLE_DEFAULT}.  Background fragment will be inserted in {@link
818     * #getContainerIdForBackground()}.
819     *
820     * @return fragment that will be inserted below GuidedStepSupportFragment.
821     */
822    protected Fragment onProvideBackgroundSupportFragment() {
823        if (getUiStyle() == UI_STYLE_ENTRANCE) {
824            return new GuidedStepBackgroundSupportFragment();
825        }
826        return null;
827    }
828
829    /**
830     * Returns container id for inserting {@link #onProvideBackgroundSupportFragment()}.  The id should be
831     * different than container id for inserting GuidedStepSupportFragment.
832     * Default value is {@link View#NO_ID}.  Subclass must override to host background fragment.
833     * @return container id for inserting {@link #onProvideBackgroundSupportFragment()}
834     */
835    protected int getContainerIdForBackground() {
836        return View.NO_ID;
837    }
838
839
840    /**
841     * Set UI style to fragment arguments,  UI style cannot be changed after initialization.
842     * @param style {@link #UI_STYLE_ACTIVITY_ROOT} {@link #UI_STYLE_DEFAULT} or
843     * {@link #UI_STYLE_ENTRANCE}.
844     */
845    public void setUiStyle(int style) {
846        int oldStyle = getUiStyle();
847        Bundle arguments = getArguments();
848        boolean isNew = false;
849        if (arguments == null) {
850            arguments = new Bundle();
851            isNew = true;
852        }
853        arguments.putInt(EXTRA_UI_STYLE, style);
854        // call setArgument() will validate if the fragment is already added.
855        if (isNew) {
856            setArguments(arguments);
857        }
858        if (style != oldStyle) {
859            onProvideFragmentTransitions();
860        }
861    }
862
863    /**
864     * Read UI style from fragment arguments.
865     *
866     * @return {@link #UI_STYLE_ACTIVITY_ROOT} {@link #UI_STYLE_DEFAULT} or
867     * {@link #UI_STYLE_ENTRANCE}.
868     */
869    public int getUiStyle() {
870        Bundle b = getArguments();
871        if (b == null) return UI_STYLE_DEFAULT;
872        return b.getInt(EXTRA_UI_STYLE, UI_STYLE_DEFAULT);
873    }
874
875    /**
876     * {@inheritDoc}
877     */
878    @Override
879    public void onCreate(Bundle savedInstanceState) {
880        super.onCreate(savedInstanceState);
881        if (DEBUG) Log.v(TAG, "onCreate");
882        // Set correct transition from saved arguments.
883        onProvideFragmentTransitions();
884        Bundle state = (savedInstanceState != null) ? savedInstanceState : getArguments();
885        if (state != null) {
886            if (mSelectedIndex == -1) {
887                mSelectedIndex = state.getInt(EXTRA_ACTION_SELECTED_INDEX, -1);
888            }
889        }
890        ArrayList<GuidedAction> actions = new ArrayList<GuidedAction>();
891        onCreateActions(actions, savedInstanceState);
892        setActions(actions);
893        ArrayList<GuidedAction> buttonActions = new ArrayList<GuidedAction>();
894        onCreateButtonActions(buttonActions, savedInstanceState);
895        setButtonActions(buttonActions);
896    }
897
898    /**
899     * {@inheritDoc}
900     */
901    @Override
902    public void onDestroyView() {
903        mGuidanceStylist.onDestroyView();
904        mActionsStylist.onDestroyView();
905        mButtonActionsStylist.onDestroyView();
906        super.onDestroyView();
907    }
908
909    /**
910     * {@inheritDoc}
911     */
912    @Override
913    public View onCreateView(LayoutInflater inflater, ViewGroup container,
914            Bundle savedInstanceState) {
915        if (DEBUG) Log.v(TAG, "onCreateView");
916
917        resolveTheme();
918        inflater = getThemeInflater(inflater);
919
920        View v = inflater.inflate(R.layout.lb_guidedstep_fragment, container, false);
921        ViewGroup guidanceContainer = (ViewGroup) v.findViewById(R.id.content_fragment);
922        ViewGroup actionContainer = (ViewGroup) v.findViewById(R.id.action_fragment);
923
924        Guidance guidance = onCreateGuidance(savedInstanceState);
925        View guidanceView = mGuidanceStylist.onCreateView(inflater, guidanceContainer, guidance);
926        guidanceContainer.addView(guidanceView);
927
928        View actionsView = mActionsStylist.onCreateView(inflater, actionContainer);
929        actionContainer.addView(actionsView);
930
931        View buttonActionsView = mButtonActionsStylist.onCreateView(inflater, actionContainer);
932        actionContainer.addView(buttonActionsView);
933        View bg = buttonActionsView.findViewById(R.id.guided_button_actions_background);
934        if (bg != null) {
935            bg.setVisibility(View.VISIBLE);
936        }
937
938        GuidedActionAdapter.EditListener editListener = new GuidedActionAdapter.EditListener() {
939
940                @Override
941                public void onImeOpen() {
942                    runImeAnimations(true);
943                }
944
945                @Override
946                public void onImeClose() {
947                    runImeAnimations(false);
948                }
949
950                @Override
951                public long onGuidedActionEdited(GuidedAction action) {
952                    return GuidedStepSupportFragment.this.onGuidedActionEditedAndProceed(action);
953                }
954        };
955
956        mAdapter = new GuidedActionAdapter(mActions, this, this, editListener,
957                mActionsStylist);
958        mButtonAdapter = new GuidedActionAdapter(mButtonActions, this, this, editListener,
959                mButtonActionsStylist);
960
961        mActionsStylist.getActionsGridView().setAdapter(mAdapter);
962        mButtonActionsStylist.getActionsGridView().setAdapter(mButtonAdapter);
963        if (mButtonActions.size() == 0) {
964            buttonActionsView.setVisibility(View.GONE);
965        } else {
966            Context ctx = mThemeWrapper != null ? mThemeWrapper : getActivity();
967            TypedValue typedValue = new TypedValue();
968            if (ctx.getTheme().resolveAttribute(R.attr.guidedActionContentWidthWeightTwoPanels,
969                    typedValue, true)) {
970                View actionsRoot = v.findViewById(R.id.action_fragment_root);
971                float weight = typedValue.getFloat();
972                LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) actionsRoot
973                        .getLayoutParams();
974                lp.weight = weight;
975                actionsRoot.setLayoutParams(lp);
976            }
977        }
978
979        int pos = (mSelectedIndex >= 0 && mSelectedIndex < mActions.size()) ?
980                mSelectedIndex : getFirstCheckedAction();
981        setSelectedActionPosition(pos);
982
983        setSelectedButtonActionPosition(0);
984
985        return v;
986    }
987
988    @Override
989    public void onResume() {
990        super.onResume();
991        mActionsStylist.getActionsGridView().requestFocus();
992    }
993
994    /**
995     * {@inheritDoc}
996     */
997    @Override
998    public void onSaveInstanceState(Bundle outState) {
999        super.onSaveInstanceState(outState);
1000        outState.putInt(EXTRA_ACTION_SELECTED_INDEX,
1001                (mActionsStylist.getActionsGridView() != null) ?
1002                        getSelectedActionPosition() : mSelectedIndex);
1003    }
1004
1005    private static boolean isGuidedStepTheme(Context context) {
1006        int resId = R.attr.guidedStepThemeFlag;
1007        TypedValue typedValue = new TypedValue();
1008        boolean found = context.getTheme().resolveAttribute(resId, typedValue, true);
1009        if (DEBUG) Log.v(TAG, "Found guided step theme flag? " + found);
1010        return found && typedValue.type == TypedValue.TYPE_INT_BOOLEAN && typedValue.data != 0;
1011    }
1012
1013    /**
1014     * Convenient method to close GuidedStepSupportFragments on top of other content or finish Activity if
1015     * GuidedStepSupportFragments were started in a separate activity.  Pops all stack entries including
1016     * {@link #UI_STYLE_ENTRANCE}; if {@link #UI_STYLE_ENTRANCE} is not found, finish the activity.
1017     */
1018    public void finishGuidedStepSupportFragments() {
1019        final FragmentManager fragmentManager = getFragmentManager();
1020        final int entryCount = fragmentManager.getBackStackEntryCount();
1021        if (entryCount > 0) {
1022            for (int i = entryCount - 1; i >= 0; i--) {
1023                BackStackEntry entry = fragmentManager.getBackStackEntryAt(i);
1024                if (isUiStyleEntrance(entry.getName())) {
1025                    GuidedStepSupportFragment top = getCurrentGuidedStepSupportFragment(fragmentManager);
1026                    if (top != null) {
1027                        top.setUiStyle(UI_STYLE_ENTRANCE);
1028                    }
1029                    fragmentManager.popBackStack(entry.getId(),
1030                            FragmentManager.POP_BACK_STACK_INCLUSIVE);
1031                    return;
1032                }
1033            }
1034        }
1035        ActivityCompat.finishAfterTransition(getActivity());
1036    }
1037
1038    /**
1039     * Convenient method to pop to fragment with Given class.
1040     * @param  guidedStepFragmentClass  Name of the Class of GuidedStepSupportFragment to pop to.
1041     * @param flags Either 0 or {@link FragmentManager#POP_BACK_STACK_INCLUSIVE}.
1042     */
1043    public void popBackStackToGuidedStepSupportFragment(Class guidedStepFragmentClass, int flags) {
1044        if (!GuidedStepSupportFragment.class.isAssignableFrom(guidedStepFragmentClass)) {
1045            return;
1046        }
1047        final FragmentManager fragmentManager = getFragmentManager();
1048        final int entryCount = fragmentManager.getBackStackEntryCount();
1049        String className = guidedStepFragmentClass.getName();
1050        if (entryCount > 0) {
1051            for (int i = entryCount - 1; i >= 0; i--) {
1052                BackStackEntry entry = fragmentManager.getBackStackEntryAt(i);
1053                String entryClassName = getGuidedStepSupportFragmentClassName(entry.getName());
1054                if (className.equals(entryClassName)) {
1055                    fragmentManager.popBackStack(entry.getId(), flags);
1056                    return;
1057                }
1058            }
1059        }
1060    }
1061
1062    private void resolveTheme() {
1063        // Look up the guidedStepTheme in the currently specified theme.  If it exists,
1064        // replace the theme with its value.
1065        FragmentActivity activity = getActivity();
1066        if (mTheme == -1 && !isGuidedStepTheme(activity)) {
1067            // Look up the guidedStepTheme in the activity's currently specified theme.  If it
1068            // exists, replace the theme with its value.
1069            int resId = R.attr.guidedStepTheme;
1070            TypedValue typedValue = new TypedValue();
1071            boolean found = activity.getTheme().resolveAttribute(resId, typedValue, true);
1072            if (DEBUG) Log.v(TAG, "Found guided step theme reference? " + found);
1073            if (found) {
1074                ContextThemeWrapper themeWrapper =
1075                        new ContextThemeWrapper(activity, typedValue.resourceId);
1076                if (isGuidedStepTheme(themeWrapper)) {
1077                    mTheme = typedValue.resourceId;
1078                    mThemeWrapper = themeWrapper;
1079                } else {
1080                    found = false;
1081                    mThemeWrapper = null;
1082                }
1083            }
1084            if (!found) {
1085                Log.e(TAG, "GuidedStepSupportFragment does not have an appropriate theme set.");
1086            }
1087        } else if (mTheme != -1) {
1088            mThemeWrapper = new ContextThemeWrapper(activity, mTheme);
1089        }
1090    }
1091
1092    private LayoutInflater getThemeInflater(LayoutInflater inflater) {
1093        if (mTheme == -1) {
1094            return inflater;
1095        } else {
1096            return inflater.cloneInContext(mThemeWrapper);
1097        }
1098    }
1099
1100    private int getFirstCheckedAction() {
1101        for (int i = 0, size = mActions.size(); i < size; i++) {
1102            if (mActions.get(i).isChecked()) {
1103                return i;
1104            }
1105        }
1106        return 0;
1107    }
1108
1109    private void runImeAnimations(boolean entering) {
1110        ArrayList<Animator> animators = new ArrayList<Animator>();
1111        if (entering) {
1112            mGuidanceStylist.onImeAppearing(animators);
1113            mActionsStylist.onImeAppearing(animators);
1114            mButtonActionsStylist.onImeAppearing(animators);
1115        } else {
1116            mGuidanceStylist.onImeDisappearing(animators);
1117            mActionsStylist.onImeDisappearing(animators);
1118            mButtonActionsStylist.onImeDisappearing(animators);
1119        }
1120        AnimatorSet set = new AnimatorSet();
1121        set.playTogether(animators);
1122        set.start();
1123    }
1124
1125}
1126