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