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