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