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;
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.TypedArray;
27 import android.graphics.Rect;
28 import android.graphics.drawable.Drawable;
29 import android.os.Build.VERSION;
30 import android.text.InputType;
31 import android.text.TextUtils;
32 import android.util.TypedValue;
33 import android.view.Gravity;
34 import android.view.KeyEvent;
35 import android.view.LayoutInflater;
36 import android.view.View;
37 import android.view.View.AccessibilityDelegate;
38 import android.view.ViewGroup;
39 import android.view.WindowManager;
40 import android.view.accessibility.AccessibilityEvent;
41 import android.view.accessibility.AccessibilityNodeInfo;
42 import android.view.inputmethod.EditorInfo;
43 import android.widget.Checkable;
44 import android.widget.EditText;
45 import android.widget.ImageView;
46 import android.widget.TextView;
47 
48 import androidx.annotation.CallSuper;
49 import androidx.annotation.NonNull;
50 import androidx.annotation.RestrictTo;
51 import androidx.core.content.ContextCompat;
52 import androidx.core.os.BuildCompat;
53 import androidx.leanback.R;
54 import androidx.leanback.transition.TransitionEpicenterCallback;
55 import androidx.leanback.transition.TransitionHelper;
56 import androidx.leanback.transition.TransitionListener;
57 import androidx.leanback.widget.GuidedActionAdapter.EditListener;
58 import androidx.leanback.widget.picker.DatePicker;
59 import androidx.recyclerview.widget.RecyclerView;
60 
61 import java.util.Calendar;
62 import java.util.Collections;
63 import 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  */
142 public 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);
sGuidedActionItemAlignFacet.setAlignmentDefs(new ItemAlignmentFacet.ItemAlignmentDef[]{alignedDef})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          */
ViewHolder(View v)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          */
ViewHolder(View v, boolean isSubAction)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          */
getContentView()234         public View getContentView() {
235             return mContentView;
236         }
237 
238         /**
239          * Returns the title view within this view holder's view.
240          */
getTitleView()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          */
getEditableTitleView()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          */
getDescriptionView()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          */
getEditableDescriptionView()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          */
getIconView()271         public ImageView getIconView() {
272             return mIconView;
273         }
274 
275         /**
276          * Returns the checkmark view within this view holder's view.
277          */
getCheckmarkView()278         public ImageView getCheckmarkView() {
279             return mCheckmarkView;
280         }
281 
282         /**
283          * Returns the chevron view within this view holder's view.
284          */
getChevronView()285         public ImageView getChevronView() {
286             return mChevronView;
287         }
288 
289         /**
290          * Returns true if in editing title, description, or activator View, false otherwise.
291          */
isInEditing()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          */
isInEditingText()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          */
isInEditingTitle()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          */
isInEditingDescription()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          */
isInEditingActivatorView()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          */
getEditingView()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          */
isSubAction()348         public boolean isSubAction() {
349             return mIsSubAction;
350         }
351 
352         /**
353          * @return Currently bound action.
354          */
getAction()355         public GuidedAction getAction() {
356             return mAction;
357         }
358 
setActivated(boolean activated)359         void setActivated(boolean activated) {
360             mActivatorView.setActivated(activated);
361             if (itemView instanceof GuidedActionItemContainer) {
362                 ((GuidedActionItemContainer) itemView).setFocusOutAllowed(!activated);
363             }
364         }
365 
366         @Override
getFacet(Class<?> facetClass)367         public Object getFacet(Class<?> facetClass) {
368             if (facetClass == ItemAlignmentFacet.class) {
369                 return sGuidedActionItemAlignFacet;
370             }
371             return null;
372         }
373 
press(boolean pressed)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      */
onCreateView(LayoutInflater inflater, final ViewGroup container)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      */
setAsButtonActions()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      */
isButtonActions()531     public boolean isButtonActions() {
532         return mButtonActions;
533     }
534 
535     /**
536      * Called when destroy the View created by GuidedActionsStylist.
537      */
onDestroyView()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      */
getActionsGridView()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      */
getSubActionsGridView()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      */
onProvideLayoutId()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      */
getItemViewType(GuidedAction action)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      */
onProvideItemLayoutId()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      */
onProvideItemLayoutId(int viewType)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      */
onCreateViewHolder(ViewGroup parent)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      */
onCreateViewHolder(ViewGroup parent, int viewType)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      */
onBindViewHolder(ViewHolder vh, GuidedAction action)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      */
openInEditMode(GuidedAction action)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 
setMaxLines(TextView view, int maxLines)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      */
setupImeOptions(ViewHolder vh, GuidedAction action)800     protected void setupImeOptions(ViewHolder vh, GuidedAction action) {
801         setupNextImeOptions(vh.getEditableTitleView());
802         setupNextImeOptions(vh.getEditableDescriptionView());
803     }
804 
setupNextImeOptions(EditText edit)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
setEditingMode(ViewHolder vh, GuidedAction action, boolean editing)816     public void setEditingMode(ViewHolder vh, GuidedAction action, boolean editing) {
817         if (editing != vh.isInEditing() && isInExpandTransition()) {
818             onEditingModeChange(vh, action, editing);
819         }
820     }
821 
setEditingMode(ViewHolder vh, boolean editing)822     void setEditingMode(ViewHolder vh, boolean editing) {
823         setEditingMode(vh, editing, true /*withTransition*/);
824     }
825 
setEditingMode(ViewHolder vh, boolean editing, boolean withTransition)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
onEditingModeChange(ViewHolder vh, GuidedAction action, boolean editing)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
onEditingModeChange(ViewHolder vh, boolean editing, boolean withTransition)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      */
onAnimateItemFocused(ViewHolder vh, boolean focused)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      */
onAnimateItemPressed(ViewHolder vh, boolean pressed)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      */
onAnimateItemPressedCancelled(ViewHolder vh)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      */
onAnimateItemChecked(ViewHolder vh, boolean checked)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      */
onBindCheckMarkView(ViewHolder vh, GuidedAction action)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      */
onBindActivatorView(ViewHolder vh, GuidedAction action)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      */
onUpdateActivatorView(ViewHolder vh, GuidedAction action)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)
setEditListener(EditListener listener)1032     public void setEditListener(EditListener listener) {
1033         mEditListener = listener;
1034     }
1035 
onEditActivatorView(final ViewHolder vh, boolean editing, final boolean withTransition)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      */
onBindChevronView(ViewHolder vh, GuidedAction action)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
setExpandedViewHolder(ViewHolder avh)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      */
isInExpandTransition()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      */
isExpandTransitionSupported()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
startExpandedTransition(ViewHolder avh)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      */
setBackKeyToCollapseSubActions(boolean backToCollapse)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      */
isBackKeyToCollapseSubActions()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      */
setBackKeyToCollapseActivatorView(boolean backToCollapse)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      */
isBackKeyToCollapseActivatorView()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      */
expandAction(GuidedAction action, final boolean withTransition)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      */
collapseAction(boolean withTransition)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 
getKeyLine()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      */
startExpanded(ViewHolder avh, final boolean withTransition)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      */
isSubActionsExpanded()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      */
isExpanded()1376     public boolean isExpanded() {
1377         return mExpandedAction != null;
1378     }
1379 
1380     /**
1381      * @return Current expanded GuidedAction or null if not expanded.
1382      */
getExpandedAction()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      */
onUpdateExpandedViewHolder(ViewHolder avh)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 
onUpdateSubActionsGridView(GuidedAction action, boolean expand)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 
updateChevronAndVisibility(ViewHolder vh)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
onImeAppearing(@onNull List<Animator> animators)1487     public void onImeAppearing(@NonNull List<Animator> animators) {
1488     }
1489 
1490     /**
1491      * {@inheritDoc}
1492      */
1493     @Override
onImeDisappearing(@onNull List<Animator> animators)1494     public void onImeDisappearing(@NonNull List<Animator> animators) {
1495     }
1496 
1497     /*
1498      * ==========================================
1499      * Private methods
1500      * ==========================================
1501      */
1502 
getFloat(Context ctx, TypedValue typedValue, int attrId)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 
getInteger(Context ctx, TypedValue typedValue, int attrId)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 
getDimension(Context ctx, TypedValue typedValue, int attrId)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 
setIcon(final ImageView iconView, GuidedAction action)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      */
getDescriptionMaxHeight(Context context, TextView title)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