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