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.animation.AnimatorListenerAdapter;
19 import android.animation.AnimatorSet;
20 import android.animation.ObjectAnimator;
21 import android.content.Context;
22 import android.content.pm.PackageManager;
23 import android.content.res.Resources;
24 import android.content.res.TypedArray;
25 import android.graphics.drawable.Drawable;
26 import android.net.Uri;
27 import android.support.annotation.NonNull;
28 import android.support.v17.leanback.R;
29 import android.support.v17.leanback.widget.VerticalGridView;
30 import android.support.v7.widget.RecyclerView;
31 import android.support.v7.widget.RecyclerView.ViewHolder;
32 import android.text.TextUtils;
33 import android.util.Log;
34 import android.util.TypedValue;
35 import android.view.animation.DecelerateInterpolator;
36 import android.view.LayoutInflater;
37 import android.view.View;
38 import android.view.ViewGroup;
39 import android.view.ViewGroup.LayoutParams;
40 import android.view.ViewPropertyAnimator;
41 import android.view.ViewTreeObserver;
42 import android.view.WindowManager;
43 import android.widget.ImageView;
44 import android.widget.TextView;
45 
46 import java.util.List;
47 
48 /**
49  * GuidedActionsStylist is used within a {@link android.support.v17.leanback.app.GuidedStepFragment}
50  * to supply the right-side panel where users can take actions. It consists of a container for the
51  * list of actions, and a stationary selector view that indicates visually the location of focus.
52  * <p>
53  * Many aspects of the base GuidedActionsStylist can be customized through theming; see the
54  * theme attributes below. Note that these attributes are not set on individual elements in layout
55  * XML, but instead would be set in a custom theme. See
56  * <a href="http://developer.android.com/guide/topics/ui/themes.html">Styles and Themes</a>
57  * for more information.
58  * <p>
59  * If these hooks are insufficient, this class may also be subclassed. Subclasses may wish to
60  * override the {@link #onProvideLayoutId} method to change the layout used to display the
61  * list container and selector, or the {@link #onProvideItemLayoutId} method to change the layout
62  * used to display each action.
63  * <p>
64  * Note: If an alternate list layout is provided, the following view IDs must be supplied:
65  * <ul>
66  * <li>{@link android.support.v17.leanback.R.id#guidedactions_selector}</li>
67  * <li>{@link android.support.v17.leanback.R.id#guidedactions_list}</li>
68  * </ul><p>
69  * These view IDs must be present in order for the stylist to function. The list ID must correspond
70  * to a {@link VerticalGridView} or subclass.
71  * <p>
72  * If an alternate item layout is provided, the following view IDs should be used to refer to base
73  * elements:
74  * <ul>
75  * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_content}</li>
76  * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_title}</li>
77  * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_description}</li>
78  * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_icon}</li>
79  * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_checkmark}</li>
80  * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_chevron}</li>
81  * </ul><p>
82  * These view IDs are allowed to be missing, in which case the corresponding views in {@link
83  * GuidedActionsStylist.ViewHolder} will be null.
84  *
85  * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsEntryAnimation
86  * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsSelectorShowAnimation
87  * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsSelectorHideAnimation
88  * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsContainerStyle
89  * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsSelectorStyle
90  * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsListStyle
91  * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemContainerStyle
92  * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemCheckmarkStyle
93  * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemIconStyle
94  * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemContentStyle
95  * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemTitleStyle
96  * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemDescriptionStyle
97  * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemChevronStyle
98  * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionCheckedAnimation
99  * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionUncheckedAnimation
100  * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionPressedAnimation
101  * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionUnpressedAnimation
102  * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionEnabledChevronAlpha
103  * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionDisabledChevronAlpha
104  * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionContentWidth
105  * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionContentWidthNoIcon
106  * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionTitleMinLines
107  * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionTitleMaxLines
108  * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionDescriptionMinLines
109  * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionVerticalPadding
110  * @see android.support.v17.leanback.app.GuidedStepFragment
111  * @see GuidedAction
112  */
113 public class GuidedActionsStylist implements FragmentAnimationProvider {
114 
115     /**
116      * ViewHolder caches information about the action item layouts' subviews. Subclasses of {@link
117      * GuidedActionsStylist} may also wish to subclass this in order to add fields.
118      * @see GuidedAction
119      */
120     public static class ViewHolder {
121 
122         public final View view;
123 
124         private View mContentView;
125         private TextView mTitleView;
126         private TextView mDescriptionView;
127         private ImageView mIconView;
128         private ImageView mCheckmarkView;
129         private ImageView mChevronView;
130 
131         /**
132          * Constructs an ViewHolder and caches the relevant subviews.
133          */
ViewHolder(View v)134         public ViewHolder(View v) {
135             view = v;
136 
137             mContentView = v.findViewById(R.id.guidedactions_item_content);
138             mTitleView = (TextView) v.findViewById(R.id.guidedactions_item_title);
139             mDescriptionView = (TextView) v.findViewById(R.id.guidedactions_item_description);
140             mIconView = (ImageView) v.findViewById(R.id.guidedactions_item_icon);
141             mCheckmarkView = (ImageView) v.findViewById(R.id.guidedactions_item_checkmark);
142             mChevronView = (ImageView) v.findViewById(R.id.guidedactions_item_chevron);
143         }
144 
145         /**
146          * Returns the content view within this view holder's view, where title and description are
147          * shown.
148          */
getContentView()149         public View getContentView() {
150             return mContentView;
151         }
152 
153         /**
154          * Returns the title view within this view holder's view.
155          */
getTitleView()156         public TextView getTitleView() {
157             return mTitleView;
158         }
159 
160         /**
161          * Returns the description view within this view holder's view.
162          */
getDescriptionView()163         public TextView getDescriptionView() {
164             return mDescriptionView;
165         }
166 
167         /**
168          * Returns the icon view within this view holder's view.
169          */
getIconView()170         public ImageView getIconView() {
171             return mIconView;
172         }
173 
174         /**
175          * Returns the checkmark view within this view holder's view.
176          */
getCheckmarkView()177         public ImageView getCheckmarkView() {
178             return mCheckmarkView;
179         }
180 
181         /**
182          * Returns the chevron view within this view holder's view.
183          */
getChevronView()184         public ImageView getChevronView() {
185             return mChevronView;
186         }
187 
188     }
189 
190     private static String TAG = "GuidedActionsStylist";
191 
192     protected View mMainView;
193     protected VerticalGridView mActionsGridView;
194     protected View mSelectorView;
195 
196     // Cached values from resources
197     private float mEnabledChevronAlpha;
198     private float mDisabledChevronAlpha;
199     private int mContentWidth;
200     private int mContentWidthNoIcon;
201     private int mTitleMinLines;
202     private int mTitleMaxLines;
203     private int mDescriptionMinLines;
204     private int mVerticalPadding;
205     private int mDisplayHeight;
206 
207     /**
208      * Creates a view appropriate for displaying a list of GuidedActions, using the provided
209      * inflater and container.
210      * <p>
211      * <i>Note: Does not actually add the created view to the container; the caller should do
212      * this.</i>
213      * @param inflater The layout inflater to be used when constructing the view.
214      * @param container The view group to be passed in the call to
215      * <code>LayoutInflater.inflate</code>.
216      * @return The view to be added to the caller's view hierarchy.
217      */
onCreateView(LayoutInflater inflater, ViewGroup container)218     public View onCreateView(LayoutInflater inflater, ViewGroup container) {
219         mMainView = inflater.inflate(onProvideLayoutId(), container, false);
220         mSelectorView = mMainView.findViewById(R.id.guidedactions_selector);
221         if (mMainView instanceof VerticalGridView) {
222             mActionsGridView = (VerticalGridView) mMainView;
223         } else {
224             mActionsGridView = (VerticalGridView) mMainView.findViewById(R.id.guidedactions_list);
225             if (mActionsGridView == null) {
226                 throw new IllegalStateException("No ListView exists.");
227             }
228             mActionsGridView.setWindowAlignmentOffset(0);
229             mActionsGridView.setWindowAlignmentOffsetPercent(50f);
230             mActionsGridView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE);
231             if (mSelectorView != null) {
232                 mActionsGridView.setOnScrollListener(new
233                         SelectorAnimator(mSelectorView, mActionsGridView));
234             }
235         }
236 
237         mActionsGridView.requestFocusFromTouch();
238 
239         if (mSelectorView != null) {
240             // ALlow focus to move to other views
241             mActionsGridView.getViewTreeObserver().addOnGlobalFocusChangeListener(
242                     new ViewTreeObserver.OnGlobalFocusChangeListener() {
243                         private boolean mChildFocused;
244 
245                         @Override
246                         public void onGlobalFocusChanged(View oldFocus, View newFocus) {
247                             View focusedChild = mActionsGridView.getFocusedChild();
248                             if (focusedChild == null) {
249                                 mSelectorView.setVisibility(View.INVISIBLE);
250                                 mChildFocused = false;
251                             } else if (!mChildFocused) {
252                                 mChildFocused = true;
253                                 mSelectorView.setVisibility(View.VISIBLE);
254                                 updateSelectorView(focusedChild);
255                             }
256                         }
257                     });
258         }
259 
260         // Cache widths, chevron alpha values, max and min text lines, etc
261         Context ctx = mMainView.getContext();
262         TypedValue val = new TypedValue();
263         mEnabledChevronAlpha = getFloat(ctx, val, R.attr.guidedActionEnabledChevronAlpha);
264         mDisabledChevronAlpha = getFloat(ctx, val, R.attr.guidedActionDisabledChevronAlpha);
265         mContentWidth = getDimension(ctx, val, R.attr.guidedActionContentWidth);
266         mContentWidthNoIcon = getDimension(ctx, val, R.attr.guidedActionContentWidthNoIcon);
267         mTitleMinLines = getInteger(ctx, val, R.attr.guidedActionTitleMinLines);
268         mTitleMaxLines = getInteger(ctx, val, R.attr.guidedActionTitleMaxLines);
269         mDescriptionMinLines = getInteger(ctx, val, R.attr.guidedActionDescriptionMinLines);
270         mVerticalPadding = getDimension(ctx, val, R.attr.guidedActionVerticalPadding);
271         mDisplayHeight = ((WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE))
272                 .getDefaultDisplay().getHeight();
273 
274         return mMainView;
275     }
276 
277     /**
278      * Returns the VerticalGridView that displays the list of GuidedActions.
279      * @return The VerticalGridView for this presenter.
280      */
getActionsGridView()281     public VerticalGridView getActionsGridView() {
282         return mActionsGridView;
283     }
284 
285     /**
286      * Provides the resource ID of the layout defining the host view for the list of guided actions.
287      * Subclasses may override to provide their own customized layouts. The base implementation
288      * returns {@link android.support.v17.leanback.R.layout#lb_guidedactions}. If overridden, the
289      * substituted layout should contain matching IDs for any views that should be managed by the
290      * base class; this can be achieved by starting with a copy of the base layout file.
291      * @return The resource ID of the layout to be inflated to define the host view for the list
292      * of GuidedActions.
293      */
onProvideLayoutId()294     public int onProvideLayoutId() {
295         return R.layout.lb_guidedactions;
296     }
297 
298     /**
299      * Provides the resource ID of the layout defining the view for an individual guided actions.
300      * Subclasses may override to provide their own customized layouts. The base implementation
301      * returns {@link android.support.v17.leanback.R.layout#lb_guidedactions_item}. If overridden,
302      * the substituted layout should contain matching IDs for any views that should be managed by
303      * the base class; this can be achieved by starting with a copy of the base layout file.
304      * @return The resource ID of the layout to be inflated to define the view to display an
305      * individual GuidedAction.
306      */
onProvideItemLayoutId()307     public int onProvideItemLayoutId() {
308         return R.layout.lb_guidedactions_item;
309     }
310 
311     /**
312      * Constructs a {@link ViewHolder} capable of representing {@link GuidedAction}s. Subclasses
313      * may choose to return a subclass of ViewHolder.
314      * <p>
315      * <i>Note: Should not actually add the created view to the parent; the caller will do
316      * this.</i>
317      * @param parent The view group to be used as the parent of the new view.
318      * @return The view to be added to the caller's view hierarchy.
319      */
onCreateViewHolder(ViewGroup parent)320     public ViewHolder onCreateViewHolder(ViewGroup parent) {
321         LayoutInflater inflater = LayoutInflater.from(parent.getContext());
322         View v = inflater.inflate(onProvideItemLayoutId(), parent, false);
323         return new ViewHolder(v);
324     }
325 
326     /**
327      * Binds a {@link ViewHolder} to a particular {@link GuidedAction}.
328      * @param vh The view holder to be associated with the given action.
329      * @param action The guided action to be displayed by the view holder's view.
330      * @return The view to be added to the caller's view hierarchy.
331      */
onBindViewHolder(ViewHolder vh, GuidedAction action)332     public void onBindViewHolder(ViewHolder vh, GuidedAction action) {
333 
334         if (vh.mTitleView != null) {
335             vh.mTitleView.setText(action.getTitle());
336         }
337         if (vh.mDescriptionView != null) {
338             vh.mDescriptionView.setText(action.getDescription());
339             vh.mDescriptionView.setVisibility(TextUtils.isEmpty(action.getDescription()) ?
340                     View.GONE : View.VISIBLE);
341         }
342         // Clients might want the check mark view to be gone entirely, in which case, ignore it.
343         if (vh.mCheckmarkView != null && vh.mCheckmarkView.getVisibility() != View.GONE) {
344             vh.mCheckmarkView.setVisibility(action.isChecked() ? View.VISIBLE : View.INVISIBLE);
345         }
346 
347         if (vh.mContentView != null) {
348             ViewGroup.LayoutParams contentLp = vh.mContentView.getLayoutParams();
349             if (setIcon(vh.mIconView, action)) {
350                 contentLp.width = mContentWidth;
351             } else {
352                 contentLp.width = mContentWidthNoIcon;
353             }
354             vh.mContentView.setLayoutParams(contentLp);
355         }
356 
357         if (vh.mChevronView != null) {
358             vh.mChevronView.setVisibility(action.hasNext() ? View.VISIBLE : View.INVISIBLE);
359             vh.mChevronView.setAlpha(action.isEnabled() ? mEnabledChevronAlpha :
360                     mDisabledChevronAlpha);
361         }
362 
363         if (action.hasMultilineDescription()) {
364             if (vh.mTitleView != null) {
365                 vh.mTitleView.setMaxLines(mTitleMaxLines);
366                 if (vh.mDescriptionView != null) {
367                     vh.mDescriptionView.setMaxHeight(getDescriptionMaxHeight(vh.view.getContext(),
368                             vh.mTitleView));
369                 }
370             }
371         } else {
372             if (vh.mTitleView != null) {
373                 vh.mTitleView.setMaxLines(mTitleMinLines);
374             }
375             if (vh.mDescriptionView != null) {
376                 vh.mDescriptionView.setMaxLines(mDescriptionMinLines);
377             }
378         }
379     }
380 
381     /**
382      * Animates the view holder's view (or subviews thereof) when the action has had its focus
383      * state changed.
384      * @param vh The view holder associated with the relevant action.
385      * @param focused True if the action has become focused, false if it has lost focus.
386      */
onAnimateItemFocused(ViewHolder vh, boolean focused)387     public void onAnimateItemFocused(ViewHolder vh, boolean focused) {
388         // No animations for this, currently, because the animation is done on
389         // mSelectorView
390     }
391 
392     /**
393      * Animates the view holder's view (or subviews thereof) when the action has had its press
394      * state changed.
395      * @param vh The view holder associated with the relevant action.
396      * @param pressed True if the action has been pressed, false if it has been unpressed.
397      */
onAnimateItemPressed(ViewHolder vh, boolean pressed)398     public void onAnimateItemPressed(ViewHolder vh, boolean pressed) {
399         int attr = pressed ? R.attr.guidedActionPressedAnimation :
400                 R.attr.guidedActionUnpressedAnimation;
401         createAnimator(vh.view, attr).start();
402     }
403 
404     /**
405      * Animates the view holder's view (or subviews thereof) when the action has had its check
406      * state changed.
407      * @param vh The view holder associated with the relevant action.
408      * @param checked True if the action has become checked, false if it has become unchecked.
409      */
onAnimateItemChecked(ViewHolder vh, boolean checked)410     public void onAnimateItemChecked(ViewHolder vh, boolean checked) {
411         final View checkView = vh.mCheckmarkView;
412         if (checkView != null) {
413             if (checked) {
414                 checkView.setVisibility(View.VISIBLE);
415                 createAnimator(checkView, R.attr.guidedActionCheckedAnimation).start();
416             } else {
417                 Animator animator = createAnimator(checkView,
418                         R.attr.guidedActionUncheckedAnimation);
419                 animator.addListener(new AnimatorListenerAdapter() {
420                     @Override
421                     public void onAnimationEnd(Animator animation) {
422                         checkView.setVisibility(View.INVISIBLE);
423                     }
424                 });
425                 animator.start();
426             }
427         }
428     }
429 
430     /*
431      * ==========================================
432      * FragmentAnimationProvider overrides
433      * ==========================================
434      */
435 
436     /**
437      * {@inheritDoc}
438      */
439     @Override
onActivityEnter(@onNull List<Animator> animators)440     public void onActivityEnter(@NonNull List<Animator> animators) {
441         animators.add(createAnimator(mMainView, R.attr.guidedActionsEntryAnimation));
442     }
443 
444     /**
445      * {@inheritDoc}
446      */
447     @Override
onActivityExit(@onNull List<Animator> animators)448     public void onActivityExit(@NonNull List<Animator> animators) {}
449 
450     /**
451      * {@inheritDoc}
452      */
453     @Override
onFragmentEnter(@onNull List<Animator> animators)454     public void onFragmentEnter(@NonNull List<Animator> animators) {
455         animators.add(createAnimator(mActionsGridView, R.attr.guidedStepEntryAnimation));
456         animators.add(createAnimator(mSelectorView, R.attr.guidedStepEntryAnimation));
457     }
458 
459     /**
460      * {@inheritDoc}
461      */
462     @Override
onFragmentExit(@onNull List<Animator> animators)463     public void onFragmentExit(@NonNull List<Animator> animators) {
464         animators.add(createAnimator(mActionsGridView, R.attr.guidedStepExitAnimation));
465         animators.add(createAnimator(mSelectorView, R.attr.guidedStepExitAnimation));
466     }
467 
468     /**
469      * {@inheritDoc}
470      */
471     @Override
onFragmentReenter(@onNull List<Animator> animators)472     public void onFragmentReenter(@NonNull List<Animator> animators) {
473         animators.add(createAnimator(mActionsGridView, R.attr.guidedStepReentryAnimation));
474         animators.add(createAnimator(mSelectorView, R.attr.guidedStepReentryAnimation));
475     }
476 
477     /**
478      * {@inheritDoc}
479      */
480     @Override
onFragmentReturn(@onNull List<Animator> animators)481     public void onFragmentReturn(@NonNull List<Animator> animators) {
482         animators.add(createAnimator(mActionsGridView, R.attr.guidedStepReturnAnimation));
483         animators.add(createAnimator(mSelectorView, R.attr.guidedStepReturnAnimation));
484     }
485 
486     /*
487      * ==========================================
488      * Private methods
489      * ==========================================
490      */
491 
updateSelectorView(View focusedChild)492     private void updateSelectorView(View focusedChild) {
493         // Display the selector view.
494         int height = focusedChild.getHeight();
495         LayoutParams lp = mSelectorView.getLayoutParams();
496         lp.height = height;
497         mSelectorView.setLayoutParams(lp);
498         mSelectorView.setAlpha(1f);
499     }
500 
getFloat(Context ctx, TypedValue typedValue, int attrId)501     private float getFloat(Context ctx, TypedValue typedValue, int attrId) {
502         ctx.getTheme().resolveAttribute(attrId, typedValue, true);
503         // Android resources don't have a native float type, so we have to use strings.
504         return Float.valueOf(ctx.getResources().getString(typedValue.resourceId));
505     }
506 
getInteger(Context ctx, TypedValue typedValue, int attrId)507     private int getInteger(Context ctx, TypedValue typedValue, int attrId) {
508         ctx.getTheme().resolveAttribute(attrId, typedValue, true);
509         return ctx.getResources().getInteger(typedValue.resourceId);
510     }
511 
getDimension(Context ctx, TypedValue typedValue, int attrId)512     private int getDimension(Context ctx, TypedValue typedValue, int attrId) {
513         ctx.getTheme().resolveAttribute(attrId, typedValue, true);
514         return ctx.getResources().getDimensionPixelSize(typedValue.resourceId);
515     }
516 
createAnimator(View v, int attrId)517     private static Animator createAnimator(View v, int attrId) {
518         Context ctx = v.getContext();
519         TypedValue typedValue = new TypedValue();
520         ctx.getTheme().resolveAttribute(attrId, typedValue, true);
521         Animator animator = AnimatorInflater.loadAnimator(ctx, typedValue.resourceId);
522         animator.setTarget(v);
523         return animator;
524     }
525 
setIcon(final ImageView iconView, GuidedAction action)526     private boolean setIcon(final ImageView iconView, GuidedAction action) {
527         Drawable icon = null;
528         if (iconView != null) {
529             Context context = iconView.getContext();
530             icon = action.getIcon();
531             if (icon != null) {
532                 // setImageDrawable resets the drawable's level unless we set the view level first.
533                 iconView.setImageLevel(icon.getLevel());
534                 iconView.setImageDrawable(icon);
535                 iconView.setVisibility(View.VISIBLE);
536             } else {
537                 iconView.setVisibility(View.GONE);
538             }
539         }
540         return icon != null;
541     }
542 
543     /**
544      * @return the max height in pixels the description can be such that the
545      *         action nicely takes up the entire screen.
546      */
getDescriptionMaxHeight(Context context, TextView title)547     private int getDescriptionMaxHeight(Context context, TextView title) {
548         // The 2 multiplier on the title height calculation is a
549         // conservative estimate for font padding which can not be
550         // calculated at this stage since the view hasn't been rendered yet.
551         return (int)(mDisplayHeight - 2*mVerticalPadding - 2*mTitleMaxLines*title.getLineHeight());
552     }
553 
554     /**
555      * SelectorAnimator
556      * Controls animation for selected item backgrounds
557      * TODO: Move into focus animation override?
558      */
559     private static class SelectorAnimator extends RecyclerView.OnScrollListener {
560 
561         private final View mSelectorView;
562         private final ViewGroup mParentView;
563         private volatile boolean mFadedOut = true;
564 
SelectorAnimator(View selectorView, ViewGroup parentView)565         SelectorAnimator(View selectorView, ViewGroup parentView) {
566             mSelectorView = selectorView;
567             mParentView = parentView;
568         }
569 
570         // We want to fade in the selector if we've stopped scrolling on it. If
571         // we're scrolling, we want to ensure to dim the selector if we haven't
572         // already. We dim the last highlighted view so that while a user is
573         // scrolling, nothing is highlighted.
574         @Override
onScrollStateChanged(RecyclerView recyclerView, int newState)575         public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
576             Animator animator = null;
577             boolean fadingOut = false;
578             if (newState == RecyclerView.SCROLL_STATE_IDLE) {
579                 // The selector starts with a height of 0. In order to scale up from
580                 // 0, we first need the set the height to 1 and scale from there.
581                 View focusedChild = mParentView.getFocusedChild();
582                 if (focusedChild != null) {
583                     int selectorHeight = mSelectorView.getHeight();
584                     float scaleY = (float) focusedChild.getHeight() / selectorHeight;
585                     AnimatorSet animators = (AnimatorSet)createAnimator(mSelectorView,
586                             R.attr.guidedActionsSelectorShowAnimation);
587                     if (mFadedOut) {
588                         // selector is completely faded out, so we can just scale before fading in.
589                         mSelectorView.setScaleY(scaleY);
590                         animator = animators.getChildAnimations().get(0);
591                     } else {
592                         // selector is not faded out, so we must animate the scale as we fade in.
593                         ((ObjectAnimator)animators.getChildAnimations().get(1))
594                                 .setFloatValues(scaleY);
595                         animator = animators;
596                     }
597                 }
598             } else {
599                 animator = createAnimator(mSelectorView, R.attr.guidedActionsSelectorHideAnimation);
600                 fadingOut = true;
601             }
602             if (animator != null) {
603                 animator.addListener(new Listener(fadingOut));
604                 animator.start();
605             }
606         }
607 
608         /**
609          * Sets {@link BaseScrollAdapterFragment#mFadedOut}
610          * {@link BaseScrollAdapterFragment#mFadedOut} is true, iff
611          * {@link BaseScrollAdapterFragment#mSelectorView} has an alpha of 0
612          * (faded out). If false the view either has an alpha of 1 (visible) or
613          * is in the process of animating.
614          */
615         private class Listener implements Animator.AnimatorListener {
616             private boolean mFadingOut;
617             private boolean mCanceled;
618 
Listener(boolean fadingOut)619             public Listener(boolean fadingOut) {
620                 mFadingOut = fadingOut;
621             }
622 
623             @Override
onAnimationStart(Animator animation)624             public void onAnimationStart(Animator animation) {
625                 if (!mFadingOut) {
626                     mFadedOut = false;
627                 }
628             }
629 
630             @Override
onAnimationEnd(Animator animation)631             public void onAnimationEnd(Animator animation) {
632                 if (!mCanceled && mFadingOut) {
633                     mFadedOut = true;
634                 }
635             }
636 
637             @Override
onAnimationCancel(Animator animation)638             public void onAnimationCancel(Animator animation) {
639                 mCanceled = true;
640             }
641 
642             @Override
onAnimationRepeat(Animator animation)643             public void onAnimationRepeat(Animator animation) {
644             }
645         }
646     }
647 
648 }
649