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