GuidedActionsStylist.java revision e2f7aef2f45dcdfe116995b64f9a7be5c68a36a1
1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 * in compliance with the License. You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software distributed under the License
10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 * or implied. See the License for the specific language governing permissions and limitations under
12 * the License.
13 */
14package android.support.v17.leanback.widget;
15
16import android.animation.Animator;
17import android.animation.AnimatorInflater;
18import android.animation.AnimatorListenerAdapter;
19import android.animation.AnimatorSet;
20import android.animation.ObjectAnimator;
21import android.content.Context;
22import android.content.pm.PackageManager;
23import android.content.res.Resources;
24import android.content.res.TypedArray;
25import android.graphics.drawable.Drawable;
26import android.net.Uri;
27import android.support.annotation.NonNull;
28import android.support.v17.leanback.R;
29import android.support.v17.leanback.widget.VerticalGridView;
30import android.support.v7.widget.RecyclerView;
31import android.support.v7.widget.RecyclerView.ViewHolder;
32import android.text.TextUtils;
33import android.util.Log;
34import android.util.TypedValue;
35import android.view.animation.DecelerateInterpolator;
36import android.view.LayoutInflater;
37import android.view.View;
38import android.view.ViewGroup;
39import android.view.ViewGroup.LayoutParams;
40import android.view.ViewPropertyAnimator;
41import android.view.ViewTreeObserver;
42import android.view.WindowManager;
43import android.widget.EditText;
44import android.widget.ImageView;
45import android.widget.TextView;
46
47import java.util.List;
48
49/**
50 * GuidedActionsStylist is used within a {@link android.support.v17.leanback.app.GuidedStepFragment}
51 * to supply the right-side panel where users can take actions. It consists of a container for the
52 * list of actions, and a stationary selector view that indicates visually the location of focus.
53 * <p>
54 * Many aspects of the base GuidedActionsStylist can be customized through theming; see the
55 * theme attributes below. Note that these attributes are not set on individual elements in layout
56 * XML, but instead would be set in a custom theme. See
57 * <a href="http://developer.android.com/guide/topics/ui/themes.html">Styles and Themes</a>
58 * for more information.
59 * <p>
60 * If these hooks are insufficient, this class may also be subclassed. Subclasses may wish to
61 * override the {@link #onProvideLayoutId} method to change the layout used to display the
62 * list container and selector, or the {@link #onProvideItemLayoutId} method to change the layout
63 * used to display each action.
64 * <p>
65 * Note: If an alternate list layout is provided, the following view IDs must be supplied:
66 * <ul>
67 * <li>{@link android.support.v17.leanback.R.id#guidedactions_selector}</li>
68 * <li>{@link android.support.v17.leanback.R.id#guidedactions_list}</li>
69 * </ul><p>
70 * These view IDs must be present in order for the stylist to function. The list ID must correspond
71 * to a {@link VerticalGridView} or subclass.
72 * <p>
73 * If an alternate item layout is provided, the following view IDs should be used to refer to base
74 * elements:
75 * <ul>
76 * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_content}</li>
77 * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_title}</li>
78 * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_description}</li>
79 * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_icon}</li>
80 * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_checkmark}</li>
81 * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_chevron}</li>
82 * </ul><p>
83 * These view IDs are allowed to be missing, in which case the corresponding views in {@link
84 * GuidedActionsStylist.ViewHolder} will be null.
85 * <p>
86 * In order to support editable actions, the view associated with guidedactions_item_title should
87 * be a subclass of {@link android.widget.EditText}, and should satisfy the {@link
88 * ImeKeyMonitor} interface.
89 *
90 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedStepImeAppearingAnimation
91 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedStepImeDisappearingAnimation
92 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsSelectorShowAnimation
93 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsSelectorHideAnimation
94 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsContainerStyle
95 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsSelectorStyle
96 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsListStyle
97 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemContainerStyle
98 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemCheckmarkStyle
99 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemIconStyle
100 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemContentStyle
101 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemTitleStyle
102 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemDescriptionStyle
103 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemChevronStyle
104 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionCheckedAnimation
105 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionUncheckedAnimation
106 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionPressedAnimation
107 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionUnpressedAnimation
108 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionEnabledChevronAlpha
109 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionDisabledChevronAlpha
110 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionContentWidth
111 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionContentWidthNoIcon
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.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    protected View mMainView;
246    protected VerticalGridView mActionsGridView;
247    protected View mSelectorView;
248
249    // Cached values from resources
250    private float mEnabledTextAlpha;
251    private float mDisabledTextAlpha;
252    private float mEnabledDescriptionAlpha;
253    private float mDisabledDescriptionAlpha;
254    private float mEnabledChevronAlpha;
255    private float mDisabledChevronAlpha;
256    private int mContentWidth;
257    private int mContentWidthNoIcon;
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        if (mMainView instanceof VerticalGridView) {
279            mActionsGridView = (VerticalGridView) mMainView;
280        } else {
281            mActionsGridView = (VerticalGridView) mMainView.findViewById(R.id.guidedactions_list);
282            if (mActionsGridView == null) {
283                throw new IllegalStateException("No ListView exists.");
284            }
285            mActionsGridView.setWindowAlignmentOffset(0);
286            mActionsGridView.setWindowAlignmentOffsetPercent(50f);
287            mActionsGridView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE);
288            if (mSelectorView != null) {
289                mActionsGridView.setOnScrollListener(new
290                        SelectorAnimator(mSelectorView, mActionsGridView));
291            }
292        }
293
294        mActionsGridView.requestFocusFromTouch();
295
296        if (mSelectorView != null) {
297            // ALlow focus to move to other views
298            mActionsGridView.getViewTreeObserver().addOnGlobalFocusChangeListener(
299                    new ViewTreeObserver.OnGlobalFocusChangeListener() {
300                        private boolean mChildFocused;
301
302                        @Override
303                        public void onGlobalFocusChanged(View oldFocus, View newFocus) {
304                            View focusedChild = mActionsGridView.getFocusedChild();
305                            if (focusedChild == null) {
306                                mSelectorView.setVisibility(View.INVISIBLE);
307                                mChildFocused = false;
308                            } else if (!mChildFocused) {
309                                mChildFocused = true;
310                                mSelectorView.setVisibility(View.VISIBLE);
311                                updateSelectorView(focusedChild);
312                            }
313                        }
314                    });
315        }
316
317        // Cache widths, chevron alpha values, max and min text lines, etc
318        Context ctx = mMainView.getContext();
319        TypedValue val = new TypedValue();
320        mEnabledChevronAlpha = getFloat(ctx, val, R.attr.guidedActionEnabledChevronAlpha);
321        mDisabledChevronAlpha = getFloat(ctx, val, R.attr.guidedActionDisabledChevronAlpha);
322        mContentWidth = getDimension(ctx, val, R.attr.guidedActionContentWidth);
323        mContentWidthNoIcon = getDimension(ctx, val, R.attr.guidedActionContentWidthNoIcon);
324        mTitleMinLines = getInteger(ctx, val, R.attr.guidedActionTitleMinLines);
325        mTitleMaxLines = getInteger(ctx, val, R.attr.guidedActionTitleMaxLines);
326        mDescriptionMinLines = getInteger(ctx, val, R.attr.guidedActionDescriptionMinLines);
327        mVerticalPadding = getDimension(ctx, val, R.attr.guidedActionVerticalPadding);
328        mDisplayHeight = ((WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE))
329                .getDefaultDisplay().getHeight();
330
331        mEnabledTextAlpha = Float.valueOf(ctx.getResources().getString(R.string
332                .lb_guidedactions_item_unselected_text_alpha));
333        mDisabledTextAlpha = Float.valueOf(ctx.getResources().getString(R.string
334                .lb_guidedactions_item_disabled_text_alpha));
335        mEnabledDescriptionAlpha = Float.valueOf(ctx.getResources().getString(R.string
336                .lb_guidedactions_item_unselected_description_text_alpha));
337        mDisabledDescriptionAlpha = Float.valueOf(ctx.getResources().getString(R.string
338                .lb_guidedactions_item_disabled_description_text_alpha));
339        return mMainView;
340    }
341
342    /**
343     * Returns the VerticalGridView that displays the list of GuidedActions.
344     * @return The VerticalGridView for this presenter.
345     */
346    public VerticalGridView getActionsGridView() {
347        return mActionsGridView;
348    }
349
350    /**
351     * Provides the resource ID of the layout defining the host view for the list of guided actions.
352     * Subclasses may override to provide their own customized layouts. The base implementation
353     * returns {@link android.support.v17.leanback.R.layout#lb_guidedactions}. If overridden, the
354     * substituted layout should contain matching IDs for any views that should be managed by the
355     * base class; this can be achieved by starting with a copy of the base layout file.
356     * @return The resource ID of the layout to be inflated to define the host view for the list
357     * of GuidedActions.
358     */
359    public int onProvideLayoutId() {
360        return R.layout.lb_guidedactions;
361    }
362
363    /**
364     * Return view type of action, each different type can have differently associated layout Id.
365     * Default implementation returns {@link #VIEW_TYPE_DEFAULT}.
366     * @param action  The action object.
367     * @return View type that used in {@link #onProvideItemLayoutId(int)}.
368     */
369    public int getItemViewType(GuidedAction action) {
370        return VIEW_TYPE_DEFAULT;
371    }
372
373    /**
374     * Provides the resource ID of the layout defining the view for an individual guided actions.
375     * Subclasses may override to provide their own customized layouts. The base implementation
376     * returns {@link android.support.v17.leanback.R.layout#lb_guidedactions_item}. If overridden,
377     * the substituted layout should contain matching IDs for any views that should be managed by
378     * the base class; this can be achieved by starting with a copy of the base layout file. Note
379     * that in order for the item to support editing, the title view should both subclass {@link
380     * android.widget.EditText} and implement {@link ImeKeyMonitor}; see {@link
381     * GuidedActionEditText}.  To support different types of Layouts, override {@link
382     * #onProvideItemLayoutId(int)}.
383     * @return The resource ID of the layout to be inflated to define the view to display an
384     * individual GuidedAction.
385     */
386    public int onProvideItemLayoutId() {
387        return R.layout.lb_guidedactions_item;
388    }
389
390    /**
391     * Provides the resource ID of the layout defining the view for an individual guided actions.
392     * Subclasses may override to provide their own customized layouts. The base implementation
393     * returns {@link android.support.v17.leanback.R.layout#lb_guidedactions_item}. If overridden,
394     * the substituted layout should contain matching IDs for any views that should be managed by
395     * the base class; this can be achieved by starting with a copy of the base layout file. Note
396     * that in order for the item to support editing, the title view should both subclass {@link
397     * android.widget.EditText} and implement {@link ImeKeyMonitor}; see {@link
398     * GuidedActionEditText}.
399     * @param viewType View type returned by {@link #getItemViewType(GuidedAction)}
400     * @return The resource ID of the layout to be inflated to define the view to display an
401     * individual GuidedAction.
402     */
403    public int onProvideItemLayoutId(int viewType) {
404        if (viewType == VIEW_TYPE_DEFAULT) {
405            return onProvideItemLayoutId();
406        } else {
407            throw new RuntimeException("ViewType " + viewType +
408                    " not supported in GuidedActionsStylist");
409        }
410    }
411
412    /**
413     * Constructs a {@link ViewHolder} capable of representing {@link GuidedAction}s. Subclasses
414     * may choose to return a subclass of ViewHolder.  To support different view types, override
415     * {@link #onCreateViewHolder(ViewGroup, int)}
416     * <p>
417     * <i>Note: Should not actually add the created view to the parent; the caller will do
418     * this.</i>
419     * @param parent The view group to be used as the parent of the new view.
420     * @return The view to be added to the caller's view hierarchy.
421     */
422    public ViewHolder onCreateViewHolder(ViewGroup parent) {
423        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
424        View v = inflater.inflate(onProvideItemLayoutId(), parent, false);
425        return new ViewHolder(v);
426    }
427
428    /**
429     * Constructs a {@link ViewHolder} capable of representing {@link GuidedAction}s. Subclasses
430     * may choose to return a subclass of ViewHolder.
431     * <p>
432     * <i>Note: Should not actually add the created view to the parent; the caller will do
433     * this.</i>
434     * @param parent The view group to be used as the parent of the new view.
435     * @param viewType The viewType returned by {@link #getItemViewType(GuidedAction)}
436     * @return The view to be added to the caller's view hierarchy.
437     */
438    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
439        if (viewType == VIEW_TYPE_DEFAULT) {
440            return onCreateViewHolder(parent);
441        }
442        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
443        View v = inflater.inflate(onProvideItemLayoutId(viewType), parent, false);
444        return new ViewHolder(v);
445    }
446
447    /**
448     * Binds a {@link ViewHolder} to a particular {@link GuidedAction}.
449     * @param vh The view holder to be associated with the given action.
450     * @param action The guided action to be displayed by the view holder's view.
451     * @return The view to be added to the caller's view hierarchy.
452     */
453    public void onBindViewHolder(ViewHolder vh, GuidedAction action) {
454
455        if (vh.mTitleView != null) {
456            vh.mTitleView.setText(action.getTitle());
457            vh.mTitleView.setAlpha(action.isEnabled() ? mEnabledTextAlpha : mDisabledTextAlpha);
458        }
459        if (vh.mDescriptionView != null) {
460            vh.mDescriptionView.setText(action.getDescription());
461            vh.mDescriptionView.setVisibility(TextUtils.isEmpty(action.getDescription()) ?
462                    View.GONE : View.VISIBLE);
463            vh.mDescriptionView.setAlpha(action.isEnabled() ? mEnabledDescriptionAlpha :
464                mDisabledDescriptionAlpha);
465        }
466        // Clients might want the check mark view to be gone entirely, in which case, ignore it.
467        if (vh.mCheckmarkView != null && vh.mCheckmarkView.getVisibility() != View.GONE) {
468            vh.mCheckmarkView.setVisibility(action.isChecked() ? View.VISIBLE : View.INVISIBLE);
469        }
470
471        if (vh.mContentView != null) {
472            ViewGroup.LayoutParams contentLp = vh.mContentView.getLayoutParams();
473            if (setIcon(vh.mIconView, action)) {
474                contentLp.width = mContentWidth;
475            } else {
476                contentLp.width = mContentWidthNoIcon;
477            }
478            vh.mContentView.setLayoutParams(contentLp);
479        }
480
481        if (vh.mChevronView != null) {
482            vh.mChevronView.setVisibility(action.hasNext() ? View.VISIBLE : View.INVISIBLE);
483            vh.mChevronView.setAlpha(action.isEnabled() ? mEnabledChevronAlpha :
484                    mDisabledChevronAlpha);
485        }
486
487        if (action.hasMultilineDescription()) {
488            if (vh.mTitleView != null) {
489                vh.mTitleView.setMaxLines(mTitleMaxLines);
490                if (vh.mDescriptionView != null) {
491                    vh.mDescriptionView.setMaxHeight(getDescriptionMaxHeight(vh.view.getContext(),
492                            vh.mTitleView));
493                }
494            }
495        } else {
496            if (vh.mTitleView != null) {
497                vh.mTitleView.setMaxLines(mTitleMinLines);
498            }
499            if (vh.mDescriptionView != null) {
500                vh.mDescriptionView.setMaxLines(mDescriptionMinLines);
501            }
502        }
503        setEditingMode(vh, action, false);
504        if (action.isFocusable()) {
505            vh.view.setFocusable(true);
506            if (vh.view instanceof ViewGroup) {
507                ((ViewGroup) vh.view).setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
508            }
509        } else {
510            vh.view.setFocusable(false);
511            if (vh.view instanceof ViewGroup) {
512                ((ViewGroup) vh.view).setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
513            }
514        }
515    }
516
517    public void setEditingMode(ViewHolder vh, GuidedAction action, boolean editing) {
518        if (editing != vh.mInEditing) {
519            vh.mInEditing = editing;
520            onEditingModeChange(vh, action, editing);
521        }
522    }
523
524    protected void onEditingModeChange(ViewHolder vh, GuidedAction action, boolean editing) {
525        TextView titleView = vh.getTitleView();
526        TextView descriptionView = vh.getDescriptionView();
527        if (editing) {
528            CharSequence editTitle = action.getEditTitle();
529            if (titleView != null && editTitle != null) {
530                titleView.setText(editTitle);
531            }
532            CharSequence editDescription = action.getEditDescription();
533            if (descriptionView != null && editDescription != null) {
534                descriptionView.setText(editDescription);
535            }
536            if (action.isDescriptionEditable()) {
537                if (descriptionView != null) {
538                    descriptionView.setVisibility(View.VISIBLE);
539                    descriptionView.setInputType(action.getDescriptionEditInputType());
540                }
541                vh.mInEditingDescription = true;
542            } else {
543                vh.mInEditingDescription = false;
544                if (titleView != null) {
545                    titleView.setInputType(action.getEditInputType());
546                }
547            }
548        } else {
549            if (titleView != null) {
550                titleView.setText(action.getTitle());
551            }
552            if (descriptionView != null) {
553                descriptionView.setText(action.getDescription());
554            }
555            if (vh.mInEditingDescription) {
556                if (descriptionView != null) {
557                    descriptionView.setVisibility(TextUtils.isEmpty(action.getDescription()) ?
558                            View.GONE : View.VISIBLE);
559                    descriptionView.setInputType(action.getDescriptionInputType());
560                }
561                vh.mInEditingDescription = false;
562            } else {
563                if (titleView != null) {
564                    titleView.setInputType(action.getInputType());
565                }
566            }
567        }
568    }
569
570    /**
571     * Animates the view holder's view (or subviews thereof) when the action has had its focus
572     * state changed.
573     * @param vh The view holder associated with the relevant action.
574     * @param focused True if the action has become focused, false if it has lost focus.
575     */
576    public void onAnimateItemFocused(ViewHolder vh, boolean focused) {
577        // No animations for this, currently, because the animation is done on
578        // mSelectorView
579    }
580
581    /**
582     * Animates the view holder's view (or subviews thereof) when the action has had its press
583     * state changed.
584     * @param vh The view holder associated with the relevant action.
585     * @param pressed True if the action has been pressed, false if it has been unpressed.
586     */
587    public void onAnimateItemPressed(ViewHolder vh, boolean pressed) {
588        int attr = pressed ? R.attr.guidedActionPressedAnimation :
589                R.attr.guidedActionUnpressedAnimation;
590        createAnimator(vh.view, attr).start();
591    }
592
593    /**
594     * Animates the view holder's view (or subviews thereof) when the action has had its check
595     * state changed.
596     * @param vh The view holder associated with the relevant action.
597     * @param checked True if the action has become checked, false if it has become unchecked.
598     */
599    public void onAnimateItemChecked(ViewHolder vh, boolean checked) {
600        final View checkView = vh.mCheckmarkView;
601        if (checkView != null) {
602            if (checked) {
603                checkView.setVisibility(View.VISIBLE);
604                createAnimator(checkView, R.attr.guidedActionCheckedAnimation).start();
605            } else {
606                Animator animator = createAnimator(checkView,
607                        R.attr.guidedActionUncheckedAnimation);
608                animator.addListener(new AnimatorListenerAdapter() {
609                    @Override
610                    public void onAnimationEnd(Animator animation) {
611                        checkView.setVisibility(View.INVISIBLE);
612                    }
613                });
614                animator.start();
615            }
616        }
617    }
618
619    /*
620     * ==========================================
621     * FragmentAnimationProvider overrides
622     * ==========================================
623     */
624
625    /**
626     * {@inheritDoc}
627     */
628    @Override
629    public void onImeAppearing(@NonNull List<Animator> animators) {
630        animators.add(createAnimator(mActionsGridView, R.attr.guidedStepImeAppearingAnimation));
631        animators.add(createAnimator(mSelectorView, R.attr.guidedStepImeAppearingAnimation));
632    }
633
634    /**
635     * {@inheritDoc}
636     */
637    @Override
638    public void onImeDisappearing(@NonNull List<Animator> animators) {
639        animators.add(createAnimator(mActionsGridView, R.attr.guidedStepImeDisappearingAnimation));
640        animators.add(createAnimator(mSelectorView, R.attr.guidedStepImeDisappearingAnimation));
641    }
642
643    /*
644     * ==========================================
645     * Private methods
646     * ==========================================
647     */
648
649    private void updateSelectorView(View focusedChild) {
650        // Display the selector view.
651        int height = focusedChild.getHeight();
652        LayoutParams lp = mSelectorView.getLayoutParams();
653        lp.height = height;
654        mSelectorView.setLayoutParams(lp);
655        mSelectorView.setAlpha(1f);
656    }
657
658    private float getFloat(Context ctx, TypedValue typedValue, int attrId) {
659        ctx.getTheme().resolveAttribute(attrId, typedValue, true);
660        // Android resources don't have a native float type, so we have to use strings.
661        return Float.valueOf(ctx.getResources().getString(typedValue.resourceId));
662    }
663
664    private int getInteger(Context ctx, TypedValue typedValue, int attrId) {
665        ctx.getTheme().resolveAttribute(attrId, typedValue, true);
666        return ctx.getResources().getInteger(typedValue.resourceId);
667    }
668
669    private int getDimension(Context ctx, TypedValue typedValue, int attrId) {
670        ctx.getTheme().resolveAttribute(attrId, typedValue, true);
671        return ctx.getResources().getDimensionPixelSize(typedValue.resourceId);
672    }
673
674    private static Animator createAnimator(View v, int attrId) {
675        Context ctx = v.getContext();
676        TypedValue typedValue = new TypedValue();
677        ctx.getTheme().resolveAttribute(attrId, typedValue, true);
678        Animator animator = AnimatorInflater.loadAnimator(ctx, typedValue.resourceId);
679        animator.setTarget(v);
680        return animator;
681    }
682
683    private boolean setIcon(final ImageView iconView, GuidedAction action) {
684        Drawable icon = null;
685        if (iconView != null) {
686            Context context = iconView.getContext();
687            icon = action.getIcon();
688            if (icon != null) {
689                // setImageDrawable resets the drawable's level unless we set the view level first.
690                iconView.setImageLevel(icon.getLevel());
691                iconView.setImageDrawable(icon);
692                iconView.setVisibility(View.VISIBLE);
693            } else {
694                iconView.setVisibility(View.GONE);
695            }
696        }
697        return icon != null;
698    }
699
700    /**
701     * @return the max height in pixels the description can be such that the
702     *         action nicely takes up the entire screen.
703     */
704    private int getDescriptionMaxHeight(Context context, TextView title) {
705        // The 2 multiplier on the title height calculation is a
706        // conservative estimate for font padding which can not be
707        // calculated at this stage since the view hasn't been rendered yet.
708        return (int)(mDisplayHeight - 2*mVerticalPadding - 2*mTitleMaxLines*title.getLineHeight());
709    }
710
711    /**
712     * SelectorAnimator
713     * Controls animation for selected item backgrounds
714     * TODO: Move into focus animation override?
715     */
716    private static class SelectorAnimator extends RecyclerView.OnScrollListener {
717
718        private final View mSelectorView;
719        private final ViewGroup mParentView;
720        private volatile boolean mFadedOut = true;
721
722        SelectorAnimator(View selectorView, ViewGroup parentView) {
723            mSelectorView = selectorView;
724            mParentView = parentView;
725        }
726
727        // We want to fade in the selector if we've stopped scrolling on it. If
728        // we're scrolling, we want to ensure to dim the selector if we haven't
729        // already. We dim the last highlighted view so that while a user is
730        // scrolling, nothing is highlighted.
731        @Override
732        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
733            Animator animator = null;
734            boolean fadingOut = false;
735            if (newState == RecyclerView.SCROLL_STATE_IDLE) {
736                // The selector starts with a height of 0. In order to scale up from
737                // 0, we first need the set the height to 1 and scale from there.
738                View focusedChild = mParentView.getFocusedChild();
739                if (focusedChild != null) {
740                    int selectorHeight = mSelectorView.getHeight();
741                    float scaleY = (float) focusedChild.getHeight() / selectorHeight;
742                    AnimatorSet animators = (AnimatorSet)createAnimator(mSelectorView,
743                            R.attr.guidedActionsSelectorShowAnimation);
744                    if (mFadedOut) {
745                        // selector is completely faded out, so we can just scale before fading in.
746                        mSelectorView.setScaleY(scaleY);
747                        animator = animators.getChildAnimations().get(0);
748                    } else {
749                        // selector is not faded out, so we must animate the scale as we fade in.
750                        ((ObjectAnimator)animators.getChildAnimations().get(1))
751                                .setFloatValues(scaleY);
752                        animator = animators;
753                    }
754                }
755            } else {
756                animator = createAnimator(mSelectorView, R.attr.guidedActionsSelectorHideAnimation);
757                fadingOut = true;
758            }
759            if (animator != null) {
760                animator.addListener(new Listener(fadingOut));
761                animator.start();
762            }
763        }
764
765        /**
766         * Sets {@link BaseScrollAdapterFragment#mFadedOut}
767         * {@link BaseScrollAdapterFragment#mFadedOut} is true, iff
768         * {@link BaseScrollAdapterFragment#mSelectorView} has an alpha of 0
769         * (faded out). If false the view either has an alpha of 1 (visible) or
770         * is in the process of animating.
771         */
772        private class Listener implements Animator.AnimatorListener {
773            private boolean mFadingOut;
774            private boolean mCanceled;
775
776            public Listener(boolean fadingOut) {
777                mFadingOut = fadingOut;
778            }
779
780            @Override
781            public void onAnimationStart(Animator animation) {
782                if (!mFadingOut) {
783                    mFadedOut = false;
784                }
785            }
786
787            @Override
788            public void onAnimationEnd(Animator animation) {
789                if (!mCanceled && mFadingOut) {
790                    mFadedOut = true;
791                }
792            }
793
794            @Override
795            public void onAnimationCancel(Animator animation) {
796                mCanceled = true;
797            }
798
799            @Override
800            public void onAnimationRepeat(Animator animation) {
801            }
802        }
803    }
804
805}
806