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 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 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<>(); 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<>(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