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