1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.tv.settings.dialog;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ObjectAnimator;
22import android.content.Context;
23import android.content.res.Resources;
24import android.database.DataSetObserver;
25import android.graphics.drawable.Drawable;
26import android.media.AudioManager;
27import android.net.Uri;
28import android.support.v7.widget.RecyclerView;
29import android.support.v7.widget.RecyclerView.ViewHolder;
30import android.text.TextUtils;
31import android.util.Log;
32import android.view.KeyEvent;
33import android.view.LayoutInflater;
34import android.view.View;
35import android.view.ViewGroup;
36import android.view.WindowManager;
37import android.view.animation.DecelerateInterpolator;
38import android.view.animation.Interpolator;
39import android.widget.AdapterView.OnItemSelectedListener;
40import android.widget.ImageView;
41import android.widget.TextView;
42
43import com.android.tv.settings.R;
44import com.android.tv.settings.dialog.DialogFragment.Action;
45import com.android.tv.settings.widget.BitmapWorkerOptions;
46import com.android.tv.settings.widget.DrawableDownloader;
47import com.android.tv.settings.widget.DrawableDownloader.BitmapCallback;
48
49import java.util.ArrayList;
50import java.util.List;
51
52/**
53 * Adapter class which creates actions.
54 *
55 * @hide
56 */
57class DialogActionAdapter extends RecyclerView.Adapter {
58    private static final String TAG = "ActionAdapter";
59    private static final boolean DEBUG = false;
60
61    private final ActionOnKeyPressAnimator mActionOnKeyPressAnimator;
62    private final ActionOnFocusAnimator mActionOnFocusAnimator;
63    private LayoutInflater mInflater;
64    private final List<Action> mActions;
65    private Action.Listener mListener;
66    private final View.OnClickListener mOnClickListener = new View.OnClickListener() {
67        @Override
68        public void onClick(View v) {
69            if (v != null && v.getWindowToken() != null && mListener != null) {
70                mListener.onActionClicked(((ActionViewHolder) v.getTag(R.id.action_title)).getAction());
71            }
72        }
73    };
74
75    public DialogActionAdapter(Action.Listener listener, Action.OnFocusListener onFocusListener,
76            List<Action> actions) {
77        super();
78        mListener = listener;
79        mActions = new ArrayList<Action>(actions);
80        mActionOnKeyPressAnimator = new ActionOnKeyPressAnimator(listener, mActions);
81        mActionOnFocusAnimator = new ActionOnFocusAnimator(onFocusListener);
82    }
83
84    @Override
85    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
86        if (mInflater == null) {
87            mInflater = (LayoutInflater) parent.getContext().getSystemService(
88                    Context.LAYOUT_INFLATER_SERVICE);
89        }
90        View v = mInflater.inflate(R.layout.lb_dialog_action_list_item, parent, false);
91        v.setTag(R.layout.lb_dialog_action_list_item, parent);
92        return new ActionViewHolder(v, mActionOnKeyPressAnimator, mActionOnFocusAnimator, mOnClickListener);
93    }
94
95    @Override
96    public void onBindViewHolder(RecyclerView.ViewHolder baseHolder, int position) {
97        ActionViewHolder holder = (ActionViewHolder) baseHolder;
98
99        if (position >= mActions.size()) {
100            return;
101        }
102
103        holder.init(mActions.get(position));
104    }
105
106    @Override
107    public int getItemCount() {
108        return mActions.size();
109    }
110
111    public int getCount() {
112        return mActions.size();
113    }
114
115    public Action getItem(int position) {
116        return mActions.get(position);
117    }
118
119    public void setListener(Action.Listener listener) {
120        mListener = listener;
121        mActionOnKeyPressAnimator.setListener(listener);
122    }
123
124    public void setOnFocusListener(Action.OnFocusListener onFocusListener) {
125        mActionOnFocusAnimator.setOnFocusListener(onFocusListener);
126    }
127
128    /**
129     * Used for serialization only.
130     */
131    public ArrayList<Action> getActions() {
132        return new ArrayList<Action>(mActions);
133    }
134
135    public void setActions(ArrayList<Action> actions) {
136        mActionOnFocusAnimator.unFocus(null);
137        mActions.clear();
138        mActions.addAll(actions);
139        notifyDataSetChanged();
140    }
141
142    public void registerDataSetObserver(DataSetObserver dataSetObserver) {
143    }
144
145    public void setOnItemSelectedListener(OnItemSelectedListener listener) {
146    }
147
148    private static class ActionViewHolder extends ViewHolder {
149
150        private final ActionOnKeyPressAnimator mActionOnKeyPressAnimator;
151        private final ActionOnFocusAnimator mActionOnFocusAnimator;
152        private final View.OnClickListener mViewOnClickListener;
153        private Action mAction;
154
155        private BitmapCallback mPendingBitmapCallback;
156
157        public ActionViewHolder(View v, ActionOnKeyPressAnimator actionOnKeyPressAnimator,
158                ActionOnFocusAnimator actionOnFocusAnimator,
159                View.OnClickListener viewOnClickListener) {
160            super(v);
161            mActionOnKeyPressAnimator = actionOnKeyPressAnimator;
162            mActionOnFocusAnimator = actionOnFocusAnimator;
163            mViewOnClickListener = viewOnClickListener;
164        }
165
166        public Action getAction() {
167            return mAction;
168        }
169
170        public void init(Action action) {
171            mAction = action;
172
173            if (mPendingBitmapCallback != null) {
174                DrawableDownloader.getInstance(
175                        itemView.getContext()).cancelDownload(mPendingBitmapCallback);
176                mPendingBitmapCallback = null;
177            }
178            TextView title = (TextView) itemView.findViewById(R.id.action_title);
179            TextView description = (TextView) itemView.findViewById(R.id.action_description);
180            description.setText(action.getDescription());
181            description.setVisibility(
182                    TextUtils.isEmpty(action.getDescription()) ? View.GONE : View.VISIBLE);
183            title.setText(action.getTitle());
184            ImageView checkmarkView = (ImageView) itemView.findViewById(R.id.action_checkmark);
185            checkmarkView.setVisibility(action.isChecked() ? View.VISIBLE : View.INVISIBLE);
186
187            ImageView indicatorView = (ImageView) itemView.findViewById(R.id.action_icon);
188            View content = itemView.findViewById(R.id.action_content);
189            ViewGroup.LayoutParams contentLp = content.getLayoutParams();
190            if (setIndicator(indicatorView, action)) {
191                contentLp.width = itemView.getContext().getResources()
192                        .getDimensionPixelSize(R.dimen.lb_action_text_width);
193            } else {
194                contentLp.width = itemView.getContext().getResources()
195                        .getDimensionPixelSize(R.dimen.lb_action_text_width_no_icon);
196            }
197            content.setLayoutParams(contentLp);
198
199            ImageView chevronView = (ImageView) itemView.findViewById(R.id.action_next_chevron);
200            chevronView.setVisibility(action.hasNext() ? View.VISIBLE : View.INVISIBLE);
201
202            final Resources res = itemView.getContext().getResources();
203            if (action.hasMultilineDescription()) {
204                title.setMaxLines(res.getInteger(R.integer.lb_dialog_action_title_max_lines));
205                description.setMaxHeight(
206                        getDescriptionMaxHeight(itemView.getContext(), title));
207            } else {
208                title.setMaxLines(res.getInteger(R.integer.lb_dialog_action_title_min_lines));
209                description.setMaxLines(
210                        res.getInteger(R.integer.lb_dialog_action_description_min_lines));
211            }
212
213            itemView.setTag(R.id.action_title, this);
214            itemView.setOnKeyListener(mActionOnKeyPressAnimator);
215            itemView.setOnClickListener(mViewOnClickListener);
216            itemView.setOnFocusChangeListener(mActionOnFocusAnimator);
217            mActionOnFocusAnimator.unFocus(itemView);
218        }
219
220        private boolean setIndicator(final ImageView indicatorView, Action action) {
221
222            Context context = indicatorView.getContext();
223            Drawable indicator = action.getIndicator(context);
224            if (indicator != null) {
225                indicatorView.setImageDrawable(indicator);
226                indicatorView.setVisibility(View.VISIBLE);
227            } else {
228                Uri iconUri = action.getIconUri();
229                if (iconUri != null) {
230                    indicatorView.setVisibility(View.INVISIBLE);
231
232                    mPendingBitmapCallback = new BitmapCallback() {
233                        @Override
234                        public void onBitmapRetrieved(Drawable bitmap) {
235                            if (bitmap != null) {
236                                indicatorView.setVisibility(View.VISIBLE);
237                                indicatorView.setImageDrawable(bitmap);
238                                fadeIn(indicatorView);
239                            }
240                            mPendingBitmapCallback = null;
241                        }
242                    };
243
244                    DrawableDownloader.getInstance(context).getBitmap(
245                            new BitmapWorkerOptions.Builder(
246                                    context).resource(iconUri)
247                                    .width(indicatorView.getLayoutParams().width).build(),
248                            mPendingBitmapCallback);
249
250                } else {
251                    indicatorView.setVisibility(View.GONE);
252                    return false;
253                }
254            }
255            return true;
256        }
257
258        private void fadeIn(View v) {
259            v.setAlpha(0f);
260            ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(v,
261                    "alpha", 1f);
262            alphaAnimator.setDuration(
263                    v.getContext().getResources().getInteger(
264                            android.R.integer.config_mediumAnimTime));
265            alphaAnimator.start();
266        }
267
268        /**
269         * @return the max height in pixels the description can be such that the
270         *         action nicely takes up the entire screen.
271         */
272        private int getDescriptionMaxHeight(Context context, TextView title) {
273            final Resources res = context.getResources();
274            final float verticalPadding = res.getDimension(R.dimen.lb_dialog_list_item_vertical_padding);
275            final int titleMaxLines = res.getInteger(R.integer.lb_dialog_action_title_max_lines);
276            final int displayHeight = ((WindowManager) context.getSystemService(
277                    Context.WINDOW_SERVICE)).getDefaultDisplay().getHeight();
278
279            // The 2 multiplier on the title height calculation is a
280            // conservative estimate for font padding which can not be
281            // calculated at this stage since the view hasn't been rendered yet.
282            return (int) (displayHeight -
283                    2 * verticalPadding - 2 * titleMaxLines * title.getLineHeight());
284        }
285
286    }
287
288    private static class ActionOnFocusAnimator implements View.OnFocusChangeListener {
289
290        private boolean mResourcesSet;
291        private float mUnselectedAlpha;
292        private float mSelectedTitleAlpha;
293        private float mDisabledTitleAlpha;
294        private float mSelectedDescriptionAlpha;
295        private float mDisabledDescriptionAlpha;
296        private float mUnselectedDescriptionAlpha;
297        private float mSelectedChevronAlpha;
298        private float mDisabledChevronAlpha;
299        private int mAnimationDuration;
300        private Action.OnFocusListener mOnFocusListener;
301        private View mSelectedView;
302
303        ActionOnFocusAnimator(Action.OnFocusListener onFocusListener) {
304            mOnFocusListener = onFocusListener;
305        }
306
307        public void setOnFocusListener(Action.OnFocusListener onFocusListener) {
308            mOnFocusListener = onFocusListener;
309        }
310
311        public void unFocus(View v) {
312            changeFocus((v != null) ? v : mSelectedView, false, false);
313        }
314
315        @Override
316        public void onFocusChange(View v, boolean hasFocus) {
317            if (hasFocus) {
318                mSelectedView = v;
319                changeFocus(v, true /* hasFocus */, true /* shouldAnimate */);
320                if (mOnFocusListener != null) {
321                    // We still call onActionFocused so that listeners can clear
322                    // state if they want.
323                    mOnFocusListener.onActionFocused(
324                            ((ActionViewHolder) v.getTag(R.id.action_title)).getAction());
325                }
326            } else {
327                if (mSelectedView == v) {
328                    mSelectedView = null;
329                }
330                changeFocus(v, false /* hasFocus */, true /* shouldAnimate */);
331            }
332        }
333
334        private void changeFocus(View v, boolean hasFocus, boolean shouldAnimate) {
335            if (v == null) {
336                return;
337            }
338
339            if (!mResourcesSet) {
340                mResourcesSet = true;
341                final Resources res = v.getContext().getResources();
342
343                mAnimationDuration = res.getInteger(R.integer.lb_dialog_animation_duration);
344                mUnselectedAlpha =
345                        Float.valueOf(res.getString(R.string.lb_dialog_list_item_unselected_text_alpha));
346
347                mSelectedTitleAlpha =
348                        Float.valueOf(res.getString(R.string.lb_dialog_list_item_selected_title_text_alpha));
349                mDisabledTitleAlpha =
350                        Float.valueOf(res.getString(R.string.lb_dialog_list_item_disabled_title_text_alpha));
351
352                mSelectedDescriptionAlpha =
353                        Float.valueOf(
354                                res.getString(R.string.lb_dialog_list_item_selected_description_text_alpha));
355                mUnselectedDescriptionAlpha =
356                        Float.valueOf(
357                                res.getString(R.string.lb_dialog_list_item_unselected_description_text_alpha));
358                mDisabledDescriptionAlpha =
359                        Float.valueOf(
360                                res.getString(R.string.lb_dialog_list_item_disabled_description_text_alpha));
361
362                mSelectedChevronAlpha =
363                        Float.valueOf(
364                                res.getString(R.string.lb_dialog_list_item_selected_chevron_background_alpha));
365                mDisabledChevronAlpha =
366                        Float.valueOf(
367                                res.getString(R.string.lb_dialog_list_item_disabled_chevron_background_alpha));
368            }
369
370            Action action = ((ActionViewHolder) v.getTag(R.id.action_title)).getAction();
371
372            float titleAlpha = action.isEnabled() && !action.infoOnly()
373                    ? (hasFocus ? mSelectedTitleAlpha : mUnselectedAlpha) : mDisabledTitleAlpha;
374            float descriptionAlpha = (!hasFocus || action.infoOnly()) ? mUnselectedDescriptionAlpha
375                    : (action.isEnabled() ? mSelectedDescriptionAlpha : mDisabledDescriptionAlpha);
376            float chevronAlpha = action.hasNext() && !action.infoOnly()
377                    ? (action.isEnabled() ? mSelectedChevronAlpha : mDisabledChevronAlpha) : 0;
378
379            TextView title = (TextView) v.findViewById(R.id.action_title);
380            setAlpha(title, shouldAnimate, titleAlpha);
381
382            TextView description = (TextView) v.findViewById(R.id.action_description);
383            setAlpha(description, shouldAnimate, descriptionAlpha);
384
385            ImageView checkmark = (ImageView) v.findViewById(R.id.action_checkmark);
386            setAlpha(checkmark, shouldAnimate, titleAlpha);
387
388            ImageView icon = (ImageView) v.findViewById(R.id.action_icon);
389            setAlpha(icon, shouldAnimate, titleAlpha);
390
391            ImageView chevron = (ImageView) v.findViewById(R.id.action_next_chevron);
392            setAlpha(chevron, shouldAnimate, chevronAlpha);
393        }
394
395        private void setAlpha(View view, boolean shouldAnimate, float alpha) {
396            if (shouldAnimate) {
397                view.animate().alpha(alpha)
398                        .setDuration(mAnimationDuration)
399                        .setInterpolator(new DecelerateInterpolator(2F))
400                        .start();
401            } else {
402                view.setAlpha(alpha);
403            }
404        }
405    }
406
407    private static class ActionOnKeyPressAnimator implements View.OnKeyListener {
408
409        private static final int SELECT_ANIM_DURATION = 100;
410        private static final int SELECT_ANIM_DELAY = 0;
411        private static final float SELECT_ANIM_SELECTED_ALPHA = 0.2f;
412        private static final float SELECT_ANIM_UNSELECTED_ALPHA = 1.0f;
413        private static final float CHECKMARK_ANIM_UNSELECTED_ALPHA = 0.0f;
414        private static final float CHECKMARK_ANIM_SELECTED_ALPHA = 1.0f;
415
416        private final List<Action> mActions;
417        private boolean mKeyPressed = false;
418        private Action.Listener mListener;
419
420        public ActionOnKeyPressAnimator(Action.Listener listener,
421                List<Action> actions) {
422            mListener = listener;
423            mActions = actions;
424        }
425
426        public void setListener(Action.Listener listener) {
427            mListener = listener;
428        }
429
430        private void playSound(Context context, int soundEffect) {
431            AudioManager manager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
432            manager.playSoundEffect(soundEffect);
433        }
434
435        /**
436         * Now only handles KEYCODE_ENTER and KEYCODE_NUMPAD_ENTER key event.
437         */
438        @Override
439        public boolean onKey(View v, int keyCode, KeyEvent event) {
440            if (v == null) {
441                return false;
442            }
443            boolean handled = false;
444            Action action = ((ActionViewHolder) v.getTag(R.id.action_title)).getAction();
445            switch (keyCode) {
446                case KeyEvent.KEYCODE_DPAD_CENTER:
447                case KeyEvent.KEYCODE_NUMPAD_ENTER:
448                case KeyEvent.KEYCODE_BUTTON_X:
449                case KeyEvent.KEYCODE_BUTTON_Y:
450                case KeyEvent.KEYCODE_ENTER:
451
452                    if (!action.isEnabled() || action.infoOnly()) {
453                        if (v.isSoundEffectsEnabled()
454                                && event.getAction() == KeyEvent.ACTION_DOWN) {
455                            // TODO: requires API 19
456                            //playSound(v.getContext(), AudioManager.FX_KEYPRESS_INVALID);
457                        }
458                        return true;
459                    }
460
461                    switch (event.getAction()) {
462                        case KeyEvent.ACTION_DOWN:
463                            if (!mKeyPressed) {
464                                mKeyPressed = true;
465
466                                if (v.isSoundEffectsEnabled()) {
467                                    playSound(v.getContext(), AudioManager.FX_KEY_CLICK);
468                                }
469
470                                if (DEBUG) {
471                                    Log.d(TAG, "Enter Key down");
472                                }
473
474                                prepareAndAnimateView(v, SELECT_ANIM_UNSELECTED_ALPHA,
475                                        SELECT_ANIM_SELECTED_ALPHA, SELECT_ANIM_DURATION,
476                                        SELECT_ANIM_DELAY, null, mKeyPressed);
477                                handled = true;
478                            }
479                            break;
480                        case KeyEvent.ACTION_UP:
481                            if (mKeyPressed) {
482                                mKeyPressed = false;
483
484                                if (DEBUG) {
485                                    Log.d(TAG, "Enter Key up");
486                                }
487
488                                prepareAndAnimateView(v, SELECT_ANIM_SELECTED_ALPHA,
489                                        SELECT_ANIM_UNSELECTED_ALPHA, SELECT_ANIM_DURATION,
490                                        SELECT_ANIM_DELAY, null, mKeyPressed);
491                                handled = true;
492                            }
493                            break;
494                        default:
495                            break;
496                    }
497                    break;
498                default:
499                    break;
500            }
501            return handled;
502        }
503
504        private void prepareAndAnimateView(final View v, float initAlpha, float destAlpha,
505                int duration,
506                int delay, Interpolator interpolator, final boolean pressed) {
507            if (v != null && v.getWindowToken() != null) {
508                final Action action = ((ActionViewHolder) v.getTag(R.id.action_title)).getAction();
509
510                if (!pressed) {
511                    fadeCheckmarks(v, action, duration, delay, interpolator);
512                }
513
514                v.setAlpha(initAlpha);
515                v.setLayerType(View.LAYER_TYPE_HARDWARE, null);
516                v.buildLayer();
517                v.animate().alpha(destAlpha).setDuration(duration).setStartDelay(delay);
518                if (interpolator != null) {
519                    v.animate().setInterpolator(interpolator);
520                }
521                v.animate().setListener(new AnimatorListenerAdapter() {
522                        @Override
523                    public void onAnimationEnd(Animator animation) {
524
525                        v.setLayerType(View.LAYER_TYPE_NONE, null);
526                        if (!pressed) {
527                            if (mListener != null) {
528                                mListener.onActionClicked(action);
529                            }
530                        }
531                    }
532                });
533                v.animate().start();
534            }
535        }
536
537        private void fadeCheckmarks(final View v, final Action action, int duration, int delay,
538                Interpolator interpolator) {
539            int actionCheckSetId = action.getCheckSetId();
540            if (actionCheckSetId != Action.NO_CHECK_SET) {
541                ViewGroup parent = (ViewGroup) v.getTag(R.layout.lb_dialog_action_list_item);
542                // Find any actions that are checked and are in the same group
543                // as the selected action. Fade their checkmarks out.
544                for (int i = 0, size = mActions.size(); i < size; i++) {
545                    Action a = mActions.get(i);
546                    if (a != action && a.getCheckSetId() == actionCheckSetId && a.isChecked()) {
547                        a.setChecked(false);
548                        View viewToAnimateOut = parent.getChildAt(i);
549                        if (viewToAnimateOut != null) {
550                            final View checkView = viewToAnimateOut.findViewById(
551                                    R.id.action_checkmark);
552                            checkView.animate().alpha(CHECKMARK_ANIM_UNSELECTED_ALPHA)
553                                    .setDuration(duration).setStartDelay(delay);
554                            if (interpolator != null) {
555                                checkView.animate().setInterpolator(interpolator);
556                            }
557                            checkView.animate().setListener(new AnimatorListenerAdapter() {
558                                    @Override
559                                public void onAnimationEnd(Animator animation) {
560                                    checkView.setVisibility(View.INVISIBLE);
561                                }
562                            });
563                        }
564                    }
565                }
566
567                // If we we'ren't already checked, fade our checkmark in.
568                if (!action.isChecked()) {
569                    action.setChecked(true);
570                    final View checkView = v.findViewById(R.id.action_checkmark);
571                    checkView.setVisibility(View.VISIBLE);
572                    checkView.setAlpha(CHECKMARK_ANIM_UNSELECTED_ALPHA);
573                    checkView.animate().alpha(CHECKMARK_ANIM_SELECTED_ALPHA).setDuration(duration)
574                            .setStartDelay(delay);
575                    if (interpolator != null) {
576                        checkView.animate().setInterpolator(interpolator);
577                    }
578                    checkView.animate().setListener(null);
579                }
580            }
581        }
582    }
583}
584