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