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