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