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 */ 83 public void onActionClicked(Action action); 84 } 85 86 public interface OnFocusListener { 87 88 /** 89 * Called when the user focuses on an action. 90 */ 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 */ 102 public void onActionSelect(Action action); 103 104 /** 105 * Called when user finish unselecting an action. 106 */ 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 final 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 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<>(); 148 mKeyPressed = false; 149 } 150 151 @Override 152 public void viewRemoved(View view) { 153 // Do nothing. 154 } 155 156 @Override 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 164 public int getCount() { 165 return mActions.size(); 166 } 167 168 @Override 169 public Object getItem(int position) { 170 return mActions.get(position); 171 } 172 173 @Override 174 public long getItemId(int position) { 175 return position; 176 } 177 178 @Override 179 public boolean hasStableIds() { 180 return true; 181 } 182 183 @Override 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 227 public ScrollAdapterBase getExpandAdapter() { 228 return null; 229 } 230 231 public void setListener(Listener listener) { 232 mListener = listener; 233 } 234 235 public void setOnFocusListener(OnFocusListener onFocusListener) { 236 mOnFocusListener = onFocusListener; 237 } 238 239 public void setOnKeyListener(OnKeyListener onKeyListener) { 240 mOnKeyListener = onKeyListener; 241 } 242 243 public void addAction(Action action) { 244 mActions.add(action); 245 notifyDataSetChanged(); 246 } 247 248 /** 249 * Used for serialization only. 250 */ 251 public ArrayList<Action> getActions() { 252 return new ArrayList<>(mActions); 253 } 254 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 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 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 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 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 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 362 void setScrollAdapterView(ScrollAdapterView scrollAdapterView) { 363 mScrollAdapterView = scrollAdapterView; 364 } 365 366 @Override 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 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 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 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 */ 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 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