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