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