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