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 mBgView;
369    private View mContentView;
370    private boolean mButtonActions;
371
372    // Cached values from resources
373    private float mEnabledTextAlpha;
374    private float mDisabledTextAlpha;
375    private float mEnabledDescriptionAlpha;
376    private float mDisabledDescriptionAlpha;
377    private float mEnabledChevronAlpha;
378    private float mDisabledChevronAlpha;
379    private int mTitleMinLines;
380    private int mTitleMaxLines;
381    private int mDescriptionMinLines;
382    private int mVerticalPadding;
383    private int mDisplayHeight;
384
385    private EditListener mEditListener;
386
387    private GuidedAction mExpandedAction = null;
388    private Object mExpandTransition;
389
390    /**
391     * Creates a view appropriate for displaying a list of GuidedActions, using the provided
392     * inflater and container.
393     * <p>
394     * <i>Note: Does not actually add the created view to the container; the caller should do
395     * this.</i>
396     * @param inflater The layout inflater to be used when constructing the view.
397     * @param container The view group to be passed in the call to
398     * <code>LayoutInflater.inflate</code>.
399     * @return The view to be added to the caller's view hierarchy.
400     */
401    public View onCreateView(LayoutInflater inflater, final ViewGroup container) {
402        TypedArray ta = inflater.getContext().getTheme().obtainStyledAttributes(
403                R.styleable.LeanbackGuidedStepTheme);
404        float keylinePercent = ta.getFloat(R.styleable.LeanbackGuidedStepTheme_guidedStepKeyline,
405                40);
406        mMainView = (ViewGroup) inflater.inflate(onProvideLayoutId(), container, false);
407        mContentView = mMainView.findViewById(mButtonActions ? R.id.guidedactions_content2 :
408                R.id.guidedactions_content);
409        mBgView = mMainView.findViewById(mButtonActions ? R.id.guidedactions_list_background2 :
410                R.id.guidedactions_list_background);
411        if (mMainView instanceof VerticalGridView) {
412            mActionsGridView = (VerticalGridView) mMainView;
413        } else {
414            mActionsGridView = (VerticalGridView) mMainView.findViewById(mButtonActions ?
415                    R.id.guidedactions_list2 : R.id.guidedactions_list);
416            if (mActionsGridView == null) {
417                throw new IllegalStateException("No ListView exists.");
418            }
419            mActionsGridView.setWindowAlignmentOffsetPercent(keylinePercent);
420            mActionsGridView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE);
421            if (!mButtonActions) {
422                mSubActionsGridView = (VerticalGridView) mMainView.findViewById(
423                        R.id.guidedactions_sub_list);
424            }
425        }
426        mActionsGridView.setFocusable(false);
427        mActionsGridView.setFocusableInTouchMode(false);
428
429        // Cache widths, chevron alpha values, max and min text lines, etc
430        Context ctx = mMainView.getContext();
431        TypedValue val = new TypedValue();
432        mEnabledChevronAlpha = getFloat(ctx, val, R.attr.guidedActionEnabledChevronAlpha);
433        mDisabledChevronAlpha = getFloat(ctx, val, R.attr.guidedActionDisabledChevronAlpha);
434        mTitleMinLines = getInteger(ctx, val, R.attr.guidedActionTitleMinLines);
435        mTitleMaxLines = getInteger(ctx, val, R.attr.guidedActionTitleMaxLines);
436        mDescriptionMinLines = getInteger(ctx, val, R.attr.guidedActionDescriptionMinLines);
437        mVerticalPadding = getDimension(ctx, val, R.attr.guidedActionVerticalPadding);
438        mDisplayHeight = ((WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE))
439                .getDefaultDisplay().getHeight();
440
441        mEnabledTextAlpha = Float.valueOf(ctx.getResources().getString(R.string
442                .lb_guidedactions_item_unselected_text_alpha));
443        mDisabledTextAlpha = Float.valueOf(ctx.getResources().getString(R.string
444                .lb_guidedactions_item_disabled_text_alpha));
445        mEnabledDescriptionAlpha = Float.valueOf(ctx.getResources().getString(R.string
446                .lb_guidedactions_item_unselected_description_text_alpha));
447        mDisabledDescriptionAlpha = Float.valueOf(ctx.getResources().getString(R.string
448                .lb_guidedactions_item_disabled_description_text_alpha));
449        return mMainView;
450    }
451
452    /**
453     * Choose the layout resource for button actions in {@link #onProvideLayoutId()}.
454     */
455    public void setAsButtonActions() {
456        if (mMainView != null) {
457            throw new IllegalStateException("setAsButtonActions() must be called before creating "
458                    + "views");
459        }
460        mButtonActions = true;
461    }
462
463    /**
464     * Returns true if it is button actions list, false for normal actions list.
465     * @return True if it is button actions list, false for normal actions list.
466     */
467    public boolean isButtonActions() {
468        return mButtonActions;
469    }
470
471    /**
472     * Called when destroy the View created by GuidedActionsStylist.
473     */
474    public void onDestroyView() {
475        mExpandedAction = null;
476        mExpandTransition = null;
477        mActionsGridView = null;
478        mSubActionsGridView = null;
479        mContentView = null;
480        mBgView = null;
481        mMainView = null;
482    }
483
484    /**
485     * Returns the VerticalGridView that displays the list of GuidedActions.
486     * @return The VerticalGridView for this presenter.
487     */
488    public VerticalGridView getActionsGridView() {
489        return mActionsGridView;
490    }
491
492    /**
493     * Returns the VerticalGridView that displays the sub actions list of an expanded action.
494     * @return The VerticalGridView that displays the sub actions list of an expanded action.
495     */
496    public VerticalGridView getSubActionsGridView() {
497        return mSubActionsGridView;
498    }
499
500    /**
501     * Provides the resource ID of the layout defining the host view for the list of guided actions.
502     * Subclasses may override to provide their own customized layouts. The base implementation
503     * returns {@link android.support.v17.leanback.R.layout#lb_guidedactions} or
504     * {@link android.support.v17.leanback.R.layout#lb_guidedbuttonactions} if
505     * {@link #isButtonActions()} is true. If overridden, the substituted layout should contain
506     * matching IDs for any views that should be managed by the base class; this can be achieved by
507     * starting with a copy of the base layout file.
508     *
509     * @return The resource ID of the layout to be inflated to define the host view for the list of
510     *         GuidedActions.
511     */
512    public int onProvideLayoutId() {
513        return mButtonActions ? R.layout.lb_guidedbuttonactions : R.layout.lb_guidedactions;
514    }
515
516    /**
517     * Return view type of action, each different type can have differently associated layout Id.
518     * Default implementation returns {@link #VIEW_TYPE_DEFAULT}.
519     * @param action  The action object.
520     * @return View type that used in {@link #onProvideItemLayoutId(int)}.
521     */
522    public int getItemViewType(GuidedAction action) {
523        if (action instanceof GuidedDatePickerAction) {
524            return VIEW_TYPE_DATE_PICKER;
525        }
526        return VIEW_TYPE_DEFAULT;
527    }
528
529    /**
530     * Provides the resource ID of the layout defining the view for an individual guided actions.
531     * Subclasses may override to provide their own customized layouts. The base implementation
532     * returns {@link android.support.v17.leanback.R.layout#lb_guidedactions_item}. If overridden,
533     * the substituted layout should contain matching IDs for any views that should be managed by
534     * the base class; this can be achieved by starting with a copy of the base layout file. Note
535     * that in order for the item to support editing, the title view should both subclass {@link
536     * android.widget.EditText} and implement {@link ImeKeyMonitor}; see {@link
537     * GuidedActionEditText}.  To support different types of Layouts, override {@link
538     * #onProvideItemLayoutId(int)}.
539     * @return The resource ID of the layout to be inflated to define the view to display an
540     * individual GuidedAction.
541     */
542    public int onProvideItemLayoutId() {
543        return R.layout.lb_guidedactions_item;
544    }
545
546    /**
547     * Provides the resource ID of the layout defining the view for an individual guided actions.
548     * Subclasses may override to provide their own customized layouts. The base implementation
549     * supports:
550     * <li>{@link android.support.v17.leanback.R.layout#lb_guidedactions_item}
551     * <li>{{@link android.support.v17.leanback.R.layout#lb_guidedactions_datepicker_item}. If
552     * overridden, the substituted layout should contain matching IDs for any views that should be
553     * managed by the base class; this can be achieved by starting with a copy of the base layout
554     * file. Note that in order for the item to support editing, the title view should both subclass
555     * {@link android.widget.EditText} and implement {@link ImeKeyMonitor}; see
556     * {@link GuidedActionEditText}.
557     *
558     * @param viewType View type returned by {@link #getItemViewType(GuidedAction)}
559     * @return The resource ID of the layout to be inflated to define the view to display an
560     *         individual GuidedAction.
561     */
562    public int onProvideItemLayoutId(int viewType) {
563        if (viewType == VIEW_TYPE_DEFAULT) {
564            return onProvideItemLayoutId();
565        } else if (viewType == VIEW_TYPE_DATE_PICKER) {
566            return R.layout.lb_guidedactions_datepicker_item;
567        } else {
568            throw new RuntimeException("ViewType " + viewType +
569                    " not supported in GuidedActionsStylist");
570        }
571    }
572
573    /**
574     * Constructs a {@link ViewHolder} capable of representing {@link GuidedAction}s. Subclasses
575     * may choose to return a subclass of ViewHolder.  To support different view types, override
576     * {@link #onCreateViewHolder(ViewGroup, int)}
577     * <p>
578     * <i>Note: Should not actually add the created view to the parent; the caller will do
579     * this.</i>
580     * @param parent The view group to be used as the parent of the new view.
581     * @return The view to be added to the caller's view hierarchy.
582     */
583    public ViewHolder onCreateViewHolder(ViewGroup parent) {
584        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
585        View v = inflater.inflate(onProvideItemLayoutId(), parent, false);
586        return new ViewHolder(v, parent == mSubActionsGridView);
587    }
588
589    /**
590     * Constructs a {@link ViewHolder} capable of representing {@link GuidedAction}s. Subclasses
591     * may choose to return a subclass of ViewHolder.
592     * <p>
593     * <i>Note: Should not actually add the created view to the parent; the caller will do
594     * this.</i>
595     * @param parent The view group to be used as the parent of the new view.
596     * @param viewType The viewType returned by {@link #getItemViewType(GuidedAction)}
597     * @return The view to be added to the caller's view hierarchy.
598     */
599    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
600        if (viewType == VIEW_TYPE_DEFAULT) {
601            return onCreateViewHolder(parent);
602        }
603        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
604        View v = inflater.inflate(onProvideItemLayoutId(viewType), parent, false);
605        return new ViewHolder(v, parent == mSubActionsGridView);
606    }
607
608    /**
609     * Binds a {@link ViewHolder} to a particular {@link GuidedAction}.
610     * @param vh The view holder to be associated with the given action.
611     * @param action The guided action to be displayed by the view holder's view.
612     * @return The view to be added to the caller's view hierarchy.
613     */
614    public void onBindViewHolder(ViewHolder vh, GuidedAction action) {
615
616        vh.mAction = action;
617        if (vh.mTitleView != null) {
618            vh.mTitleView.setText(action.getTitle());
619            vh.mTitleView.setAlpha(action.isEnabled() ? mEnabledTextAlpha : mDisabledTextAlpha);
620            vh.mTitleView.setFocusable(false);
621            vh.mTitleView.setClickable(false);
622            vh.mTitleView.setLongClickable(false);
623        }
624        if (vh.mDescriptionView != null) {
625            vh.mDescriptionView.setText(action.getDescription());
626            vh.mDescriptionView.setVisibility(TextUtils.isEmpty(action.getDescription()) ?
627                    View.GONE : View.VISIBLE);
628            vh.mDescriptionView.setAlpha(action.isEnabled() ? mEnabledDescriptionAlpha :
629                mDisabledDescriptionAlpha);
630            vh.mDescriptionView.setFocusable(false);
631            vh.mDescriptionView.setClickable(false);
632            vh.mDescriptionView.setLongClickable(false);
633        }
634        // Clients might want the check mark view to be gone entirely, in which case, ignore it.
635        if (vh.mCheckmarkView != null) {
636            onBindCheckMarkView(vh, action);
637        }
638        setIcon(vh.mIconView, action);
639
640        if (action.hasMultilineDescription()) {
641            if (vh.mTitleView != null) {
642                setMaxLines(vh.mTitleView, mTitleMaxLines);
643                if (vh.mDescriptionView != null) {
644                    vh.mDescriptionView.setMaxHeight(getDescriptionMaxHeight(
645                            vh.itemView.getContext(), vh.mTitleView));
646                }
647            }
648        } else {
649            if (vh.mTitleView != null) {
650                setMaxLines(vh.mTitleView, mTitleMinLines);
651            }
652            if (vh.mDescriptionView != null) {
653                setMaxLines(vh.mDescriptionView, mDescriptionMinLines);
654            }
655        }
656        if (vh.mActivatorView != null) {
657            onBindActivatorView(vh, action);
658        }
659        setEditingMode(vh, action, false);
660        if (action.isFocusable()) {
661            vh.itemView.setFocusable(true);
662            ((ViewGroup) vh.itemView).setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
663        } else {
664            vh.itemView.setFocusable(false);
665            ((ViewGroup) vh.itemView).setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
666        }
667        setupImeOptions(vh, action);
668
669        updateChevronAndVisibility(vh);
670    }
671
672    private static void setMaxLines(TextView view, int maxLines) {
673        // setSingleLine must be called before setMaxLines because it resets maximum to
674        // Integer.MAX_VALUE.
675        if (maxLines == 1) {
676            view.setSingleLine(true);
677        } else {
678            view.setSingleLine(false);
679            view.setMaxLines(maxLines);
680        }
681    }
682
683    /**
684     * Called by {@link #onBindViewHolder(ViewHolder, GuidedAction)} to setup IME options.  Default
685     * implementation assigns {@link EditorInfo#IME_ACTION_DONE}.  Subclass may override.
686     * @param vh The view holder to be associated with the given action.
687     * @param action The guided action to be displayed by the view holder's view.
688     */
689    protected void setupImeOptions(ViewHolder vh, GuidedAction action) {
690        setupNextImeOptions(vh.getEditableTitleView());
691        setupNextImeOptions(vh.getEditableDescriptionView());
692    }
693
694    private void setupNextImeOptions(EditText edit) {
695        if (edit != null) {
696            edit.setImeOptions(EditorInfo.IME_ACTION_NEXT);
697        }
698    }
699
700    public void setEditingMode(ViewHolder vh, GuidedAction action, boolean editing) {
701        if (editing != vh.isInEditing() && !isInExpandTransition()) {
702            onEditingModeChange(vh, action, editing);
703        }
704    }
705
706    protected void onEditingModeChange(ViewHolder vh, GuidedAction action, boolean editing) {
707        action = vh.getAction();
708        TextView titleView = vh.getTitleView();
709        TextView descriptionView = vh.getDescriptionView();
710        if (editing) {
711            CharSequence editTitle = action.getEditTitle();
712            if (titleView != null && editTitle != null) {
713                titleView.setText(editTitle);
714            }
715            CharSequence editDescription = action.getEditDescription();
716            if (descriptionView != null && editDescription != null) {
717                descriptionView.setText(editDescription);
718            }
719            if (action.isDescriptionEditable()) {
720                if (descriptionView != null) {
721                    descriptionView.setVisibility(View.VISIBLE);
722                    descriptionView.setInputType(action.getDescriptionEditInputType());
723                }
724                vh.mEditingMode = EDITING_DESCRIPTION;
725            } else if (action.isEditable()){
726                if (titleView != null) {
727                    titleView.setInputType(action.getEditInputType());
728                }
729                vh.mEditingMode = EDITING_TITLE;
730            } else if (vh.mActivatorView != null) {
731                onEditActivatorView(vh, action, editing);
732                vh.mEditingMode = EDITING_ACTIVATOR_VIEW;
733            }
734        } else {
735            if (titleView != null) {
736                titleView.setText(action.getTitle());
737            }
738            if (descriptionView != null) {
739                descriptionView.setText(action.getDescription());
740            }
741            if (vh.mEditingMode == EDITING_DESCRIPTION) {
742                if (descriptionView != null) {
743                    descriptionView.setVisibility(TextUtils.isEmpty(action.getDescription()) ?
744                            View.GONE : View.VISIBLE);
745                    descriptionView.setInputType(action.getDescriptionInputType());
746                }
747            } else if (vh.mEditingMode == EDITING_TITLE) {
748                if (titleView != null) {
749                    titleView.setInputType(action.getInputType());
750                }
751            } else if (vh.mEditingMode == EDITING_ACTIVATOR_VIEW) {
752                if (vh.mActivatorView != null) {
753                    onEditActivatorView(vh, action, editing);
754                }
755            }
756            vh.mEditingMode = EDITING_NONE;
757        }
758    }
759
760    /**
761     * Animates the view holder's view (or subviews thereof) when the action has had its focus
762     * state changed.
763     * @param vh The view holder associated with the relevant action.
764     * @param focused True if the action has become focused, false if it has lost focus.
765     */
766    public void onAnimateItemFocused(ViewHolder vh, boolean focused) {
767        // No animations for this, currently, because the animation is done on
768        // mSelectorView
769    }
770
771    /**
772     * Animates the view holder's view (or subviews thereof) when the action has had its press
773     * state changed.
774     * @param vh The view holder associated with the relevant action.
775     * @param pressed True if the action has been pressed, false if it has been unpressed.
776     */
777    public void onAnimateItemPressed(ViewHolder vh, boolean pressed) {
778        int attr = pressed ? R.attr.guidedActionPressedAnimation :
779                R.attr.guidedActionUnpressedAnimation;
780        createAnimator(vh.itemView, attr).start();
781    }
782
783    /**
784     * Resets the view holder's view to unpressed state.
785     * @param vh The view holder associated with the relevant action.
786     */
787    public void onAnimateItemPressedCancelled(ViewHolder vh) {
788        createAnimator(vh.itemView, R.attr.guidedActionUnpressedAnimation).end();
789    }
790
791    /**
792     * Animates the view holder's view (or subviews thereof) when the action has had its check state
793     * changed. Default implementation calls setChecked() if {@link ViewHolder#getCheckmarkView()}
794     * is instance of {@link Checkable}.
795     *
796     * @param vh The view holder associated with the relevant action.
797     * @param checked True if the action has become checked, false if it has become unchecked.
798     * @see #onBindCheckMarkView(ViewHolder, GuidedAction)
799     */
800    public void onAnimateItemChecked(ViewHolder vh, boolean checked) {
801        if (vh.mCheckmarkView instanceof Checkable) {
802            ((Checkable) vh.mCheckmarkView).setChecked(checked);
803        }
804    }
805
806    /**
807     * Sets states of check mark view, called by {@link #onBindViewHolder(ViewHolder, GuidedAction)}
808     * when action's checkset Id is other than {@link GuidedAction#NO_CHECK_SET}. Default
809     * implementation assigns drawable loaded from theme attribute
810     * {@link android.R.attr#listChoiceIndicatorMultiple} for checkbox or
811     * {@link android.R.attr#listChoiceIndicatorSingle} for radio button. Subclass rarely needs
812     * override the method, instead app can provide its own drawable that supports transition
813     * animations, change theme attributes {@link android.R.attr#listChoiceIndicatorMultiple} and
814     * {@link android.R.attr#listChoiceIndicatorSingle} in {android.support.v17.leanback.R.
815     * styleable#LeanbackGuidedStepTheme}.
816     *
817     * @param vh The view holder associated with the relevant action.
818     * @param action The GuidedAction object to bind to.
819     * @see #onAnimateItemChecked(ViewHolder, boolean)
820     */
821    public void onBindCheckMarkView(ViewHolder vh, GuidedAction action) {
822        if (action.getCheckSetId() != GuidedAction.NO_CHECK_SET) {
823            vh.mCheckmarkView.setVisibility(View.VISIBLE);
824            int attrId = action.getCheckSetId() == GuidedAction.CHECKBOX_CHECK_SET_ID ?
825                    android.R.attr.listChoiceIndicatorMultiple :
826                    android.R.attr.listChoiceIndicatorSingle;
827            final Context context = vh.mCheckmarkView.getContext();
828            Drawable drawable = null;
829            TypedValue typedValue = new TypedValue();
830            if (context.getTheme().resolveAttribute(attrId, typedValue, true)) {
831                drawable = ContextCompat.getDrawable(context, typedValue.resourceId);
832            }
833            vh.mCheckmarkView.setImageDrawable(drawable);
834            if (vh.mCheckmarkView instanceof Checkable) {
835                ((Checkable) vh.mCheckmarkView).setChecked(action.isChecked());
836            }
837        } else {
838            vh.mCheckmarkView.setVisibility(View.GONE);
839        }
840    }
841
842    /**
843     * Performs binding activator view value to action.  Default implementation supports
844     * GuidedDatePickerAction, subclass may override to add support of other views.
845     * @param vh ViewHolder of activator view.
846     * @param action GuidedAction to bind.
847     */
848    public void onBindActivatorView(ViewHolder vh, GuidedAction action) {
849        if (action instanceof GuidedDatePickerAction) {
850            GuidedDatePickerAction dateAction = (GuidedDatePickerAction) action;
851            DatePicker dateView = (DatePicker) vh.mActivatorView;
852            dateView.setDatePickerFormat(dateAction.getDatePickerFormat());
853            if (dateAction.getMinDate() != Long.MIN_VALUE) {
854                dateView.setMinDate(dateAction.getMinDate());
855            }
856            if (dateAction.getMaxDate() != Long.MAX_VALUE) {
857                dateView.setMaxDate(dateAction.getMaxDate());
858            }
859            Calendar c = Calendar.getInstance();
860            c.setTimeInMillis(dateAction.getDate());
861            dateView.updateDate(c.get(Calendar.YEAR), c.get(Calendar.MONTH),
862                    c.get(Calendar.DAY_OF_MONTH), false);
863        }
864    }
865
866    /**
867     * Performs updating GuidedAction from activator view.  Default implementation supports
868     * GuidedDatePickerAction, subclass may override to add support of other views.
869     * @param vh ViewHolder of activator view.
870     * @param action GuidedAction to update.
871     * @return True if value has been updated, false otherwise.
872     */
873    public boolean onUpdateActivatorView(ViewHolder vh, GuidedAction action) {
874        if (action instanceof GuidedDatePickerAction) {
875            GuidedDatePickerAction dateAction = (GuidedDatePickerAction) action;
876            DatePicker dateView = (DatePicker) vh.mActivatorView;
877            if (dateAction.getDate() != dateView.getDate()) {
878                dateAction.setDate(dateView.getDate());
879                return true;
880            }
881        }
882        return false;
883    }
884
885    /**
886     * Sets listener for reporting view being edited.
887     * @hide
888     */
889    public void setEditListener(EditListener listener) {
890        mEditListener = listener;
891    }
892
893    void onEditActivatorView(final ViewHolder vh, final GuidedAction action,
894            boolean editing) {
895        if (editing) {
896            vh.itemView.setFocusable(false);
897            vh.mActivatorView.requestFocus();
898            setExpandedViewHolder(vh);
899            vh.mActivatorView.setOnClickListener(new View.OnClickListener() {
900                @Override
901                public void onClick(View v) {
902                    if (!isInExpandTransition()) {
903                        setEditingMode(vh, action, false);
904                    }
905                }
906            });
907        } else {
908            if (onUpdateActivatorView(vh, action)) {
909                if (mEditListener != null) {
910                    mEditListener.onGuidedActionEditedAndProceed(action);
911                }
912            }
913            vh.itemView.setFocusable(true);
914            vh.itemView.requestFocus();
915            setExpandedViewHolder(null);
916            vh.mActivatorView.setOnClickListener(null);
917            vh.mActivatorView.setClickable(false);
918        }
919    }
920
921    /**
922     * Sets states of chevron view, called by {@link #onBindViewHolder(ViewHolder, GuidedAction)}.
923     * Subclass may override.
924     *
925     * @param vh The view holder associated with the relevant action.
926     * @param action The GuidedAction object to bind to.
927     */
928    public void onBindChevronView(ViewHolder vh, GuidedAction action) {
929        final boolean hasNext = action.hasNext();
930        final boolean hasSubActions = action.hasSubActions();
931        if (hasNext || hasSubActions) {
932            vh.mChevronView.setVisibility(View.VISIBLE);
933            vh.mChevronView.setAlpha(action.isEnabled() ? mEnabledChevronAlpha :
934                    mDisabledChevronAlpha);
935            if (hasNext) {
936                float r = mMainView != null
937                        && mMainView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL ? 180f : 0f;
938                vh.mChevronView.setRotation(r);
939            } else if (action == mExpandedAction) {
940                vh.mChevronView.setRotation(270);
941            } else {
942                vh.mChevronView.setRotation(90);
943            }
944        } else {
945            vh.mChevronView.setVisibility(View.GONE);
946
947        }
948    }
949
950    /**
951     * Expands or collapse the sub actions list view.
952     * @param avh When not null, fill sub actions list of this ViewHolder into sub actions list and
953     * hide the other items in main list.  When null, collapse the sub actions list.
954     */
955    public void setExpandedViewHolder(ViewHolder avh) {
956        if (isInExpandTransition()) {
957            return;
958        }
959        if (isExpandTransitionSupported()) {
960            startExpandedTransition(avh);
961        } else {
962            onUpdateExpandedViewHolder(avh);
963        }
964    }
965
966    /**
967     * Returns true if it is running an expanding or collapsing transition, false otherwise.
968     * @return True if it is running an expanding or collapsing transition, false otherwise.
969     */
970    public boolean isInExpandTransition() {
971        return mExpandTransition != null;
972    }
973
974    /**
975     * Returns if expand/collapse animation is supported.  When this method returns true,
976     * {@link #startExpandedTransition(ViewHolder)} will be used.  When this method returns false,
977     * {@link #onUpdateExpandedViewHolder(ViewHolder)} will be called.
978     * @return True if it is running an expanding or collapsing transition, false otherwise.
979     */
980    public boolean isExpandTransitionSupported() {
981        return VERSION.SDK_INT >= 21;
982    }
983
984    /**
985     * Start transition to expand or collapse GuidedActionStylist.
986     * @param avh When not null, the GuidedActionStylist expands the sub actions of avh.  When null
987     * the GuidedActionStylist will collapse sub actions.
988     */
989    public void startExpandedTransition(ViewHolder avh) {
990        ViewHolder focusAvh = null; // expand / collapse view holder
991        final int count = mActionsGridView.getChildCount();
992        for (int i = 0; i < count; i++) {
993            ViewHolder vh = (ViewHolder) mActionsGridView
994                    .getChildViewHolder(mActionsGridView.getChildAt(i));
995            if (avh == null && vh.itemView.getVisibility() == View.VISIBLE) {
996                // going to collapse this one.
997                focusAvh = vh;
998                break;
999            } else if (avh != null && vh.getAction() == avh.getAction()) {
1000                // going to expand this one.
1001                focusAvh = vh;
1002                break;
1003            }
1004        }
1005        if (focusAvh == null) {
1006            // huh?
1007            onUpdateExpandedViewHolder(avh);
1008            return;
1009        }
1010        boolean isSubActionTransition = focusAvh.getAction().hasSubActions();
1011        Object set = TransitionHelper.createTransitionSet(false);
1012        float slideDistance = isSubActionTransition ? focusAvh.itemView.getHeight() :
1013                focusAvh.itemView.getHeight() * 0.5f;
1014        Object slideAndFade = TransitionHelper.createFadeAndShortSlide(Gravity.TOP | Gravity.BOTTOM,
1015                slideDistance);
1016        Object changeFocusItemTransform = TransitionHelper.createChangeTransform();
1017        Object changeFocusItemBounds = TransitionHelper.createChangeBounds(false);
1018        Object fade = TransitionHelper.createFadeTransition(TransitionHelper.FADE_IN |
1019                TransitionHelper.FADE_OUT);
1020        Object changeGridBounds = TransitionHelper.createChangeBounds(false);
1021        if (avh == null) {
1022            TransitionHelper.setStartDelay(slideAndFade, 150);
1023            TransitionHelper.setStartDelay(changeFocusItemTransform, 100);
1024            TransitionHelper.setStartDelay(changeFocusItemBounds, 100);
1025        } else {
1026            TransitionHelper.setStartDelay(fade, 100);
1027            TransitionHelper.setStartDelay(changeGridBounds, 100);
1028            TransitionHelper.setStartDelay(changeFocusItemTransform, 50);
1029            TransitionHelper.setStartDelay(changeFocusItemBounds, 50);
1030        }
1031        for (int i = 0; i < count; i++) {
1032            ViewHolder vh = (ViewHolder) mActionsGridView
1033                    .getChildViewHolder(mActionsGridView.getChildAt(i));
1034            if (vh == focusAvh) {
1035                // going to expand/collapse this one.
1036                if (isSubActionTransition) {
1037                    TransitionHelper.include(changeFocusItemTransform, vh.itemView);
1038                    TransitionHelper.include(changeFocusItemBounds, vh.itemView);
1039                }
1040            } else {
1041                // going to slide this item to top / bottom.
1042                TransitionHelper.include(slideAndFade, vh.itemView);
1043                TransitionHelper.exclude(fade, vh.itemView, true);
1044            }
1045        }
1046        TransitionHelper.include(changeGridBounds, mSubActionsGridView);
1047        TransitionHelper.addTransition(set, slideAndFade);
1048        // note that we don't run ChangeBounds for activating view due to the rounding problem
1049        // of multiple level views ChangeBounds animation causing vertical jittering.
1050        if (isSubActionTransition) {
1051            TransitionHelper.addTransition(set, changeFocusItemTransform);
1052            TransitionHelper.addTransition(set, changeFocusItemBounds);
1053        }
1054        TransitionHelper.addTransition(set, fade);
1055        TransitionHelper.addTransition(set, changeGridBounds);
1056        mExpandTransition = set;
1057        TransitionHelper.addTransitionListener(mExpandTransition, new TransitionListener() {
1058            @Override
1059            public void onTransitionEnd(Object transition) {
1060                mExpandTransition = null;
1061            }
1062        });
1063        if (avh != null && mSubActionsGridView.getTop() != avh.itemView.getTop()) {
1064            // For expanding, set the initial position of subActionsGridView before running
1065            // a ChangeBounds on it.
1066            final ViewHolder toUpdate = avh;
1067            mSubActionsGridView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
1068                @Override
1069                public void onLayoutChange(View v, int left, int top, int right, int bottom,
1070                        int oldLeft, int oldTop, int oldRight, int oldBottom) {
1071                    if (mSubActionsGridView == null) {
1072                        return;
1073                    }
1074                    mSubActionsGridView.removeOnLayoutChangeListener(this);
1075                    mMainView.post(new Runnable() {
1076                        public void run() {
1077                            if (mMainView == null) {
1078                                return;
1079                            }
1080                            TransitionHelper.beginDelayedTransition(mMainView, mExpandTransition);
1081                            onUpdateExpandedViewHolder(toUpdate);
1082                        }
1083                    });
1084                }
1085            });
1086            ViewGroup.MarginLayoutParams lp =
1087                    (ViewGroup.MarginLayoutParams) mSubActionsGridView.getLayoutParams();
1088            lp.topMargin = avh.itemView.getTop();
1089            lp.height = 0;
1090            mSubActionsGridView.setLayoutParams(lp);
1091            return;
1092        }
1093        TransitionHelper.beginDelayedTransition(mMainView, mExpandTransition);
1094        onUpdateExpandedViewHolder(avh);
1095    }
1096
1097    /**
1098     * @return True if sub actions list is expanded.
1099     */
1100    public boolean isSubActionsExpanded() {
1101        return mExpandedAction != null;
1102    }
1103
1104    /**
1105     * @return Current expanded GuidedAction or null if not expanded.
1106     */
1107    public GuidedAction getExpandedAction() {
1108        return mExpandedAction;
1109    }
1110
1111    /**
1112     * Expand or collapse GuidedActionStylist.
1113     * @param avh When not null, the GuidedActionStylist expands the sub actions of avh.  When null
1114     * the GuidedActionStylist will collapse sub actions.
1115     */
1116    public void onUpdateExpandedViewHolder(ViewHolder avh) {
1117
1118        // Note about setting the prune child flag back & forth here: without this, the actions that
1119        // go off the screen from the top or bottom become invisible forever. This is because once
1120        // an action is expanded, it takes more space which in turn kicks out some other actions
1121        // off of the screen. Once, this action is collapsed (after the second click) and the
1122        // visibility flag is set back to true for all existing actions,
1123        // the off-the-screen actions are pruned from the view, thus
1124        // could not be accessed, had we not disabled pruning prior to this.
1125        if (avh == null) {
1126            mExpandedAction = null;
1127            mActionsGridView.setPruneChild(true);
1128        } else if (avh.getAction() != mExpandedAction) {
1129            mExpandedAction = avh.getAction();
1130            mActionsGridView.setPruneChild(false);
1131        }
1132        // In expanding mode, notifyItemChange on expanded item will reset the translationY by
1133        // the default ItemAnimator.  So disable ItemAnimation in expanding mode.
1134        mActionsGridView.setAnimateChildLayout(false);
1135        final int count = mActionsGridView.getChildCount();
1136        for (int i = 0; i < count; i++) {
1137            ViewHolder vh = (ViewHolder) mActionsGridView
1138                    .getChildViewHolder(mActionsGridView.getChildAt(i));
1139            updateChevronAndVisibility(vh);
1140        }
1141        if (mSubActionsGridView != null) {
1142            if (avh != null && avh.getAction().hasSubActions()) {
1143                ViewGroup.MarginLayoutParams lp =
1144                        (ViewGroup.MarginLayoutParams) mSubActionsGridView.getLayoutParams();
1145                lp.topMargin = avh.itemView.getTop();
1146                lp.height = ViewGroup.MarginLayoutParams.MATCH_PARENT;
1147                mSubActionsGridView.setLayoutParams(lp);
1148                mSubActionsGridView.setVisibility(View.VISIBLE);
1149                mSubActionsGridView.requestFocus();
1150                mSubActionsGridView.setSelectedPosition(0);
1151                ((GuidedActionAdapter) mSubActionsGridView.getAdapter())
1152                        .setActions(avh.getAction().getSubActions());
1153            } else if (mSubActionsGridView.getVisibility() == View.VISIBLE) {
1154                mSubActionsGridView.setVisibility(View.INVISIBLE);
1155                ViewGroup.MarginLayoutParams lp =
1156                        (ViewGroup.MarginLayoutParams) mSubActionsGridView.getLayoutParams();
1157                lp.height = 0;
1158                mSubActionsGridView.setLayoutParams(lp);
1159                ((GuidedActionAdapter) mSubActionsGridView.getAdapter())
1160                        .setActions(Collections.EMPTY_LIST);
1161                mActionsGridView.requestFocus();
1162            }
1163        }
1164    }
1165
1166    private void updateChevronAndVisibility(ViewHolder vh) {
1167        if (!vh.isSubAction()) {
1168            if (mExpandedAction == null) {
1169                vh.itemView.setVisibility(View.VISIBLE);
1170                vh.itemView.setTranslationY(0);
1171                if (vh.mActivatorView != null) {
1172                    vh.setActivated(false);
1173                }
1174            } else if (vh.getAction() == mExpandedAction) {
1175                vh.itemView.setVisibility(View.VISIBLE);
1176                if (vh.getAction().hasSubActions()) {
1177                    vh.itemView.setTranslationY(- vh.itemView.getHeight());
1178                } else if (vh.mActivatorView != null) {
1179                    vh.itemView.setTranslationY(0);
1180                    vh.setActivated(true);
1181                }
1182            } else {
1183                vh.itemView.setVisibility(View.INVISIBLE);
1184                vh.itemView.setTranslationY(0);
1185            }
1186        }
1187        if (vh.mChevronView != null) {
1188            onBindChevronView(vh, vh.getAction());
1189        }
1190    }
1191
1192    /*
1193     * ==========================================
1194     * FragmentAnimationProvider overrides
1195     * ==========================================
1196     */
1197
1198    /**
1199     * {@inheritDoc}
1200     */
1201    @Override
1202    public void onImeAppearing(@NonNull List<Animator> animators) {
1203    }
1204
1205    /**
1206     * {@inheritDoc}
1207     */
1208    @Override
1209    public void onImeDisappearing(@NonNull List<Animator> animators) {
1210    }
1211
1212    /*
1213     * ==========================================
1214     * Private methods
1215     * ==========================================
1216     */
1217
1218    private float getFloat(Context ctx, TypedValue typedValue, int attrId) {
1219        ctx.getTheme().resolveAttribute(attrId, typedValue, true);
1220        // Android resources don't have a native float type, so we have to use strings.
1221        return Float.valueOf(ctx.getResources().getString(typedValue.resourceId));
1222    }
1223
1224    private int getInteger(Context ctx, TypedValue typedValue, int attrId) {
1225        ctx.getTheme().resolveAttribute(attrId, typedValue, true);
1226        return ctx.getResources().getInteger(typedValue.resourceId);
1227    }
1228
1229    private int getDimension(Context ctx, TypedValue typedValue, int attrId) {
1230        ctx.getTheme().resolveAttribute(attrId, typedValue, true);
1231        return ctx.getResources().getDimensionPixelSize(typedValue.resourceId);
1232    }
1233
1234    private static Animator createAnimator(View v, int attrId) {
1235        Context ctx = v.getContext();
1236        TypedValue typedValue = new TypedValue();
1237        ctx.getTheme().resolveAttribute(attrId, typedValue, true);
1238        Animator animator = AnimatorInflater.loadAnimator(ctx, typedValue.resourceId);
1239        animator.setTarget(v);
1240        return animator;
1241    }
1242
1243    private boolean setIcon(final ImageView iconView, GuidedAction action) {
1244        Drawable icon = null;
1245        if (iconView != null) {
1246            Context context = iconView.getContext();
1247            icon = action.getIcon();
1248            if (icon != null) {
1249                // setImageDrawable resets the drawable's level unless we set the view level first.
1250                iconView.setImageLevel(icon.getLevel());
1251                iconView.setImageDrawable(icon);
1252                iconView.setVisibility(View.VISIBLE);
1253            } else {
1254                iconView.setVisibility(View.GONE);
1255            }
1256        }
1257        return icon != null;
1258    }
1259
1260    /**
1261     * @return the max height in pixels the description can be such that the
1262     *         action nicely takes up the entire screen.
1263     */
1264    private int getDescriptionMaxHeight(Context context, TextView title) {
1265        // The 2 multiplier on the title height calculation is a
1266        // conservative estimate for font padding which can not be
1267        // calculated at this stage since the view hasn't been rendered yet.
1268        return (int)(mDisplayHeight - 2*mVerticalPadding - 2*mTitleMaxLines*title.getLineHeight());
1269    }
1270
1271}
1272