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