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 */ 77 public void onActionClicked(Action action); 78 } 79 80 public interface OnFocusListener { 81 82 /** 83 * Called when the user focuses on an action. 84 */ 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 */ 96 public void onActionSelect(Action action); 97 98 /** 99 * Called when user finish unselecting an action. 100 */ 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 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 146 public void viewRemoved(View view) { 147 // Do nothing. 148 } 149 150 @Override 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 158 public int getCount() { 159 return mActions.size(); 160 } 161 162 @Override 163 public Object getItem(int position) { 164 return mActions.get(position); 165 } 166 167 @Override 168 public long getItemId(int position) { 169 return position; 170 } 171 172 @Override 173 public boolean hasStableIds() { 174 return true; 175 } 176 177 @Override 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 221 public ScrollAdapterBase getExpandAdapter() { 222 return null; 223 } 224 225 public void setListener(Listener listener) { 226 mListener = listener; 227 } 228 229 public void setOnFocusListener(OnFocusListener onFocusListener) { 230 mOnFocusListener = onFocusListener; 231 } 232 233 public void setOnKeyListener(OnKeyListener onKeyListener) { 234 mOnKeyListener = onKeyListener; 235 } 236 237 public void addAction(Action action) { 238 mActions.add(action); 239 notifyDataSetChanged(); 240 } 241 242 /** 243 * Used for serialization only. 244 */ 245 public ArrayList<Action> getActions() { 246 return new ArrayList<>(mActions); 247 } 248 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 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 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 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 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 329 void setScrollAdapterView(ScrollAdapterView scrollAdapterView) { 330 mScrollAdapterView = scrollAdapterView; 331 } 332 333 @Override 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 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 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 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 */ 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 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