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