GuidedActionsStylist.java revision ebd3d9078dbaebd10a9506ca086435eb63e8a2d2
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 */
14package android.support.v17.leanback.widget;
15
16import android.animation.Animator;
17import android.animation.AnimatorInflater;
18import android.animation.AnimatorListenerAdapter;
19import android.animation.AnimatorSet;
20import android.animation.ObjectAnimator;
21import android.content.Context;
22import android.content.pm.PackageManager;
23import android.content.res.Resources;
24import android.content.res.TypedArray;
25import android.graphics.drawable.Drawable;
26import android.net.Uri;
27import android.support.annotation.NonNull;
28import android.support.v17.leanback.R;
29import android.support.v17.leanback.widget.VerticalGridView;
30import android.support.v7.widget.RecyclerView;
31import android.support.v7.widget.RecyclerView.ViewHolder;
32import android.text.TextUtils;
33import android.util.Log;
34import android.util.TypedValue;
35import android.view.animation.DecelerateInterpolator;
36import android.view.LayoutInflater;
37import android.view.View;
38import android.view.ViewGroup;
39import android.view.ViewGroup.LayoutParams;
40import android.view.ViewPropertyAnimator;
41import android.view.ViewTreeObserver;
42import android.view.WindowManager;
43import android.widget.ImageView;
44import android.widget.TextView;
45
46import java.util.List;
47
48/**
49 * GuidedActionsStylist is used within a GuidedStepFragment to supply the right-side panel
50 * where users can take actions. It consists of a container for the list of actions, and a
51 * 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 */
113public 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         */
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         */
149        public View getContentView() {
150            return mContentView;
151        }
152
153        /**
154         * Returns the title view within this view holder's view.
155         */
156        public TextView getTitleView() {
157            return mTitleView;
158        }
159
160        /**
161         * Returns the description view within this view holder's view.
162         */
163        public TextView getDescriptionView() {
164            return mDescriptionView;
165        }
166
167        /**
168         * Returns the icon view within this view holder's view.
169         */
170        public ImageView getIconView() {
171            return mIconView;
172        }
173
174        /**
175         * Returns the checkmark view within this view holder's view.
176         */
177        public ImageView getCheckmarkView() {
178            return mCheckmarkView;
179        }
180
181        /**
182         * Returns the chevron view within this view holder's view.
183         */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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, R.attr.guidedActionCheckedAnimation);
418                animator.addListener(new AnimatorListenerAdapter() {
419                    @Override
420                    public void onAnimationEnd(Animator animation) {
421                        checkView.setVisibility(View.INVISIBLE);
422                    }
423                });
424                animator.start();
425            }
426        }
427    }
428
429    /*
430     * ==========================================
431     * FragmentAnimationProvider overrides
432     * ==========================================
433     */
434
435    /**
436     * {@inheritDoc}
437     */
438    @Override
439    public void onActivityEnter(@NonNull List<Animator> animators) {
440        animators.add(createAnimator(mMainView, R.attr.guidedActionsEntryAnimation));
441    }
442
443    /**
444     * {@inheritDoc}
445     */
446    @Override
447    public void onActivityExit(@NonNull List<Animator> animators) {}
448
449    /**
450     * {@inheritDoc}
451     */
452    @Override
453    public void onFragmentEnter(@NonNull List<Animator> animators) {
454        animators.add(createAnimator(mActionsGridView, R.attr.guidedStepEntryAnimation));
455        animators.add(createAnimator(mSelectorView, R.attr.guidedStepEntryAnimation));
456    }
457
458    /**
459     * {@inheritDoc}
460     */
461    @Override
462    public void onFragmentExit(@NonNull List<Animator> animators) {
463        animators.add(createAnimator(mActionsGridView, R.attr.guidedStepExitAnimation));
464        animators.add(createAnimator(mSelectorView, R.attr.guidedStepExitAnimation));
465    }
466
467    /**
468     * {@inheritDoc}
469     */
470    @Override
471    public void onFragmentReenter(@NonNull List<Animator> animators) {
472        animators.add(createAnimator(mActionsGridView, R.attr.guidedStepReentryAnimation));
473        animators.add(createAnimator(mSelectorView, R.attr.guidedStepReentryAnimation));
474    }
475
476    /**
477     * {@inheritDoc}
478     */
479    @Override
480    public void onFragmentReturn(@NonNull List<Animator> animators) {
481        animators.add(createAnimator(mActionsGridView, R.attr.guidedStepReturnAnimation));
482        animators.add(createAnimator(mSelectorView, R.attr.guidedStepReturnAnimation));
483    }
484
485    /*
486     * ==========================================
487     * Private methods
488     * ==========================================
489     */
490
491    private void updateSelectorView(View focusedChild) {
492        // Display the selector view.
493        int height = focusedChild.getHeight();
494        LayoutParams lp = mSelectorView.getLayoutParams();
495        lp.height = height;
496        mSelectorView.setLayoutParams(lp);
497        mSelectorView.setAlpha(1f);
498    }
499
500    private float getFloat(Context ctx, TypedValue typedValue, int attrId) {
501        ctx.getTheme().resolveAttribute(attrId, typedValue, true);
502        // Android resources don't have a native float type, so we have to use strings.
503        return Float.valueOf(ctx.getResources().getString(typedValue.resourceId));
504    }
505
506    private int getInteger(Context ctx, TypedValue typedValue, int attrId) {
507        ctx.getTheme().resolveAttribute(attrId, typedValue, true);
508        return ctx.getResources().getInteger(typedValue.resourceId);
509    }
510
511    private int getDimension(Context ctx, TypedValue typedValue, int attrId) {
512        ctx.getTheme().resolveAttribute(attrId, typedValue, true);
513        return ctx.getResources().getDimensionPixelSize(typedValue.resourceId);
514    }
515
516    private static Animator createAnimator(View v, int attrId) {
517        Context ctx = v.getContext();
518        TypedValue typedValue = new TypedValue();
519        ctx.getTheme().resolveAttribute(attrId, typedValue, true);
520        Animator animator = AnimatorInflater.loadAnimator(ctx, typedValue.resourceId);
521        animator.setTarget(v);
522        return animator;
523    }
524
525    private boolean setIcon(final ImageView iconView, GuidedAction action) {
526        Drawable icon = null;
527        if (iconView != null) {
528            Context context = iconView.getContext();
529            icon = action.getIcon();
530            if (icon != null) {
531                iconView.setImageDrawable(icon);
532                iconView.setVisibility(View.VISIBLE);
533            } else {
534                iconView.setVisibility(View.GONE);
535            }
536        }
537        return icon != null;
538    }
539
540    /**
541     * @return the max height in pixels the description can be such that the
542     *         action nicely takes up the entire screen.
543     */
544    private int getDescriptionMaxHeight(Context context, TextView title) {
545        // The 2 multiplier on the title height calculation is a
546        // conservative estimate for font padding which can not be
547        // calculated at this stage since the view hasn't been rendered yet.
548        return (int)(mDisplayHeight - 2*mVerticalPadding - 2*mTitleMaxLines*title.getLineHeight());
549    }
550
551    /**
552     * SelectorAnimator
553     * Controls animation for selected item backgrounds
554     * TODO: Move into focus animation override?
555     */
556    private static class SelectorAnimator extends RecyclerView.OnScrollListener {
557
558        private final View mSelectorView;
559        private final ViewGroup mParentView;
560        private volatile boolean mFadedOut = true;
561
562        SelectorAnimator(View selectorView, ViewGroup parentView) {
563            mSelectorView = selectorView;
564            mParentView = parentView;
565        }
566
567        // We want to fade in the selector if we've stopped scrolling on it. If
568        // we're scrolling, we want to ensure to dim the selector if we haven't
569        // already. We dim the last highlighted view so that while a user is
570        // scrolling, nothing is highlighted.
571        @Override
572        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
573            Animator animator = null;
574            boolean fadingOut = false;
575            if (newState == RecyclerView.SCROLL_STATE_IDLE) {
576                // The selector starts with a height of 0. In order to scale up from
577                // 0, we first need the set the height to 1 and scale from there.
578                View focusedChild = mParentView.getFocusedChild();
579                if (focusedChild != null) {
580                    int selectorHeight = mSelectorView.getHeight();
581                    float scaleY = (float) focusedChild.getHeight() / selectorHeight;
582                    AnimatorSet animators = (AnimatorSet)createAnimator(mSelectorView,
583                            R.attr.guidedActionsSelectorShowAnimation);
584                    if (mFadedOut) {
585                        // selector is completely faded out, so we can just scale before fading in.
586                        mSelectorView.setScaleY(scaleY);
587                        animator = animators.getChildAnimations().get(0);
588                    } else {
589                        // selector is not faded out, so we must animate the scale as we fade in.
590                        ((ObjectAnimator)animators.getChildAnimations().get(1))
591                                .setFloatValues(scaleY);
592                        animator = animators;
593                    }
594                }
595            } else {
596                animator = createAnimator(mSelectorView, R.attr.guidedActionsSelectorHideAnimation);
597                fadingOut = true;
598            }
599            if (animator != null) {
600                animator.addListener(new Listener(fadingOut));
601                animator.start();
602            }
603        }
604
605        /**
606         * Sets {@link BaseScrollAdapterFragment#mFadedOut}
607         * {@link BaseScrollAdapterFragment#mFadedOut} is true, iff
608         * {@link BaseScrollAdapterFragment#mSelectorView} has an alpha of 0
609         * (faded out). If false the view either has an alpha of 1 (visible) or
610         * is in the process of animating.
611         */
612        private class Listener implements Animator.AnimatorListener {
613            private boolean mFadingOut;
614            private boolean mCanceled;
615
616            public Listener(boolean fadingOut) {
617                mFadingOut = fadingOut;
618            }
619
620            @Override
621            public void onAnimationStart(Animator animation) {
622                if (!mFadingOut) {
623                    mFadedOut = false;
624                }
625            }
626
627            @Override
628            public void onAnimationEnd(Animator animation) {
629                if (!mCanceled && mFadingOut) {
630                    mFadedOut = true;
631                }
632            }
633
634            @Override
635            public void onAnimationCancel(Animator animation) {
636                mCanceled = true;
637            }
638
639            @Override
640            public void onAnimationRepeat(Animator animation) {
641            }
642        }
643    }
644
645}
646