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