GuidedActionsStylist.java revision be6eb618b4ba8a74d69fa04c77c717b1fcbea818
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 */
14package android.support.v17.leanback.widget;
15
16import android.animation.Animator;
17import android.animation.AnimatorInflater;
18import android.animation.AnimatorListenerAdapter;
19import android.animation.AnimatorSet;
20import android.animation.ObjectAnimator;
21import android.content.Context;
22import android.content.pm.PackageManager;
23import android.content.res.Resources;
24import android.content.res.TypedArray;
25import android.graphics.Rect;
26import android.graphics.drawable.Drawable;
27import android.net.Uri;
28import android.support.annotation.NonNull;
29import android.support.v17.leanback.R;
30import android.support.v17.leanback.widget.VerticalGridView;
31import android.support.v4.content.ContextCompat;
32import android.support.v4.view.ViewCompat;
33import android.support.v7.widget.RecyclerView;
34import android.support.v7.widget.RecyclerView.ViewHolder;
35import android.text.TextUtils;
36import android.util.Log;
37import android.util.TypedValue;
38import android.view.animation.DecelerateInterpolator;
39import android.view.inputmethod.EditorInfo;
40import android.view.LayoutInflater;
41import android.view.View;
42import android.view.ViewGroup;
43import android.view.ViewGroup.LayoutParams;
44import android.view.ViewPropertyAnimator;
45import android.view.ViewTreeObserver;
46import android.view.WindowManager;
47import android.widget.Checkable;
48import android.widget.EditText;
49import android.widget.ImageView;
50import android.widget.TextView;
51
52import java.util.Collections;
53import java.util.List;
54
55/**
56 * GuidedActionsStylist is used within a {@link android.support.v17.leanback.app.GuidedStepFragment}
57 * to supply the right-side panel where users can take actions. It consists of a container for the
58 * list of actions, and a stationary selector view that indicates visually the location of focus.
59 * GuidedActionsStylist has two different layouts: default is for normal actions including text,
60 * radio, checkbox etc, the other when {@link #setAsButtonActions()} is called is recommended for
61 * button actions such as "yes", "no".
62 * <p>
63 * Many aspects of the base GuidedActionsStylist can be customized through theming; see the
64 * theme attributes below. Note that these attributes are not set on individual elements in layout
65 * XML, but instead would be set in a custom theme. See
66 * <a href="http://developer.android.com/guide/topics/ui/themes.html">Styles and Themes</a>
67 * for more information.
68 * <p>
69 * If these hooks are insufficient, this class may also be subclassed. Subclasses may wish to
70 * override the {@link #onProvideLayoutId} method to change the layout used to display the
71 * list container and selector, or the {@link #onProvideItemLayoutId} method to change the layout
72 * used to display each action.
73 * <p>
74 * Note: If an alternate list layout is provided, the following view IDs must be supplied:
75 * <ul>
76 * <li>{@link android.support.v17.leanback.R.id#guidedactions_selector}</li>
77 * <li>{@link android.support.v17.leanback.R.id#guidedactions_list}</li>
78 * </ul><p>
79 * These view IDs must be present in order for the stylist to function. The list ID must correspond
80 * to a {@link VerticalGridView} or subclass.
81 * <p>
82 * If an alternate item layout is provided, the following view IDs should be used to refer to base
83 * elements:
84 * <ul>
85 * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_content}</li>
86 * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_title}</li>
87 * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_description}</li>
88 * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_icon}</li>
89 * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_checkmark}</li>
90 * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_chevron}</li>
91 * </ul><p>
92 * These view IDs are allowed to be missing, in which case the corresponding views in {@link
93 * GuidedActionsStylist.ViewHolder} will be null.
94 * <p>
95 * In order to support editable actions, the view associated with guidedactions_item_title should
96 * be a subclass of {@link android.widget.EditText}, and should satisfy the {@link
97 * ImeKeyMonitor} interface.
98 *
99 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedStepImeAppearingAnimation
100 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedStepImeDisappearingAnimation
101 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsSelectorShowAnimation
102 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsSelectorHideAnimation
103 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsSelectorStyle
104 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsListStyle
105 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedSubActionsListStyle
106 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedButtonActionsListStyle
107 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemContainerStyle
108 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemCheckmarkStyle
109 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemIconStyle
110 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemContentStyle
111 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemTitleStyle
112 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemDescriptionStyle
113 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemChevronStyle
114 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionPressedAnimation
115 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionUnpressedAnimation
116 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionEnabledChevronAlpha
117 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionDisabledChevronAlpha
118 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionTitleMinLines
119 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionTitleMaxLines
120 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionDescriptionMinLines
121 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionVerticalPadding
122 * @see android.R.styleable#Theme_listChoiceIndicatorSingle
123 * @see android.R.styleable#Theme_listChoiceIndicatorMultiple
124 * @see android.support.v17.leanback.app.GuidedStepFragment
125 * @see GuidedAction
126 */
127public class GuidedActionsStylist implements FragmentAnimationProvider {
128
129    /**
130     * Default viewType that associated with default layout Id for the action item.
131     * @see #getItemViewType(GuidedAction)
132     * @see #onProvideItemLayoutId(int)
133     * @see #onCreateViewHolder(ViewGroup, int)
134     */
135    public static final int VIEW_TYPE_DEFAULT = 0;
136
137    /**
138     * ViewHolder caches information about the action item layouts' subviews. Subclasses of {@link
139     * GuidedActionsStylist} may also wish to subclass this in order to add fields.
140     * @see GuidedAction
141     */
142    public static class ViewHolder extends RecyclerView.ViewHolder {
143
144        private GuidedAction mAction;
145        private View mContentView;
146        private TextView mTitleView;
147        private TextView mDescriptionView;
148        private ImageView mIconView;
149        private ImageView mCheckmarkView;
150        private ImageView mChevronView;
151        private boolean mInEditing;
152        private boolean mInEditingDescription;
153        private final boolean mIsSubAction;
154
155        /**
156         * Constructs an ViewHolder and caches the relevant subviews.
157         */
158        public ViewHolder(View v) {
159            this(v, false);
160        }
161
162        /**
163         * Constructs an ViewHolder for sub action and caches the relevant subviews.
164         */
165        public ViewHolder(View v, boolean isSubAction) {
166            super(v);
167
168            mContentView = v.findViewById(R.id.guidedactions_item_content);
169            mTitleView = (TextView) v.findViewById(R.id.guidedactions_item_title);
170            mDescriptionView = (TextView) v.findViewById(R.id.guidedactions_item_description);
171            mIconView = (ImageView) v.findViewById(R.id.guidedactions_item_icon);
172            mCheckmarkView = (ImageView) v.findViewById(R.id.guidedactions_item_checkmark);
173            mChevronView = (ImageView) v.findViewById(R.id.guidedactions_item_chevron);
174            mIsSubAction = isSubAction;
175        }
176
177        /**
178         * Returns the content view within this view holder's view, where title and description are
179         * shown.
180         */
181        public View getContentView() {
182            return mContentView;
183        }
184
185        /**
186         * Returns the title view within this view holder's view.
187         */
188        public TextView getTitleView() {
189            return mTitleView;
190        }
191
192        /**
193         * Convenience method to return an editable version of the title, if possible,
194         * or null if the title view isn't an EditText.
195         */
196        public EditText getEditableTitleView() {
197            return (mTitleView instanceof EditText) ? (EditText)mTitleView : null;
198        }
199
200        /**
201         * Returns the description view within this view holder's view.
202         */
203        public TextView getDescriptionView() {
204            return mDescriptionView;
205        }
206
207        /**
208         * Convenience method to return an editable version of the description, if possible,
209         * or null if the description view isn't an EditText.
210         */
211        public EditText getEditableDescriptionView() {
212            return (mDescriptionView instanceof EditText) ? (EditText)mDescriptionView : null;
213        }
214
215        /**
216         * Returns the icon view within this view holder's view.
217         */
218        public ImageView getIconView() {
219            return mIconView;
220        }
221
222        /**
223         * Returns the checkmark view within this view holder's view.
224         */
225        public ImageView getCheckmarkView() {
226            return mCheckmarkView;
227        }
228
229        /**
230         * Returns the chevron view within this view holder's view.
231         */
232        public ImageView getChevronView() {
233            return mChevronView;
234        }
235
236        /**
237         * Returns true if the TextView is in editing title or description, false otherwise.
238         */
239        public boolean isInEditing() {
240            return mInEditing;
241        }
242
243        /**
244         * Returns true if the TextView is in editing description, false otherwise.
245         */
246        public boolean isInEditingDescription() {
247            return mInEditingDescription;
248        }
249
250        /**
251         * @return Current editing title view or description view or null if not in editing.
252         */
253        public View getEditingView() {
254            if (mInEditing) {
255                return mInEditingDescription ?  mDescriptionView : mTitleView;
256            } else {
257                return null;
258            }
259        }
260
261        /**
262         * @return True if bound action is inside {@link GuidedAction#getSubActions()}, false
263         * otherwise.
264         */
265        public boolean isSubAction() {
266            return mIsSubAction;
267        }
268
269        /**
270         * @return Currently bound action.
271         */
272        public GuidedAction getAction() {
273            return mAction;
274        }
275    }
276
277    private static String TAG = "GuidedActionsStylist";
278
279    private ViewGroup mMainView;
280    private VerticalGridView mActionsGridView;
281    private VerticalGridView mSubActionsGridView;
282    private View mBgView;
283    private View mSelectorView;
284    private View mContentView;
285    private boolean mButtonActions;
286
287    private Animator mSelectorAnimator;
288
289    // Cached values from resources
290    private float mEnabledTextAlpha;
291    private float mDisabledTextAlpha;
292    private float mEnabledDescriptionAlpha;
293    private float mDisabledDescriptionAlpha;
294    private float mEnabledChevronAlpha;
295    private float mDisabledChevronAlpha;
296    private int mTitleMinLines;
297    private int mTitleMaxLines;
298    private int mDescriptionMinLines;
299    private int mVerticalPadding;
300    private int mDisplayHeight;
301
302    private GuidedAction mExpandedAction = null;
303
304    private final RecyclerView.OnScrollListener mOnGridScrollListener =
305            new RecyclerView.OnScrollListener() {
306        @Override
307        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
308            if (newState == RecyclerView.SCROLL_STATE_IDLE) {
309                if (mSelectorView.getAlpha() != 1f) {
310                    updateSelectorView(true);
311                }
312            }
313        }
314    };
315
316    /**
317     * Creates a view appropriate for displaying a list of GuidedActions, using the provided
318     * inflater and container.
319     * <p>
320     * <i>Note: Does not actually add the created view to the container; the caller should do
321     * this.</i>
322     * @param inflater The layout inflater to be used when constructing the view.
323     * @param container The view group to be passed in the call to
324     * <code>LayoutInflater.inflate</code>.
325     * @return The view to be added to the caller's view hierarchy.
326     */
327    public View onCreateView(LayoutInflater inflater, ViewGroup container) {
328        mMainView = (ViewGroup) inflater.inflate(onProvideLayoutId(), container, false);
329        mContentView = mMainView.findViewById(mButtonActions ? R.id.guidedactions_content2 :
330                R.id.guidedactions_content);
331        mSelectorView = mMainView.findViewById(mButtonActions ? R.id.guidedactions_selector2 :
332                R.id.guidedactions_selector);
333        mSelectorView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
334            @Override
335            public void onLayoutChange(View v, int left, int top, int right, int bottom,
336                    int oldLeft, int oldTop, int oldRight, int oldBottom) {
337                updateSelectorView(false);
338            }
339        });
340        mBgView = mMainView.findViewById(mButtonActions ? R.id.guidedactions_list_background2 :
341                R.id.guidedactions_list_background);
342        if (mMainView instanceof VerticalGridView) {
343            mActionsGridView = (VerticalGridView) mMainView;
344        } else {
345            mActionsGridView = (VerticalGridView) mMainView.findViewById(mButtonActions ?
346                    R.id.guidedactions_list2 : R.id.guidedactions_list);
347            if (mActionsGridView == null) {
348                throw new IllegalStateException("No ListView exists.");
349            }
350            mActionsGridView.setWindowAlignmentOffset(0);
351            mActionsGridView.setWindowAlignmentOffsetPercent(50f);
352            mActionsGridView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE);
353            if (mSelectorView != null) {
354                mActionsGridView.setOnScrollListener(mOnGridScrollListener);
355            }
356            if (!mButtonActions) {
357                mSubActionsGridView = (VerticalGridView) mMainView.findViewById(
358                        R.id.guidedactions_sub_list);
359                if (mSelectorView != null && mSubActionsGridView != null) {
360                    mSubActionsGridView.setOnScrollListener(mOnGridScrollListener);
361                }
362            }
363        }
364
365        if (mSelectorView != null) {
366            // ALlow focus to move to other views
367            mMainView.getViewTreeObserver().addOnGlobalFocusChangeListener(
368                    mGlobalFocusChangeListener);
369        }
370
371        // Cache widths, chevron alpha values, max and min text lines, etc
372        Context ctx = mMainView.getContext();
373        TypedValue val = new TypedValue();
374        mEnabledChevronAlpha = getFloat(ctx, val, R.attr.guidedActionEnabledChevronAlpha);
375        mDisabledChevronAlpha = getFloat(ctx, val, R.attr.guidedActionDisabledChevronAlpha);
376        mTitleMinLines = getInteger(ctx, val, R.attr.guidedActionTitleMinLines);
377        mTitleMaxLines = getInteger(ctx, val, R.attr.guidedActionTitleMaxLines);
378        mDescriptionMinLines = getInteger(ctx, val, R.attr.guidedActionDescriptionMinLines);
379        mVerticalPadding = getDimension(ctx, val, R.attr.guidedActionVerticalPadding);
380        mDisplayHeight = ((WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE))
381                .getDefaultDisplay().getHeight();
382
383        mEnabledTextAlpha = Float.valueOf(ctx.getResources().getString(R.string
384                .lb_guidedactions_item_unselected_text_alpha));
385        mDisabledTextAlpha = Float.valueOf(ctx.getResources().getString(R.string
386                .lb_guidedactions_item_disabled_text_alpha));
387        mEnabledDescriptionAlpha = Float.valueOf(ctx.getResources().getString(R.string
388                .lb_guidedactions_item_unselected_description_text_alpha));
389        mDisabledDescriptionAlpha = Float.valueOf(ctx.getResources().getString(R.string
390                .lb_guidedactions_item_disabled_description_text_alpha));
391        return mMainView;
392    }
393
394    /**
395     * Choose the layout resource for button actions in {@link #onProvideLayoutId()}.
396     */
397    public void setAsButtonActions() {
398        if (mMainView != null) {
399            throw new IllegalStateException("setAsButtonActions() must be called before creating "
400                    + "views");
401        }
402        mButtonActions = true;
403    }
404
405    /**
406     * Returns true if it is button actions list, false for normal actions list.
407     * @return True if it is button actions list, false for normal actions list.
408     */
409    public boolean isButtonActions() {
410        return mButtonActions;
411    }
412
413    final ViewTreeObserver.OnGlobalFocusChangeListener mGlobalFocusChangeListener =
414            new ViewTreeObserver.OnGlobalFocusChangeListener() {
415
416        @Override
417        public void onGlobalFocusChanged(View oldFocus, View newFocus) {
418            updateSelectorView(false);
419        }
420    };
421
422    /**
423     * Called when destroy the View created by GuidedActionsStylist.
424     */
425    public void onDestroyView() {
426        if (mSelectorView != null) {
427            mMainView.getViewTreeObserver().removeOnGlobalFocusChangeListener(
428                    mGlobalFocusChangeListener);
429        }
430        endSelectorAnimator();
431        mExpandedAction = null;
432        mActionsGridView = null;
433        mSubActionsGridView = null;
434        mSelectorView = null;
435        mContentView = null;
436        mBgView = null;
437        mMainView = null;
438    }
439
440    /**
441     * Returns the VerticalGridView that displays the list of GuidedActions.
442     * @return The VerticalGridView for this presenter.
443     */
444    public VerticalGridView getActionsGridView() {
445        return mActionsGridView;
446    }
447
448    /**
449     * Returns the VerticalGridView that displays the sub actions list of an expanded action.
450     * @return The VerticalGridView that displays the sub actions list of an expanded action.
451     */
452    public VerticalGridView getSubActionsGridView() {
453        return mSubActionsGridView;
454    }
455
456    /**
457     * Provides the resource ID of the layout defining the host view for the list of guided actions.
458     * Subclasses may override to provide their own customized layouts. The base implementation
459     * returns {@link android.support.v17.leanback.R.layout#lb_guidedactions} or
460     * {@link android.support.v17.leanback.R.layout#lb_guidedbuttonactions} if
461     * {@link #isButtonActions()} is true. If overridden, the substituted layout should contain
462     * matching IDs for any views that should be managed by the base class; this can be achieved by
463     * starting with a copy of the base layout file.
464     *
465     * @return The resource ID of the layout to be inflated to define the host view for the list of
466     *         GuidedActions.
467     */
468    public int onProvideLayoutId() {
469        return mButtonActions ? R.layout.lb_guidedbuttonactions : R.layout.lb_guidedactions;
470    }
471
472    /**
473     * Return view type of action, each different type can have differently associated layout Id.
474     * Default implementation returns {@link #VIEW_TYPE_DEFAULT}.
475     * @param action  The action object.
476     * @return View type that used in {@link #onProvideItemLayoutId(int)}.
477     */
478    public int getItemViewType(GuidedAction action) {
479        return VIEW_TYPE_DEFAULT;
480    }
481
482    /**
483     * Provides the resource ID of the layout defining the view for an individual guided actions.
484     * Subclasses may override to provide their own customized layouts. The base implementation
485     * returns {@link android.support.v17.leanback.R.layout#lb_guidedactions_item}. If overridden,
486     * the substituted layout should contain matching IDs for any views that should be managed by
487     * the base class; this can be achieved by starting with a copy of the base layout file. Note
488     * that in order for the item to support editing, the title view should both subclass {@link
489     * android.widget.EditText} and implement {@link ImeKeyMonitor}; see {@link
490     * GuidedActionEditText}.  To support different types of Layouts, override {@link
491     * #onProvideItemLayoutId(int)}.
492     * @return The resource ID of the layout to be inflated to define the view to display an
493     * individual GuidedAction.
494     */
495    public int onProvideItemLayoutId() {
496        return R.layout.lb_guidedactions_item;
497    }
498
499    /**
500     * Provides the resource ID of the layout defining the view for an individual guided actions.
501     * Subclasses may override to provide their own customized layouts. The base implementation
502     * returns {@link android.support.v17.leanback.R.layout#lb_guidedactions_item}. If overridden,
503     * the substituted layout should contain matching IDs for any views that should be managed by
504     * the base class; this can be achieved by starting with a copy of the base layout file. Note
505     * that in order for the item to support editing, the title view should both subclass {@link
506     * android.widget.EditText} and implement {@link ImeKeyMonitor}; see {@link
507     * GuidedActionEditText}.
508     * @param viewType View type returned by {@link #getItemViewType(GuidedAction)}
509     * @return The resource ID of the layout to be inflated to define the view to display an
510     * individual GuidedAction.
511     */
512    public int onProvideItemLayoutId(int viewType) {
513        if (viewType == VIEW_TYPE_DEFAULT) {
514            return onProvideItemLayoutId();
515        } else {
516            throw new RuntimeException("ViewType " + viewType +
517                    " not supported in GuidedActionsStylist");
518        }
519    }
520
521    /**
522     * Constructs a {@link ViewHolder} capable of representing {@link GuidedAction}s. Subclasses
523     * may choose to return a subclass of ViewHolder.  To support different view types, override
524     * {@link #onCreateViewHolder(ViewGroup, int)}
525     * <p>
526     * <i>Note: Should not actually add the created view to the parent; the caller will do
527     * this.</i>
528     * @param parent The view group to be used as the parent of the new view.
529     * @return The view to be added to the caller's view hierarchy.
530     */
531    public ViewHolder onCreateViewHolder(ViewGroup parent) {
532        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
533        View v = inflater.inflate(onProvideItemLayoutId(), parent, false);
534        return new ViewHolder(v, parent == mSubActionsGridView);
535    }
536
537    /**
538     * Constructs a {@link ViewHolder} capable of representing {@link GuidedAction}s. Subclasses
539     * may choose to return a subclass of ViewHolder.
540     * <p>
541     * <i>Note: Should not actually add the created view to the parent; the caller will do
542     * this.</i>
543     * @param parent The view group to be used as the parent of the new view.
544     * @param viewType The viewType returned by {@link #getItemViewType(GuidedAction)}
545     * @return The view to be added to the caller's view hierarchy.
546     */
547    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
548        if (viewType == VIEW_TYPE_DEFAULT) {
549            return onCreateViewHolder(parent);
550        }
551        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
552        View v = inflater.inflate(onProvideItemLayoutId(viewType), parent, false);
553        return new ViewHolder(v, parent == mSubActionsGridView);
554    }
555
556    /**
557     * Binds a {@link ViewHolder} to a particular {@link GuidedAction}.
558     * @param vh The view holder to be associated with the given action.
559     * @param action The guided action to be displayed by the view holder's view.
560     * @return The view to be added to the caller's view hierarchy.
561     */
562    public void onBindViewHolder(ViewHolder vh, GuidedAction action) {
563
564        vh.mAction = action;
565        if (vh.mTitleView != null) {
566            vh.mTitleView.setText(action.getTitle());
567            vh.mTitleView.setAlpha(action.isEnabled() ? mEnabledTextAlpha : mDisabledTextAlpha);
568            vh.mTitleView.setFocusable(action.isEditable());
569        }
570        if (vh.mDescriptionView != null) {
571            vh.mDescriptionView.setText(action.getDescription());
572            vh.mDescriptionView.setVisibility(TextUtils.isEmpty(action.getDescription()) ?
573                    View.GONE : View.VISIBLE);
574            vh.mDescriptionView.setAlpha(action.isEnabled() ? mEnabledDescriptionAlpha :
575                mDisabledDescriptionAlpha);
576            vh.mDescriptionView.setFocusable(action.isDescriptionEditable());
577        }
578        // Clients might want the check mark view to be gone entirely, in which case, ignore it.
579        if (vh.mCheckmarkView != null) {
580            onBindCheckMarkView(vh, action);
581        }
582
583        if (action.hasMultilineDescription()) {
584            if (vh.mTitleView != null) {
585                vh.mTitleView.setMaxLines(mTitleMaxLines);
586                if (vh.mDescriptionView != null) {
587                    vh.mDescriptionView.setMaxHeight(getDescriptionMaxHeight(
588                            vh.itemView.getContext(), vh.mTitleView));
589                }
590            }
591        } else {
592            if (vh.mTitleView != null) {
593                vh.mTitleView.setMaxLines(mTitleMinLines);
594            }
595            if (vh.mDescriptionView != null) {
596                vh.mDescriptionView.setMaxLines(mDescriptionMinLines);
597            }
598        }
599        setEditingMode(vh, action, false);
600        if (action.isFocusable()) {
601            vh.itemView.setFocusable(true);
602            ((ViewGroup) vh.itemView).setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
603        } else {
604            vh.itemView.setFocusable(false);
605            ((ViewGroup) vh.itemView).setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
606        }
607        setupImeOptions(vh, action);
608
609        updateChevronAndVisibility(vh);
610    }
611
612    /**
613     * Called by {@link #onBindViewHolder(ViewHolder, GuidedAction)} to setup IME options.  Default
614     * implementation assigns {@link EditorInfo#IME_ACTION_DONE}.  Subclass may override.
615     * @param vh The view holder to be associated with the given action.
616     * @param action The guided action to be displayed by the view holder's view.
617     */
618    protected void setupImeOptions(ViewHolder vh, GuidedAction action) {
619        setupNextImeOptions(vh.getEditableTitleView());
620        setupNextImeOptions(vh.getEditableDescriptionView());
621    }
622
623    private void setupNextImeOptions(EditText edit) {
624        if (edit != null) {
625            edit.setImeOptions(EditorInfo.IME_ACTION_NEXT);
626        }
627    }
628
629    public void setEditingMode(ViewHolder vh, GuidedAction action, boolean editing) {
630        if (editing != vh.mInEditing) {
631            vh.mInEditing = editing;
632            onEditingModeChange(vh, action, editing);
633        }
634    }
635
636    protected void onEditingModeChange(ViewHolder vh, GuidedAction action, boolean editing) {
637        action = vh.getAction();
638        TextView titleView = vh.getTitleView();
639        TextView descriptionView = vh.getDescriptionView();
640        if (editing) {
641            CharSequence editTitle = action.getEditTitle();
642            if (titleView != null && editTitle != null) {
643                titleView.setText(editTitle);
644            }
645            CharSequence editDescription = action.getEditDescription();
646            if (descriptionView != null && editDescription != null) {
647                descriptionView.setText(editDescription);
648            }
649            if (action.isDescriptionEditable()) {
650                if (descriptionView != null) {
651                    descriptionView.setVisibility(View.VISIBLE);
652                    descriptionView.setInputType(action.getDescriptionEditInputType());
653                }
654                vh.mInEditingDescription = true;
655            } else {
656                vh.mInEditingDescription = false;
657                if (titleView != null) {
658                    titleView.setInputType(action.getEditInputType());
659                }
660            }
661        } else {
662            if (titleView != null) {
663                titleView.setText(action.getTitle());
664            }
665            if (descriptionView != null) {
666                descriptionView.setText(action.getDescription());
667            }
668            if (vh.mInEditingDescription) {
669                if (descriptionView != null) {
670                    descriptionView.setVisibility(TextUtils.isEmpty(action.getDescription()) ?
671                            View.GONE : View.VISIBLE);
672                    descriptionView.setInputType(action.getDescriptionInputType());
673                }
674                vh.mInEditingDescription = false;
675            } else {
676                if (titleView != null) {
677                    titleView.setInputType(action.getInputType());
678                }
679            }
680        }
681    }
682
683    /**
684     * Animates the view holder's view (or subviews thereof) when the action has had its focus
685     * state changed.
686     * @param vh The view holder associated with the relevant action.
687     * @param focused True if the action has become focused, false if it has lost focus.
688     */
689    public void onAnimateItemFocused(ViewHolder vh, boolean focused) {
690        // No animations for this, currently, because the animation is done on
691        // mSelectorView
692    }
693
694    /**
695     * Animates the view holder's view (or subviews thereof) when the action has had its press
696     * state changed.
697     * @param vh The view holder associated with the relevant action.
698     * @param pressed True if the action has been pressed, false if it has been unpressed.
699     */
700    public void onAnimateItemPressed(ViewHolder vh, boolean pressed) {
701        int attr = pressed ? R.attr.guidedActionPressedAnimation :
702                R.attr.guidedActionUnpressedAnimation;
703        createAnimator(vh.itemView, attr).start();
704    }
705
706    /**
707     * Resets the view holder's view to unpressed state.
708     * @param vh The view holder associated with the relevant action.
709     */
710    public void onAnimateItemPressedCancelled(ViewHolder vh) {
711        createAnimator(vh.itemView, R.attr.guidedActionUnpressedAnimation).end();
712    }
713
714    /**
715     * Animates the view holder's view (or subviews thereof) when the action has had its check state
716     * changed. Default implementation calls setChecked() if {@link ViewHolder#getCheckmarkView()}
717     * is instance of {@link Checkable}.
718     *
719     * @param vh The view holder associated with the relevant action.
720     * @param checked True if the action has become checked, false if it has become unchecked.
721     * @see #onBindCheckMarkView(ViewHolder, GuidedAction)
722     */
723    public void onAnimateItemChecked(ViewHolder vh, boolean checked) {
724        if (vh.mCheckmarkView instanceof Checkable) {
725            ((Checkable) vh.mCheckmarkView).setChecked(checked);
726        }
727    }
728
729    /**
730     * Sets states of check mark view, called by {@link #onBindViewHolder(ViewHolder, GuidedAction)}
731     * when action's checkset Id is other than {@link GuidedAction#NO_CHECK_SET}. Default
732     * implementation assigns drawable loaded from theme attribute
733     * {@link android.R.attr#listChoiceIndicatorMultiple} for checkbox or
734     * {@link android.R.attr#listChoiceIndicatorSingle} for radio button. Subclass rarely needs
735     * override the method, instead app can provide its own drawable that supports transition
736     * animations, change theme attributes {@link android.R.attr#listChoiceIndicatorMultiple} and
737     * {@link android.R.attr#listChoiceIndicatorSingle} in {android.support.v17.leanback.R.
738     * styleable#LeanbackGuidedStepTheme}.
739     *
740     * @param vh The view holder associated with the relevant action.
741     * @param action The GuidedAction object to bind to.
742     * @see #onAnimateItemChecked(ViewHolder, boolean)
743     */
744    public void onBindCheckMarkView(ViewHolder vh, GuidedAction action) {
745        if (action.getCheckSetId() != GuidedAction.NO_CHECK_SET) {
746            vh.mCheckmarkView.setVisibility(View.VISIBLE);
747            int attrId = action.getCheckSetId() == GuidedAction.CHECKBOX_CHECK_SET_ID ?
748                    android.R.attr.listChoiceIndicatorMultiple :
749                    android.R.attr.listChoiceIndicatorSingle;
750            final Context context = vh.mCheckmarkView.getContext();
751            Drawable drawable = null;
752            TypedValue typedValue = new TypedValue();
753            if (context.getTheme().resolveAttribute(attrId, typedValue, true)) {
754                drawable = ContextCompat.getDrawable(context, typedValue.resourceId);
755            }
756            vh.mCheckmarkView.setImageDrawable(drawable);
757            if (vh.mCheckmarkView instanceof Checkable) {
758                ((Checkable) vh.mCheckmarkView).setChecked(action.isChecked());
759            }
760        } else {
761            vh.mCheckmarkView.setVisibility(View.GONE);
762        }
763    }
764
765    /**
766     * Sets states of chevron view, called by {@link #onBindViewHolder(ViewHolder, GuidedAction)}.
767     * Subclass may override.
768     *
769     * @param vh The view holder associated with the relevant action.
770     * @param action The GuidedAction object to bind to.
771     */
772    public void onBindChevronView(ViewHolder vh, GuidedAction action) {
773        final boolean hasNext = action.hasNext();
774        final boolean hasSubActions = action.hasSubActions();
775        if (hasNext || hasSubActions) {
776            vh.mChevronView.setVisibility(View.VISIBLE);
777            vh.mChevronView.setAlpha(action.isEnabled() ? mEnabledChevronAlpha :
778                    mDisabledChevronAlpha);
779            if (hasNext) {
780                vh.mChevronView.setRotation(0f);
781            } else if (action == mExpandedAction) {
782                vh.mChevronView.setRotation(270);
783            } else {
784                vh.mChevronView.setRotation(90);
785            }
786        } else {
787            vh.mChevronView.setVisibility(View.GONE);
788
789        }
790    }
791
792    /**
793     * Expands or collapse the sub actions list view.
794     * @param avh When not null, fill sub actions list of this ViewHolder into sub actions list and
795     * hide the other items in main list.  When null, collapse the sub actions list.
796     */
797    public void setExpandedViewHolder(ViewHolder avh) {
798        if (mSubActionsGridView == null) {
799            return;
800        }
801        if (avh == null) {
802            mExpandedAction = null;
803        } else if (avh.getAction() != mExpandedAction) {
804            mExpandedAction = avh.getAction();
805        }
806        updateAllChevronAndVisibility(avh);
807    }
808
809    /**
810     * @return True if sub actions list is expanded.
811     */
812    public boolean isSubActionsExpanded() {
813        return mExpandedAction != null;
814    }
815
816    /**
817     * @return Current expanded GuidedAction or null if not expanded.
818     */
819    public GuidedAction getExpandedAction() {
820        return mExpandedAction;
821    }
822
823    private void updateAllChevronAndVisibility(final ViewHolder avh) {
824        final int count = mActionsGridView.getChildCount();
825        for (int i = 0; i < count; i++) {
826            ViewHolder vh = (ViewHolder) mActionsGridView
827                    .getChildViewHolder(mActionsGridView.getChildAt(i));
828            updateChevronAndVisibility(vh);
829        }
830        if (mSubActionsGridView != null) {
831            updateExpandStatus(avh);
832        }
833    }
834
835    private void updateExpandStatus(ViewHolder avh) {
836        if (avh != null) {
837            mSubActionsGridView.setVisibility(View.VISIBLE);
838            mSubActionsGridView.requestFocus();
839            mSubActionsGridView.setSelectedPosition(0);
840            ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams)
841                    mActionsGridView.getLayoutParams();
842            lp.topMargin = - avh.itemView.getHeight();
843            lp.bottomMargin = avh.itemView.getHeight();
844            mActionsGridView.setLayoutParams(lp);
845            lp = (ViewGroup.MarginLayoutParams) mSubActionsGridView.getLayoutParams();
846            lp.topMargin = avh.itemView.getTop();
847            mSubActionsGridView.setLayoutParams(lp);
848            ((GuidedActionAdapter) mSubActionsGridView.getAdapter())
849                    .setActions(avh.getAction().getSubActions());
850        } else {
851            mSubActionsGridView.setVisibility(View.INVISIBLE);
852            ((GuidedActionAdapter) mSubActionsGridView.getAdapter())
853                    .setActions(Collections.EMPTY_LIST);
854            ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams)
855                    mActionsGridView.getLayoutParams();
856            lp.bottomMargin = lp.topMargin = 0;
857            mActionsGridView.setLayoutParams(lp);
858            mActionsGridView.requestFocus();
859        }
860    }
861
862    private void updateChevronAndVisibility(ViewHolder vh) {
863        if (!vh.isSubAction()) {
864            if (mExpandedAction == null || vh.getAction() == mExpandedAction) {
865                vh.itemView.setVisibility(View.VISIBLE);
866            } else {
867                vh.itemView.setVisibility(View.INVISIBLE);
868            }
869        }
870        if (vh.mChevronView != null) {
871            onBindChevronView(vh, vh.getAction());
872        }
873    }
874
875    /*
876     * ==========================================
877     * FragmentAnimationProvider overrides
878     * ==========================================
879     */
880
881    /**
882     * {@inheritDoc}
883     */
884    @Override
885    public void onImeAppearing(@NonNull List<Animator> animators) {
886        animators.add(createAnimator(mContentView, R.attr.guidedStepImeAppearingAnimation));
887    }
888
889    /**
890     * {@inheritDoc}
891     */
892    @Override
893    public void onImeDisappearing(@NonNull List<Animator> animators) {
894        animators.add(createAnimator(mContentView, R.attr.guidedStepImeDisappearingAnimation));
895    }
896
897    /*
898     * ==========================================
899     * Private methods
900     * ==========================================
901     */
902
903    private float getFloat(Context ctx, TypedValue typedValue, int attrId) {
904        ctx.getTheme().resolveAttribute(attrId, typedValue, true);
905        // Android resources don't have a native float type, so we have to use strings.
906        return Float.valueOf(ctx.getResources().getString(typedValue.resourceId));
907    }
908
909    private int getInteger(Context ctx, TypedValue typedValue, int attrId) {
910        ctx.getTheme().resolveAttribute(attrId, typedValue, true);
911        return ctx.getResources().getInteger(typedValue.resourceId);
912    }
913
914    private int getDimension(Context ctx, TypedValue typedValue, int attrId) {
915        ctx.getTheme().resolveAttribute(attrId, typedValue, true);
916        return ctx.getResources().getDimensionPixelSize(typedValue.resourceId);
917    }
918
919    private static Animator createAnimator(View v, int attrId) {
920        Context ctx = v.getContext();
921        TypedValue typedValue = new TypedValue();
922        ctx.getTheme().resolveAttribute(attrId, typedValue, true);
923        Animator animator = AnimatorInflater.loadAnimator(ctx, typedValue.resourceId);
924        animator.setTarget(v);
925        return animator;
926    }
927
928    private boolean setIcon(final ImageView iconView, GuidedAction action) {
929        Drawable icon = null;
930        if (iconView != null) {
931            Context context = iconView.getContext();
932            icon = action.getIcon();
933            if (icon != null) {
934                // setImageDrawable resets the drawable's level unless we set the view level first.
935                iconView.setImageLevel(icon.getLevel());
936                iconView.setImageDrawable(icon);
937                iconView.setVisibility(View.VISIBLE);
938            } else {
939                iconView.setVisibility(View.GONE);
940            }
941        }
942        return icon != null;
943    }
944
945    /**
946     * @return the max height in pixels the description can be such that the
947     *         action nicely takes up the entire screen.
948     */
949    private int getDescriptionMaxHeight(Context context, TextView title) {
950        // The 2 multiplier on the title height calculation is a
951        // conservative estimate for font padding which can not be
952        // calculated at this stage since the view hasn't been rendered yet.
953        return (int)(mDisplayHeight - 2*mVerticalPadding - 2*mTitleMaxLines*title.getLineHeight());
954    }
955
956    private void endSelectorAnimator() {
957        if (mSelectorAnimator != null) {
958            mSelectorAnimator.end();
959            mSelectorAnimator = null;
960        }
961    }
962
963    private void updateSelectorView(boolean animate) {
964        if ((mActionsGridView == null && mSubActionsGridView == null)
965                || mSelectorView == null || mSelectorView.getHeight() <= 0) {
966            return;
967        }
968        RecyclerView actionsGridView = null;
969        View focusedChild = mActionsGridView.getFocusedChild();
970        if (focusedChild == null && mSubActionsGridView != null) {
971            focusedChild = mSubActionsGridView.getFocusedChild();
972            if (focusedChild != null) {
973                actionsGridView = mSubActionsGridView;
974            }
975        } else {
976            actionsGridView = mActionsGridView;
977        }
978        endSelectorAnimator();
979        if (focusedChild == null || !actionsGridView.hasFocus()
980                || actionsGridView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) {
981            if (animate) {
982                mSelectorAnimator = createAnimator(mSelectorView,
983                        R.attr.guidedActionsSelectorHideAnimation);
984                mSelectorAnimator.start();
985            } else {
986                mSelectorView.setAlpha(0f);
987            }
988        } else {
989            final float scaleY = (float) focusedChild.getHeight() / mSelectorView.getHeight();
990            Rect r = new Rect(0, 0, focusedChild.getWidth(), focusedChild.getHeight());
991            mMainView.offsetDescendantRectToMyCoords(focusedChild, r);
992            mMainView.offsetRectIntoDescendantCoords(mSelectorView, r);
993            mSelectorView.setTranslationY(r.exactCenterY() - mSelectorView.getHeight() * 0.5f);
994            if (animate) {
995                mSelectorAnimator = createAnimator(mSelectorView,
996                        R.attr.guidedActionsSelectorShowAnimation);
997                ((ObjectAnimator) ((AnimatorSet) mSelectorAnimator).getChildAnimations().get(1))
998                        .setFloatValues(scaleY);
999                mSelectorAnimator.start();
1000            } else {
1001                mSelectorView.setAlpha(1f);
1002                mSelectorView.setScaleY(scaleY);
1003            }
1004        }
1005    }
1006
1007}
1008