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