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