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