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 
17 package com.android.tv.settings.dialog.old;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.ObjectAnimator;
22 import android.content.Context;
23 import android.content.res.Resources;
24 import android.graphics.Bitmap;
25 import android.graphics.drawable.Drawable;
26 import android.media.AudioManager;
27 import android.net.Uri;
28 import android.text.TextUtils;
29 import android.util.Log;
30 import android.util.TypedValue;
31 import android.view.KeyEvent;
32 import android.view.LayoutInflater;
33 import android.view.View;
34 import android.view.ViewGroup;
35 import android.view.WindowManager;
36 import android.view.animation.DecelerateInterpolator;
37 import android.view.animation.Interpolator;
38 import android.widget.BaseAdapter;
39 import android.widget.ImageView;
40 import android.widget.TextView;
41 
42 import com.android.tv.settings.R;
43 import com.android.tv.settings.widget.BitmapDownloader;
44 import com.android.tv.settings.widget.BitmapDownloader.BitmapCallback;
45 import com.android.tv.settings.widget.BitmapWorkerOptions;
46 import com.android.tv.settings.widget.ScrollAdapter;
47 import com.android.tv.settings.widget.ScrollAdapterBase;
48 import com.android.tv.settings.widget.ScrollAdapterView;
49 import com.android.tv.settings.widget.ScrollAdapterView.OnScrollListener;
50 
51 import java.util.ArrayList;
52 import java.util.List;
53 
54 /**
55  * Adapter class which creates actions.
56  */
57 public class ActionAdapter extends BaseAdapter implements ScrollAdapter,
58         OnScrollListener, View.OnKeyListener, View.OnClickListener {
59     private static final String TAG = "ActionAdapter";
60 
61     private static final boolean DEBUG = false;
62 
63     private static final int SELECT_ANIM_DURATION = 100;
64     private static final int SELECT_ANIM_DELAY = 0;
65     private static final float SELECT_ANIM_SELECTED_ALPHA = 0.2f;
66     private static final float SELECT_ANIM_UNSELECTED_ALPHA = 1.0f;
67     private static final float CHECKMARK_ANIM_UNSELECTED_ALPHA = 0.0f;
68     private static final float CHECKMARK_ANIM_SELECTED_ALPHA = 1.0f;
69 
70     private static Integer sDescriptionMaxHeight = null;
71 
72     // TODO: this constant is only in KLP: update when KLP has a more standard SDK.
73     private static final int FX_KEYPRESS_INVALID = 9; // AudioManager.FX_KEYPRESS_INVALID;
74 
75     /**
76      * Object listening for adapter events.
77      */
78     public interface Listener {
79 
80         /**
81          * Called when the user clicks on an action.
82          */
onActionClicked(Action action)83         public void onActionClicked(Action action);
84     }
85 
86     public interface OnFocusListener {
87 
88         /**
89          * Called when the user focuses on an action.
90          */
onActionFocused(Action action)91         public void onActionFocused(Action action);
92     }
93 
94     /**
95      * Object listening for adapter action select/unselect events.
96      */
97     public interface OnKeyListener {
98 
99         /**
100          * Called when user finish selecting an action.
101          */
onActionSelect(Action action)102         public void onActionSelect(Action action);
103 
104         /**
105          * Called when user finish unselecting an action.
106          */
onActionUnselect(Action action)107         public void onActionUnselect(Action action);
108     }
109 
110 
111     private final Context mContext;
112     private final float mUnselectedAlpha;
113     private final float mSelectedTitleAlpha;
114     private final float mDisabledTitleAlpha;
115     private final float mSelectedDescriptionAlpha;
116     private final float mDisabledDescriptionAlpha;
117     private final float mUnselectedDescriptionAlpha;
118     private final float mSelectedChevronAlpha;
119     private final float mDisabledChevronAlpha;
120     private List<Action> mActions;
121     private Listener mListener;
122     private OnFocusListener mOnFocusListener;
123     private OnKeyListener mOnKeyListener;
124     private boolean mKeyPressed;
125     private ScrollAdapterView mScrollAdapterView;
126     private final int mAnimationDuration;
127     private View mSelectedView = null;
128 
ActionAdapter(Context context)129     public ActionAdapter(Context context) {
130         super();
131         mContext = context;
132         final Resources res = context.getResources();
133 
134         mAnimationDuration = res.getInteger(R.integer.dialog_animation_duration);
135         mUnselectedAlpha = getFloat(R.dimen.list_item_unselected_text_alpha);
136 
137         mSelectedTitleAlpha = getFloat(R.dimen.list_item_selected_title_text_alpha);
138         mDisabledTitleAlpha = getFloat(R.dimen.list_item_disabled_title_text_alpha);
139 
140         mSelectedDescriptionAlpha = getFloat(R.dimen.list_item_selected_description_text_alpha);
141         mUnselectedDescriptionAlpha = getFloat(R.dimen.list_item_unselected_description_text_alpha);
142         mDisabledDescriptionAlpha = getFloat(R.dimen.list_item_disabled_description_text_alpha);
143 
144         mSelectedChevronAlpha = getFloat(R.dimen.list_item_selected_chevron_background_alpha);
145         mDisabledChevronAlpha = getFloat(R.dimen.list_item_disabled_chevron_background_alpha);
146 
147         mActions = new ArrayList<Action>();
148         mKeyPressed = false;
149     }
150 
151     @Override
viewRemoved(View view)152     public void viewRemoved(View view) {
153         // Do nothing.
154     }
155 
156     @Override
getScrapView(ViewGroup parent)157     public View getScrapView(ViewGroup parent) {
158         LayoutInflater inflater = LayoutInflater.from(mContext);
159         View view = inflater.inflate(R.layout.settings_list_item, parent, false);
160         return view;
161     }
162 
163     @Override
getCount()164     public int getCount() {
165         return mActions.size();
166     }
167 
168     @Override
getItem(int position)169     public Object getItem(int position) {
170         return mActions.get(position);
171     }
172 
173     @Override
getItemId(int position)174     public long getItemId(int position) {
175         return position;
176     }
177 
178     @Override
hasStableIds()179     public boolean hasStableIds() {
180         return true;
181     }
182 
183     @Override
getView(int position, View convertView, ViewGroup parent)184     public View getView(int position, View convertView, ViewGroup parent) {
185         if (convertView == null) {
186             convertView = getScrapView(parent);
187         }
188         Action action = mActions.get(position);
189         TextView title = (TextView) convertView.findViewById(R.id.action_title);
190         TextView description = (TextView) convertView.findViewById(R.id.action_description);
191         description.setText(action.getDescription());
192         description.setVisibility(
193                 TextUtils.isEmpty(action.getDescription()) ? View.GONE : View.VISIBLE);
194         title.setText(action.getTitle());
195         ImageView checkmarkView = (ImageView) convertView.findViewById(R.id.action_checkmark);
196         checkmarkView.setVisibility(action.isChecked() ? View.VISIBLE : View.INVISIBLE);
197 
198         ImageView indicatorView = (ImageView) convertView.findViewById(R.id.action_icon);
199         setIndicator(indicatorView, action);
200 
201         ImageView chevronView = (ImageView) convertView.findViewById(R.id.action_next_chevron);
202         chevronView.setVisibility(action.hasNext() ? View.VISIBLE : View.GONE);
203 
204         View chevronBackgroundView = convertView.findViewById(R.id.action_next_chevron_background);
205         chevronBackgroundView.setVisibility(action.hasNext() ? View.VISIBLE : View.INVISIBLE);
206 
207         final Resources res = convertView.getContext().getResources();
208         if (action.hasMultilineDescription()) {
209             title.setMaxLines(res.getInteger(R.integer.action_title_max_lines));
210             description.setMaxHeight(
211                     getDescriptionMaxHeight(convertView.getContext(), title, description));
212         } else {
213             title.setMaxLines(res.getInteger(R.integer.action_title_min_lines));
214             description.setMaxLines(
215                     res.getInteger(R.integer.action_description_min_lines));
216         }
217 
218         convertView.setTag(R.id.action_title, action);
219         convertView.setOnKeyListener(this);
220         convertView.setOnClickListener(this);
221         changeFocus(convertView, false /* hasFocus */, false /* shouldAnimate */);
222 
223         return convertView;
224     }
225 
226     @Override
getExpandAdapter()227     public ScrollAdapterBase getExpandAdapter() {
228         return null;
229     }
230 
setListener(Listener listener)231     public void setListener(Listener listener) {
232         mListener = listener;
233     }
234 
setOnFocusListener(OnFocusListener onFocusListener)235     public void setOnFocusListener(OnFocusListener onFocusListener) {
236         mOnFocusListener = onFocusListener;
237     }
238 
setOnKeyListener(OnKeyListener onKeyListener)239     public void setOnKeyListener(OnKeyListener onKeyListener) {
240         mOnKeyListener = onKeyListener;
241     }
242 
addAction(Action action)243     public void addAction(Action action) {
244         mActions.add(action);
245         notifyDataSetChanged();
246     }
247 
248     /**
249      * Used for serialization only.
250      */
getActions()251     public ArrayList<Action> getActions() {
252         return new ArrayList<Action>(mActions);
253     }
254 
setActions(ArrayList<Action> actions)255     public void setActions(ArrayList<Action> actions) {
256         changeFocus(mSelectedView, false /* hasFocus */, false /* shouldAnimate */);
257         mActions.clear();
258         mActions.addAll(actions);
259         notifyDataSetChanged();
260     }
261 
262     // We want to highlight a view if we've stopped scrolling on it (mainPosition = 0).
263     // If mainPosition is not 0, we don't want to do anything with view, but we do want to ensure
264     // we dim the last highlighted view so that while a user is scrolling, nothing is highlighted.
265     @Override
onScrolled(View view, int position, float mainPosition, float secondPosition)266     public void onScrolled(View view, int position, float mainPosition, float secondPosition) {
267         boolean hasFocus = (mainPosition == 0.0);
268         if (hasFocus) {
269             if (view != null) {
270                 changeFocus(view, true /* hasFocus */, true /* shouldAniamte */);
271                 mSelectedView = view;
272             }
273         } else if (mSelectedView != null) {
274             changeFocus(mSelectedView, false /* hasFocus */, true /* shouldAniamte */);
275             mSelectedView = null;
276         }
277     }
278 
changeFocus(View v, boolean hasFocus, boolean shouldAnimate)279     private void changeFocus(View v, boolean hasFocus, boolean shouldAnimate) {
280         if (v == null) {
281             return;
282         }
283         Action action = (Action) v.getTag(R.id.action_title);
284 
285         float titleAlpha = action.isEnabled() && !action.infoOnly()
286                 ? (hasFocus ? mSelectedTitleAlpha : mUnselectedAlpha) : mDisabledTitleAlpha;
287         float descriptionAlpha = (!hasFocus || action.infoOnly()) ? mUnselectedDescriptionAlpha
288                 : (action.isEnabled() ? mSelectedDescriptionAlpha : mDisabledDescriptionAlpha);
289         float chevronAlpha = action.hasNext() && !action.infoOnly()
290                 ? (action.isEnabled() ? mSelectedChevronAlpha : mDisabledChevronAlpha) : 0;
291 
292         TextView title = (TextView) v.findViewById(R.id.action_title);
293         setAlpha(title, shouldAnimate, titleAlpha);
294 
295         TextView description = (TextView) v.findViewById(R.id.action_description);
296         setAlpha(description, shouldAnimate, descriptionAlpha);
297 
298         ImageView checkmark = (ImageView) v.findViewById(R.id.action_checkmark);
299         setAlpha(checkmark, shouldAnimate, titleAlpha);
300 
301         ImageView icon = (ImageView) v.findViewById(R.id.action_icon);
302         setAlpha(icon, shouldAnimate, titleAlpha);
303 
304         View chevronBackground = v.findViewById(R.id.action_next_chevron_background);
305         setAlpha(chevronBackground, shouldAnimate, chevronAlpha);
306 
307         if (mOnFocusListener != null && hasFocus) {
308             // We still call onActionFocused so that listeners can clear state if they want.
309             mOnFocusListener.onActionFocused((Action) v.getTag(R.id.action_title));
310         }
311     }
312 
setIndicator(final ImageView indicatorView, Action action)313     private void setIndicator(final ImageView indicatorView, Action action) {
314 
315         Drawable indicator = action.getIndicator(mContext);
316         if (indicator != null) {
317             indicatorView.setImageDrawable(indicator);
318             indicatorView.setVisibility(View.VISIBLE);
319         } else {
320             Uri iconUri = action.getIconUri();
321             if (iconUri != null) {
322                 indicatorView.setVisibility(View.INVISIBLE);
323 
324                 BitmapDownloader.getInstance(mContext).getBitmap(new BitmapWorkerOptions.Builder(
325                         mContext).resource(iconUri)
326                         .width(indicatorView.getLayoutParams().width).build(),
327                         new BitmapCallback() {
328                             @Override
329                             public void onBitmapRetrieved(Bitmap bitmap) {
330                                 if (bitmap != null) {
331                                     indicatorView.setVisibility(View.VISIBLE);
332                                     indicatorView.setImageBitmap(bitmap);
333                                     fadeIn(indicatorView);
334                                 }
335                             }
336                         });
337             } else {
338                 indicatorView.setVisibility(View.GONE);
339             }
340         }
341     }
342 
fadeIn(View v)343     private void fadeIn(View v) {
344         v.setAlpha(0f);
345         ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(v, "alpha", 1f);
346         alphaAnimator.setDuration(mContext.getResources().getInteger(
347                 android.R.integer.config_mediumAnimTime));
348         alphaAnimator.start();
349     }
350 
setAlpha(View view, boolean shouldAnimate, float alpha)351     private void setAlpha(View view, boolean shouldAnimate, float alpha) {
352         if (shouldAnimate) {
353             view.animate().alpha(alpha)
354                     .setDuration(mAnimationDuration)
355                     .setInterpolator(new DecelerateInterpolator(2F))
356                     .start();
357         } else {
358             view.setAlpha(alpha);
359         }
360     }
361 
setScrollAdapterView(ScrollAdapterView scrollAdapterView)362     void setScrollAdapterView(ScrollAdapterView scrollAdapterView) {
363         mScrollAdapterView = scrollAdapterView;
364     }
365 
366     @Override
onClick(View v)367     public void onClick(View v) {
368         if (v != null && v.getWindowToken() != null && mListener != null) {
369             final Action action = (Action) v.getTag(R.id.action_title);
370             mListener.onActionClicked(action);
371         }
372     }
373 
374     /**
375      * Now only handles KEYCODE_ENTER and KEYCODE_NUMPAD_ENTER key event.
376      */
377     @Override
onKey(View v, int keyCode, KeyEvent event)378     public boolean onKey(View v, int keyCode, KeyEvent event) {
379         if (v == null) {
380             return false;
381         }
382         boolean handled = false;
383         Action action = (Action) v.getTag(R.id.action_title);
384         switch (keyCode) {
385             case KeyEvent.KEYCODE_DPAD_CENTER:
386             case KeyEvent.KEYCODE_NUMPAD_ENTER:
387             case KeyEvent.KEYCODE_BUTTON_X:
388             case KeyEvent.KEYCODE_BUTTON_Y:
389             case KeyEvent.KEYCODE_ENTER:
390                 AudioManager manager = (AudioManager) v.getContext().getSystemService(
391                         Context.AUDIO_SERVICE);
392                 if (!action.isEnabled() || action.infoOnly()) {
393                     if (v.isSoundEffectsEnabled() && event.getAction() == KeyEvent.ACTION_DOWN) {
394                         manager.playSoundEffect(FX_KEYPRESS_INVALID);
395                     }
396                     return true;
397                 }
398 
399                 switch (event.getAction()) {
400                     case KeyEvent.ACTION_DOWN:
401                         if (!mKeyPressed) {
402                             mKeyPressed = true;
403 
404                             if (v.isSoundEffectsEnabled()) {
405                                 manager.playSoundEffect(AudioManager.FX_KEY_CLICK);
406                             }
407 
408                             if (DEBUG) {
409                                 Log.d(TAG, "Enter Key down");
410                             }
411 
412                             prepareAndAnimateView(v, SELECT_ANIM_UNSELECTED_ALPHA,
413                                     SELECT_ANIM_SELECTED_ALPHA, SELECT_ANIM_DURATION,
414                                     SELECT_ANIM_DELAY, null, mKeyPressed);
415                             handled = true;
416                         }
417                         break;
418                     case KeyEvent.ACTION_UP:
419                         if (mKeyPressed) {
420                             mKeyPressed = false;
421 
422                             if (DEBUG) {
423                                 Log.d(TAG, "Enter Key up");
424                             }
425 
426                             prepareAndAnimateView(v, SELECT_ANIM_SELECTED_ALPHA,
427                                     SELECT_ANIM_UNSELECTED_ALPHA, SELECT_ANIM_DURATION,
428                                     SELECT_ANIM_DELAY, null, mKeyPressed);
429                             handled = true;
430                         }
431                         break;
432                     default:
433                         break;
434                 }
435                 break;
436             default:
437                 break;
438         }
439         return handled;
440     }
441 
prepareAndAnimateView(final View v, float initAlpha, float destAlpha, int duration, int delay, Interpolator interpolator, final boolean pressed)442     private void prepareAndAnimateView(final View v, float initAlpha, float destAlpha, int duration,
443             int delay, Interpolator interpolator, final boolean pressed) {
444         if (v != null && v.getWindowToken() != null) {
445             final Action action = (Action) v.getTag(R.id.action_title);
446 
447             if (!pressed) {
448                 fadeCheckmarks(v, action, duration, delay, interpolator);
449             }
450 
451             v.setAlpha(initAlpha);
452             v.setLayerType(View.LAYER_TYPE_HARDWARE, null);
453             v.buildLayer();
454             v.animate().alpha(destAlpha).setDuration(duration).setStartDelay(delay);
455             if (interpolator != null) {
456                 v.animate().setInterpolator(interpolator);
457             }
458             v.animate().setListener(new AnimatorListenerAdapter() {
459                 @Override
460                 public void onAnimationEnd(Animator animation) {
461 
462                     v.setLayerType(View.LAYER_TYPE_NONE, null);
463                     if (pressed) {
464                         if (mOnKeyListener != null) {
465                             mOnKeyListener.onActionSelect(action);
466                         }
467                     } else {
468                         if (mOnKeyListener != null) {
469                             mOnKeyListener.onActionUnselect(action);
470                         }
471                         if (mListener != null) {
472                             mListener.onActionClicked(action);
473                         }
474                     }
475                 }
476             });
477             v.animate().start();
478         }
479     }
480 
fadeCheckmarks(final View v, final Action action, int duration, int delay, Interpolator interpolator)481     private void fadeCheckmarks(final View v, final Action action, int duration, int delay,
482             Interpolator interpolator) {
483         int actionCheckSetId = action.getCheckSetId();
484         if (actionCheckSetId != Action.NO_CHECK_SET) {
485             // Find any actions that are checked and are in the same group
486             // as the selected action. Fade their checkmarks out.
487             for (int i = 0, size = mActions.size(); i < size; i++) {
488                 Action a = mActions.get(i);
489                 if (a != action && a.getCheckSetId() == actionCheckSetId && a.isChecked()) {
490                     a.setChecked(false);
491                     if (mScrollAdapterView != null) {
492                         View viewToAnimateOut = mScrollAdapterView.getItemView(i);
493                         if (viewToAnimateOut != null) {
494                             final View checkView = viewToAnimateOut.findViewById(
495                                     R.id.action_checkmark);
496                             checkView.animate().alpha(CHECKMARK_ANIM_UNSELECTED_ALPHA)
497                                     .setDuration(duration).setStartDelay(delay);
498                             if (interpolator != null) {
499                                 checkView.animate().setInterpolator(interpolator);
500                             }
501                             checkView.animate().setListener(new AnimatorListenerAdapter() {
502                                 @Override
503                                 public void onAnimationEnd(Animator animation) {
504                                     checkView.setVisibility(View.INVISIBLE);
505                                 }
506                             });
507                         }
508                     }
509                 }
510             }
511 
512             // If we we'ren't already checked, fade our checkmark in.
513             if (!action.isChecked()) {
514                 action.setChecked(true);
515                 if (mScrollAdapterView != null) {
516                     final View checkView = v.findViewById(R.id.action_checkmark);
517                     checkView.setVisibility(View.VISIBLE);
518                     checkView.setAlpha(CHECKMARK_ANIM_UNSELECTED_ALPHA);
519                     checkView.animate().alpha(CHECKMARK_ANIM_SELECTED_ALPHA).setDuration(duration)
520                             .setStartDelay(delay);
521                     if (interpolator != null) {
522                         checkView.animate().setInterpolator(interpolator);
523                     }
524                     checkView.animate().setListener(null);
525                 }
526             }
527         }
528     }
529 
530     /**
531      * @return the max height in pixels the description can be such that the
532      * action nicely takes up the entire screen.
533      */
getDescriptionMaxHeight(Context context, TextView title, TextView description)534     private static Integer getDescriptionMaxHeight(Context context, TextView title,
535             TextView description) {
536         if (sDescriptionMaxHeight == null) {
537             final Resources res = context.getResources();
538             final float verticalPadding = res.getDimension(R.dimen.list_item_vertical_padding);
539             final int titleMaxLines = res.getInteger(R.integer.action_title_max_lines);
540             final int displayHeight = ((WindowManager) context.getSystemService(
541                     Context.WINDOW_SERVICE)).getDefaultDisplay().getHeight();
542 
543             // The 2 multiplier on the title height calculation is a conservative
544             // estimate for font padding which can not be calculated at this stage
545             // since the view hasn't been rendered yet.
546             sDescriptionMaxHeight = (int) (displayHeight -
547                     2 * verticalPadding - 2 * titleMaxLines * title.getLineHeight());
548         }
549         return sDescriptionMaxHeight;
550     }
551 
getFloat(int resourceId)552     private float getFloat(int resourceId) {
553         TypedValue buffer = new TypedValue();
554         mContext.getResources().getValue(resourceId, buffer, true);
555         return buffer.getFloat();
556     }
557 }
558