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