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