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