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