GuidedActionsStylist.java revision 43e10e99e55c1c2eeca31fa13e9cc84160850f59
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.v4.view.ViewCompat;
31import android.support.v7.widget.RecyclerView;
32import android.support.v7.widget.RecyclerView.ViewHolder;
33import android.text.TextUtils;
34import android.util.Log;
35import android.util.TypedValue;
36import android.view.animation.DecelerateInterpolator;
37import android.view.LayoutInflater;
38import android.view.View;
39import android.view.ViewGroup;
40import android.view.ViewGroup.LayoutParams;
41import android.view.ViewPropertyAnimator;
42import android.view.ViewTreeObserver;
43import android.view.WindowManager;
44import android.widget.EditText;
45import android.widget.ImageView;
46import android.widget.TextView;
47
48import java.util.List;
49
50/**
51 * GuidedActionsStylist is used within a {@link android.support.v17.leanback.app.GuidedStepFragment}
52 * to supply the right-side panel where users can take actions. It consists of a container for the
53 * list of actions, and a stationary selector view that indicates visually the location of focus.
54 * <p>
55 * Many aspects of the base GuidedActionsStylist can be customized through theming; see the
56 * theme attributes below. Note that these attributes are not set on individual elements in layout
57 * XML, but instead would be set in a custom theme. See
58 * <a href="http://developer.android.com/guide/topics/ui/themes.html">Styles and Themes</a>
59 * for more information.
60 * <p>
61 * If these hooks are insufficient, this class may also be subclassed. Subclasses may wish to
62 * override the {@link #onProvideLayoutId} method to change the layout used to display the
63 * list container and selector, or the {@link #onProvideItemLayoutId} method to change the layout
64 * used to display each action.
65 * <p>
66 * Note: If an alternate list layout is provided, the following view IDs must be supplied:
67 * <ul>
68 * <li>{@link android.support.v17.leanback.R.id#guidedactions_selector}</li>
69 * <li>{@link android.support.v17.leanback.R.id#guidedactions_list}</li>
70 * </ul><p>
71 * These view IDs must be present in order for the stylist to function. The list ID must correspond
72 * to a {@link VerticalGridView} or subclass.
73 * <p>
74 * If an alternate item layout is provided, the following view IDs should be used to refer to base
75 * elements:
76 * <ul>
77 * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_content}</li>
78 * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_title}</li>
79 * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_description}</li>
80 * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_icon}</li>
81 * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_checkmark}</li>
82 * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_chevron}</li>
83 * </ul><p>
84 * These view IDs are allowed to be missing, in which case the corresponding views in {@link
85 * GuidedActionsStylist.ViewHolder} will be null.
86 * <p>
87 * In order to support editable actions, the view associated with guidedactions_item_title should
88 * be a subclass of {@link android.widget.EditText}, and should satisfy the {@link
89 * ImeKeyMonitor} interface.
90 *
91 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedStepImeAppearingAnimation
92 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedStepImeDisappearingAnimation
93 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsSelectorShowAnimation
94 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsSelectorHideAnimation
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_guidedActionTitleMinLines
111 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionTitleMaxLines
112 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionDescriptionMinLines
113 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionVerticalPadding
114 * @see android.support.v17.leanback.app.GuidedStepFragment
115 * @see GuidedAction
116 */
117public class GuidedActionsStylist implements FragmentAnimationProvider {
118
119    /**
120     * Default viewType that associated with default layout Id for the action item.
121     * @see #getItemViewType(GuidedAction)
122     * @see #onProvideItemLayoutId(int)
123     * @see #onCreateViewHolder(ViewGroup, int)
124     */
125    public static final int VIEW_TYPE_DEFAULT = 0;
126
127    /**
128     * ViewHolder caches information about the action item layouts' subviews. Subclasses of {@link
129     * GuidedActionsStylist} may also wish to subclass this in order to add fields.
130     * @see GuidedAction
131     */
132    public static class ViewHolder {
133
134        public final View view;
135
136        private View mContentView;
137        private TextView mTitleView;
138        private TextView mDescriptionView;
139        private ImageView mIconView;
140        private ImageView mCheckmarkView;
141        private ImageView mChevronView;
142        private boolean mInEditing;
143        private boolean mInEditingDescription;
144
145        /**
146         * Constructs an ViewHolder and caches the relevant subviews.
147         */
148        public ViewHolder(View v) {
149            view = v;
150
151            mContentView = v.findViewById(R.id.guidedactions_item_content);
152            mTitleView = (TextView) v.findViewById(R.id.guidedactions_item_title);
153            mDescriptionView = (TextView) v.findViewById(R.id.guidedactions_item_description);
154            mIconView = (ImageView) v.findViewById(R.id.guidedactions_item_icon);
155            mCheckmarkView = (ImageView) v.findViewById(R.id.guidedactions_item_checkmark);
156            mChevronView = (ImageView) v.findViewById(R.id.guidedactions_item_chevron);
157        }
158
159        /**
160         * Returns the content view within this view holder's view, where title and description are
161         * shown.
162         */
163        public View getContentView() {
164            return mContentView;
165        }
166
167        /**
168         * Returns the title view within this view holder's view.
169         */
170        public TextView getTitleView() {
171            return mTitleView;
172        }
173
174        /**
175         * Convenience method to return an editable version of the title, if possible,
176         * or null if the title view isn't an EditText.
177         */
178        public EditText getEditableTitleView() {
179            return (mTitleView instanceof EditText) ? (EditText)mTitleView : null;
180        }
181
182        /**
183         * Returns the description view within this view holder's view.
184         */
185        public TextView getDescriptionView() {
186            return mDescriptionView;
187        }
188
189        /**
190         * Convenience method to return an editable version of the description, if possible,
191         * or null if the description view isn't an EditText.
192         */
193        public EditText getEditableDescriptionView() {
194            return (mDescriptionView instanceof EditText) ? (EditText)mDescriptionView : null;
195        }
196
197        /**
198         * Returns the icon view within this view holder's view.
199         */
200        public ImageView getIconView() {
201            return mIconView;
202        }
203
204        /**
205         * Returns the checkmark view within this view holder's view.
206         */
207        public ImageView getCheckmarkView() {
208            return mCheckmarkView;
209        }
210
211        /**
212         * Returns the chevron view within this view holder's view.
213         */
214        public ImageView getChevronView() {
215            return mChevronView;
216        }
217
218        /**
219         * Returns true if the TextView is in editing title or description, false otherwise.
220         */
221        public boolean isInEditing() {
222            return mInEditing;
223        }
224
225        /**
226         * Returns true if the TextView is in editing description, false otherwise.
227         */
228        public boolean isInEditingDescription() {
229            return mInEditingDescription;
230        }
231
232        public View getEditingView() {
233            if (mInEditing) {
234                return mInEditingDescription ?  mDescriptionView : mTitleView;
235            } else {
236                return null;
237            }
238        }
239    }
240
241    private static String TAG = "GuidedActionsStylist";
242
243    private View mMainView;
244    private VerticalGridView mActionsGridView;
245    private View mBgView;
246    private View mSelectorView;
247    private boolean mButtonActions;
248
249    // Cached values from resources
250    private float mEnabledTextAlpha;
251    private float mDisabledTextAlpha;
252    private float mEnabledDescriptionAlpha;
253    private float mDisabledDescriptionAlpha;
254    private float mEnabledChevronAlpha;
255    private float mDisabledChevronAlpha;
256    private int mTitleMinLines;
257    private int mTitleMaxLines;
258    private int mDescriptionMinLines;
259    private int mVerticalPadding;
260    private int mDisplayHeight;
261
262    /**
263     * Creates a view appropriate for displaying a list of GuidedActions, using the provided
264     * inflater and container.
265     * <p>
266     * <i>Note: Does not actually add the created view to the container; the caller should do
267     * this.</i>
268     * @param inflater The layout inflater to be used when constructing the view.
269     * @param container The view group to be passed in the call to
270     * <code>LayoutInflater.inflate</code>.
271     * @return The view to be added to the caller's view hierarchy.
272     */
273    public View onCreateView(LayoutInflater inflater, ViewGroup container) {
274        mMainView = inflater.inflate(onProvideLayoutId(), container, false);
275        mSelectorView = mMainView.findViewById(R.id.guidedactions_selector);
276        mSelectorView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
277            @Override
278            public void onLayoutChange(View v, int left, int top, int right, int bottom,
279                    int oldLeft, int oldTop, int oldRight, int oldBottom) {
280                final View focusedChild = mActionsGridView.getFocusedChild();
281                if (focusedChild != null && mSelectorView.getVisibility() == View.VISIBLE &&
282                        mSelectorView.getHeight() > 0) {
283                    mSelectorView.setScaleY((float) focusedChild.getHeight()
284                            / mSelectorView.getHeight());
285                }
286            }
287        });
288        mBgView = mMainView.findViewById(R.id.guided_button_actions_background);
289        if (mMainView instanceof VerticalGridView) {
290            mActionsGridView = (VerticalGridView) mMainView;
291        } else {
292            mActionsGridView = (VerticalGridView) mMainView.findViewById(R.id.guidedactions_list);
293            if (mActionsGridView == null) {
294                throw new IllegalStateException("No ListView exists.");
295            }
296            mActionsGridView.setWindowAlignmentOffset(0);
297            mActionsGridView.setWindowAlignmentOffsetPercent(50f);
298            mActionsGridView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE);
299            if (mSelectorView != null) {
300                mActionsGridView.setOnScrollListener(new
301                        SelectorAnimator(mSelectorView, mActionsGridView));
302            }
303        }
304
305        if (mSelectorView != null) {
306            // ALlow focus to move to other views
307            mActionsGridView.getViewTreeObserver().addOnGlobalFocusChangeListener(
308                    mGlobalFocusChangeListener);
309        }
310
311        // Cache widths, chevron alpha values, max and min text lines, etc
312        Context ctx = mMainView.getContext();
313        TypedValue val = new TypedValue();
314        mEnabledChevronAlpha = getFloat(ctx, val, R.attr.guidedActionEnabledChevronAlpha);
315        mDisabledChevronAlpha = getFloat(ctx, val, R.attr.guidedActionDisabledChevronAlpha);
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     * Default implementation turns on background for actions and applies different Ids to views so
336     * that GuidedStepFragment could run transitions against two action lists.  The method is called
337     * by GuidedStepFragment, app may override this function when replacing default layout file
338     * provided by {@link #onProvideLayoutId()}
339     */
340    public void setAsButtonActions() {
341        mButtonActions = true;
342        mMainView.setId(R.id.guidedactions_root2);
343        ViewCompat.setTransitionName(mMainView, "guidedactions_root");
344        mActionsGridView.setId(R.id.guidedactions_list2);
345        mSelectorView.setId(R.id.guidedactions_selector2);
346        ViewCompat.setTransitionName(mSelectorView, "guidedactions_selector2");
347        mBgView.setId(R.id.guided_button_actions_background2);
348        ViewCompat.setTransitionName(mBgView, "guided_button_actions_background2");
349        mBgView.setVisibility(View.VISIBLE);
350    }
351
352    /**
353     * Returns true if {@link #setAsButtonActions()} was called, false otherwise.
354     * @return True if {@link #setAsButtonActions()} was called, false otherwise.
355     */
356    public boolean isButtonActions() {
357        return mButtonActions;
358    }
359
360    final ViewTreeObserver.OnGlobalFocusChangeListener mGlobalFocusChangeListener =
361            new ViewTreeObserver.OnGlobalFocusChangeListener() {
362        private boolean mChildFocused;
363
364        @Override
365        public void onGlobalFocusChanged(View oldFocus, View newFocus) {
366            final View focusedChild = mActionsGridView.getFocusedChild();
367            if (focusedChild == null) {
368                mSelectorView.setVisibility(View.INVISIBLE);
369                mChildFocused = false;
370            } else if (!mChildFocused) {
371                mChildFocused = true;
372                mSelectorView.setVisibility(View.VISIBLE);
373                if (mSelectorView.getHeight() > 0) {
374                    mSelectorView.setScaleY((float) focusedChild.getHeight()
375                            / mSelectorView.getHeight());
376                }
377            }
378        }
379    };
380
381    /**
382     * Called when destroy the View created by GuidedActionsStylist.
383     */
384    public void onDestroyView() {
385        if (mSelectorView != null) {
386            mActionsGridView.getViewTreeObserver().removeOnGlobalFocusChangeListener(
387                    mGlobalFocusChangeListener);
388        }
389        mActionsGridView = null;
390        mSelectorView = null;
391        mBgView = null;
392        mMainView = null;
393    }
394
395    /**
396     * Returns the VerticalGridView that displays the list of GuidedActions.
397     * @return The VerticalGridView for this presenter.
398     */
399    public VerticalGridView getActionsGridView() {
400        return mActionsGridView;
401    }
402
403    /**
404     * Provides the resource ID of the layout defining the host view for the list of 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}. If overridden, the
407     * substituted layout should contain matching IDs for any views that should be managed by the
408     * base class; this can be achieved by starting with a copy of the base layout file.
409     * @return The resource ID of the layout to be inflated to define the host view for the list
410     * of GuidedActions.
411     */
412    public int onProvideLayoutId() {
413        return R.layout.lb_guidedactions;
414    }
415
416    /**
417     * Return view type of action, each different type can have differently associated layout Id.
418     * Default implementation returns {@link #VIEW_TYPE_DEFAULT}.
419     * @param action  The action object.
420     * @return View type that used in {@link #onProvideItemLayoutId(int)}.
421     */
422    public int getItemViewType(GuidedAction action) {
423        return VIEW_TYPE_DEFAULT;
424    }
425
426    /**
427     * Provides the resource ID of the layout defining the view for an individual guided actions.
428     * Subclasses may override to provide their own customized layouts. The base implementation
429     * returns {@link android.support.v17.leanback.R.layout#lb_guidedactions_item}. If overridden,
430     * the substituted layout should contain matching IDs for any views that should be managed by
431     * the base class; this can be achieved by starting with a copy of the base layout file. Note
432     * that in order for the item to support editing, the title view should both subclass {@link
433     * android.widget.EditText} and implement {@link ImeKeyMonitor}; see {@link
434     * GuidedActionEditText}.  To support different types of Layouts, override {@link
435     * #onProvideItemLayoutId(int)}.
436     * @return The resource ID of the layout to be inflated to define the view to display an
437     * individual GuidedAction.
438     */
439    public int onProvideItemLayoutId() {
440        return R.layout.lb_guidedactions_item;
441    }
442
443    /**
444     * Provides the resource ID of the layout defining the view for an individual guided actions.
445     * Subclasses may override to provide their own customized layouts. The base implementation
446     * returns {@link android.support.v17.leanback.R.layout#lb_guidedactions_item}. If overridden,
447     * the substituted layout should contain matching IDs for any views that should be managed by
448     * the base class; this can be achieved by starting with a copy of the base layout file. Note
449     * that in order for the item to support editing, the title view should both subclass {@link
450     * android.widget.EditText} and implement {@link ImeKeyMonitor}; see {@link
451     * GuidedActionEditText}.
452     * @param viewType View type returned by {@link #getItemViewType(GuidedAction)}
453     * @return The resource ID of the layout to be inflated to define the view to display an
454     * individual GuidedAction.
455     */
456    public int onProvideItemLayoutId(int viewType) {
457        if (viewType == VIEW_TYPE_DEFAULT) {
458            return onProvideItemLayoutId();
459        } else {
460            throw new RuntimeException("ViewType " + viewType +
461                    " not supported in GuidedActionsStylist");
462        }
463    }
464
465    /**
466     * Constructs a {@link ViewHolder} capable of representing {@link GuidedAction}s. Subclasses
467     * may choose to return a subclass of ViewHolder.  To support different view types, override
468     * {@link #onCreateViewHolder(ViewGroup, int)}
469     * <p>
470     * <i>Note: Should not actually add the created view to the parent; the caller will do
471     * this.</i>
472     * @param parent The view group to be used as the parent of the new view.
473     * @return The view to be added to the caller's view hierarchy.
474     */
475    public ViewHolder onCreateViewHolder(ViewGroup parent) {
476        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
477        View v = inflater.inflate(onProvideItemLayoutId(), parent, false);
478        return new ViewHolder(v);
479    }
480
481    /**
482     * Constructs a {@link ViewHolder} capable of representing {@link GuidedAction}s. Subclasses
483     * may choose to return a subclass of ViewHolder.
484     * <p>
485     * <i>Note: Should not actually add the created view to the parent; the caller will do
486     * this.</i>
487     * @param parent The view group to be used as the parent of the new view.
488     * @param viewType The viewType returned by {@link #getItemViewType(GuidedAction)}
489     * @return The view to be added to the caller's view hierarchy.
490     */
491    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
492        if (viewType == VIEW_TYPE_DEFAULT) {
493            return onCreateViewHolder(parent);
494        }
495        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
496        View v = inflater.inflate(onProvideItemLayoutId(viewType), parent, false);
497        return new ViewHolder(v);
498    }
499
500    /**
501     * Binds a {@link ViewHolder} to a particular {@link GuidedAction}.
502     * @param vh The view holder to be associated with the given action.
503     * @param action The guided action to be displayed by the view holder's view.
504     * @return The view to be added to the caller's view hierarchy.
505     */
506    public void onBindViewHolder(ViewHolder vh, GuidedAction action) {
507
508        if (vh.mTitleView != null) {
509            vh.mTitleView.setText(action.getTitle());
510            vh.mTitleView.setAlpha(action.isEnabled() ? mEnabledTextAlpha : mDisabledTextAlpha);
511            vh.mTitleView.setFocusable(action.isEditable());
512        }
513        if (vh.mDescriptionView != null) {
514            vh.mDescriptionView.setText(action.getDescription());
515            vh.mDescriptionView.setVisibility(TextUtils.isEmpty(action.getDescription()) ?
516                    View.GONE : View.VISIBLE);
517            vh.mDescriptionView.setAlpha(action.isEnabled() ? mEnabledDescriptionAlpha :
518                mDisabledDescriptionAlpha);
519            vh.mDescriptionView.setFocusable(action.isDescriptionEditable());
520        }
521        // Clients might want the check mark view to be gone entirely, in which case, ignore it.
522        if (vh.mCheckmarkView != null && vh.mCheckmarkView.getVisibility() != View.GONE) {
523            vh.mCheckmarkView.setVisibility(action.isChecked() ? View.VISIBLE : View.INVISIBLE);
524        }
525
526        if (vh.mChevronView != null) {
527            vh.mChevronView.setVisibility(action.hasNext() ? View.VISIBLE : View.GONE);
528            vh.mChevronView.setAlpha(action.isEnabled() ? mEnabledChevronAlpha :
529                    mDisabledChevronAlpha);
530        }
531
532        if (action.hasMultilineDescription()) {
533            if (vh.mTitleView != null) {
534                vh.mTitleView.setMaxLines(mTitleMaxLines);
535                if (vh.mDescriptionView != null) {
536                    vh.mDescriptionView.setMaxHeight(getDescriptionMaxHeight(vh.view.getContext(),
537                            vh.mTitleView));
538                }
539            }
540        } else {
541            if (vh.mTitleView != null) {
542                vh.mTitleView.setMaxLines(mTitleMinLines);
543            }
544            if (vh.mDescriptionView != null) {
545                vh.mDescriptionView.setMaxLines(mDescriptionMinLines);
546            }
547        }
548        setEditingMode(vh, action, false);
549        if (action.isFocusable()) {
550            vh.view.setFocusable(true);
551            if (vh.view instanceof ViewGroup) {
552                ((ViewGroup) vh.view).setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
553            }
554        } else {
555            vh.view.setFocusable(false);
556            if (vh.view instanceof ViewGroup) {
557                ((ViewGroup) vh.view).setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
558            }
559        }
560    }
561
562    public void setEditingMode(ViewHolder vh, GuidedAction action, boolean editing) {
563        if (editing != vh.mInEditing) {
564            vh.mInEditing = editing;
565            onEditingModeChange(vh, action, editing);
566        }
567    }
568
569    protected void onEditingModeChange(ViewHolder vh, GuidedAction action, boolean editing) {
570        TextView titleView = vh.getTitleView();
571        TextView descriptionView = vh.getDescriptionView();
572        if (editing) {
573            CharSequence editTitle = action.getEditTitle();
574            if (titleView != null && editTitle != null) {
575                titleView.setText(editTitle);
576            }
577            CharSequence editDescription = action.getEditDescription();
578            if (descriptionView != null && editDescription != null) {
579                descriptionView.setText(editDescription);
580            }
581            if (action.isDescriptionEditable()) {
582                if (descriptionView != null) {
583                    descriptionView.setVisibility(View.VISIBLE);
584                    descriptionView.setInputType(action.getDescriptionEditInputType());
585                }
586                vh.mInEditingDescription = true;
587            } else {
588                vh.mInEditingDescription = false;
589                if (titleView != null) {
590                    titleView.setInputType(action.getEditInputType());
591                }
592            }
593        } else {
594            if (titleView != null) {
595                titleView.setText(action.getTitle());
596            }
597            if (descriptionView != null) {
598                descriptionView.setText(action.getDescription());
599            }
600            if (vh.mInEditingDescription) {
601                if (descriptionView != null) {
602                    descriptionView.setVisibility(TextUtils.isEmpty(action.getDescription()) ?
603                            View.GONE : View.VISIBLE);
604                    descriptionView.setInputType(action.getDescriptionInputType());
605                }
606                vh.mInEditingDescription = false;
607            } else {
608                if (titleView != null) {
609                    titleView.setInputType(action.getInputType());
610                }
611            }
612        }
613    }
614
615    /**
616     * Animates the view holder's view (or subviews thereof) when the action has had its focus
617     * state changed.
618     * @param vh The view holder associated with the relevant action.
619     * @param focused True if the action has become focused, false if it has lost focus.
620     */
621    public void onAnimateItemFocused(ViewHolder vh, boolean focused) {
622        // No animations for this, currently, because the animation is done on
623        // mSelectorView
624    }
625
626    /**
627     * Animates the view holder's view (or subviews thereof) when the action has had its press
628     * state changed.
629     * @param vh The view holder associated with the relevant action.
630     * @param pressed True if the action has been pressed, false if it has been unpressed.
631     */
632    public void onAnimateItemPressed(ViewHolder vh, boolean pressed) {
633        int attr = pressed ? R.attr.guidedActionPressedAnimation :
634                R.attr.guidedActionUnpressedAnimation;
635        createAnimator(vh.view, attr).start();
636    }
637
638    /**
639     * Resets the view holder's view to unpressed state.
640     * @param vh The view holder associated with the relevant action.
641     */
642    public void onAnimateItemPressedCancelled(ViewHolder vh) {
643        createAnimator(vh.view, R.attr.guidedActionUnpressedAnimation).end();
644    }
645
646    /**
647     * Animates the view holder's view (or subviews thereof) when the action has had its check
648     * state changed.
649     * @param vh The view holder associated with the relevant action.
650     * @param checked True if the action has become checked, false if it has become unchecked.
651     */
652    public void onAnimateItemChecked(ViewHolder vh, boolean checked) {
653        final View checkView = vh.mCheckmarkView;
654        if (checkView != null) {
655            if (checked) {
656                checkView.setVisibility(View.VISIBLE);
657                createAnimator(checkView, R.attr.guidedActionCheckedAnimation).start();
658            } else {
659                Animator animator = createAnimator(checkView,
660                        R.attr.guidedActionUncheckedAnimation);
661                animator.addListener(new AnimatorListenerAdapter() {
662                    @Override
663                    public void onAnimationEnd(Animator animation) {
664                        checkView.setVisibility(View.INVISIBLE);
665                    }
666                });
667                animator.start();
668            }
669        }
670    }
671
672    /*
673     * ==========================================
674     * FragmentAnimationProvider overrides
675     * ==========================================
676     */
677
678    /**
679     * {@inheritDoc}
680     */
681    @Override
682    public void onImeAppearing(@NonNull List<Animator> animators) {
683        animators.add(createAnimator(mActionsGridView, R.attr.guidedStepImeAppearingAnimation));
684        animators.add(createAnimator(mSelectorView, R.attr.guidedStepImeAppearingAnimation));
685    }
686
687    /**
688     * {@inheritDoc}
689     */
690    @Override
691    public void onImeDisappearing(@NonNull List<Animator> animators) {
692        animators.add(createAnimator(mActionsGridView, R.attr.guidedStepImeDisappearingAnimation));
693        animators.add(createAnimator(mSelectorView, R.attr.guidedStepImeDisappearingAnimation));
694    }
695
696    /*
697     * ==========================================
698     * Private methods
699     * ==========================================
700     */
701
702    private float getFloat(Context ctx, TypedValue typedValue, int attrId) {
703        ctx.getTheme().resolveAttribute(attrId, typedValue, true);
704        // Android resources don't have a native float type, so we have to use strings.
705        return Float.valueOf(ctx.getResources().getString(typedValue.resourceId));
706    }
707
708    private int getInteger(Context ctx, TypedValue typedValue, int attrId) {
709        ctx.getTheme().resolveAttribute(attrId, typedValue, true);
710        return ctx.getResources().getInteger(typedValue.resourceId);
711    }
712
713    private int getDimension(Context ctx, TypedValue typedValue, int attrId) {
714        ctx.getTheme().resolveAttribute(attrId, typedValue, true);
715        return ctx.getResources().getDimensionPixelSize(typedValue.resourceId);
716    }
717
718    private static Animator createAnimator(View v, int attrId) {
719        Context ctx = v.getContext();
720        TypedValue typedValue = new TypedValue();
721        ctx.getTheme().resolveAttribute(attrId, typedValue, true);
722        Animator animator = AnimatorInflater.loadAnimator(ctx, typedValue.resourceId);
723        animator.setTarget(v);
724        return animator;
725    }
726
727    private boolean setIcon(final ImageView iconView, GuidedAction action) {
728        Drawable icon = null;
729        if (iconView != null) {
730            Context context = iconView.getContext();
731            icon = action.getIcon();
732            if (icon != null) {
733                // setImageDrawable resets the drawable's level unless we set the view level first.
734                iconView.setImageLevel(icon.getLevel());
735                iconView.setImageDrawable(icon);
736                iconView.setVisibility(View.VISIBLE);
737            } else {
738                iconView.setVisibility(View.GONE);
739            }
740        }
741        return icon != null;
742    }
743
744    /**
745     * @return the max height in pixels the description can be such that the
746     *         action nicely takes up the entire screen.
747     */
748    private int getDescriptionMaxHeight(Context context, TextView title) {
749        // The 2 multiplier on the title height calculation is a
750        // conservative estimate for font padding which can not be
751        // calculated at this stage since the view hasn't been rendered yet.
752        return (int)(mDisplayHeight - 2*mVerticalPadding - 2*mTitleMaxLines*title.getLineHeight());
753    }
754
755    /**
756     * SelectorAnimator
757     * Controls animation for selected item backgrounds
758     * TODO: Move into focus animation override?
759     */
760    private static class SelectorAnimator extends RecyclerView.OnScrollListener {
761
762        private final View mSelectorView;
763        private final ViewGroup mParentView;
764        private volatile boolean mFadedOut = true;
765
766        SelectorAnimator(View selectorView, ViewGroup parentView) {
767            mSelectorView = selectorView;
768            mParentView = parentView;
769        }
770
771        // We want to fade in the selector if we've stopped scrolling on it. If
772        // we're scrolling, we want to ensure to dim the selector if we haven't
773        // already. We dim the last highlighted view so that while a user is
774        // scrolling, nothing is highlighted.
775        @Override
776        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
777            Animator animator = null;
778            boolean fadingOut = false;
779            if (newState == RecyclerView.SCROLL_STATE_IDLE) {
780                // The selector starts with a height of 0. In order to scale up from
781                // 0, we first need the set the height to 1 and scale from there.
782                View focusedChild = mParentView.getFocusedChild();
783                if (focusedChild != null) {
784                    int selectorHeight = mSelectorView.getHeight();
785                    float scaleY = (float) focusedChild.getHeight() / selectorHeight;
786                    AnimatorSet animators = (AnimatorSet)createAnimator(mSelectorView,
787                            R.attr.guidedActionsSelectorShowAnimation);
788                    if (mFadedOut) {
789                        // selector is completely faded out, so we can just scale before fading in.
790                        mSelectorView.setScaleY(scaleY);
791                        animator = animators.getChildAnimations().get(0);
792                    } else {
793                        // selector is not faded out, so we must animate the scale as we fade in.
794                        ((ObjectAnimator)animators.getChildAnimations().get(1))
795                                .setFloatValues(scaleY);
796                        animator = animators;
797                    }
798                }
799            } else {
800                animator = createAnimator(mSelectorView, R.attr.guidedActionsSelectorHideAnimation);
801                fadingOut = true;
802            }
803            if (animator != null) {
804                animator.addListener(new Listener(fadingOut));
805                animator.start();
806            }
807        }
808
809        /**
810         * Sets {@link BaseScrollAdapterFragment#mFadedOut}
811         * {@link BaseScrollAdapterFragment#mFadedOut} is true, iff
812         * {@link BaseScrollAdapterFragment#mSelectorView} has an alpha of 0
813         * (faded out). If false the view either has an alpha of 1 (visible) or
814         * is in the process of animating.
815         */
816        private class Listener implements Animator.AnimatorListener {
817            private boolean mFadingOut;
818            private boolean mCanceled;
819
820            public Listener(boolean fadingOut) {
821                mFadingOut = fadingOut;
822            }
823
824            @Override
825            public void onAnimationStart(Animator animation) {
826                if (!mFadingOut) {
827                    mFadedOut = false;
828                }
829            }
830
831            @Override
832            public void onAnimationEnd(Animator animation) {
833                if (!mCanceled && mFadingOut) {
834                    mFadedOut = true;
835                }
836            }
837
838            @Override
839            public void onAnimationCancel(Animator animation) {
840                mCanceled = true;
841            }
842
843            @Override
844            public void onAnimationRepeat(Animator animation) {
845            }
846        }
847    }
848
849}
850