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; 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.database.DataSetObserver; 25 import android.graphics.drawable.Drawable; 26 import android.media.AudioManager; 27 import android.net.Uri; 28 import android.support.v7.widget.RecyclerView; 29 import android.support.v7.widget.RecyclerView.ViewHolder; 30 import android.text.TextUtils; 31 import android.util.Log; 32 import android.view.KeyEvent; 33 import android.view.LayoutInflater; 34 import android.view.View; 35 import android.view.ViewGroup; 36 import android.view.WindowManager; 37 import android.view.animation.DecelerateInterpolator; 38 import android.view.animation.Interpolator; 39 import android.widget.AdapterView.OnItemSelectedListener; 40 import android.widget.ImageView; 41 import android.widget.TextView; 42 43 import com.android.tv.settings.R; 44 import com.android.tv.settings.dialog.DialogFragment.Action; 45 import com.android.tv.settings.widget.BitmapWorkerOptions; 46 import com.android.tv.settings.widget.DrawableDownloader; 47 import com.android.tv.settings.widget.DrawableDownloader.BitmapCallback; 48 49 import java.util.ArrayList; 50 import java.util.List; 51 52 /** 53 * Adapter class which creates actions. 54 * 55 * @hide 56 */ 57 class DialogActionAdapter extends RecyclerView.Adapter { 58 private static final String TAG = "ActionAdapter"; 59 private static final boolean DEBUG = false; 60 61 private final ActionOnKeyPressAnimator mActionOnKeyPressAnimator; 62 private final ActionOnFocusAnimator mActionOnFocusAnimator; 63 private LayoutInflater mInflater; 64 private final List<Action> mActions; 65 private Action.Listener mListener; 66 private final View.OnClickListener mOnClickListener = new View.OnClickListener() { 67 @Override 68 public void onClick(View v) { 69 if (v != null && v.getWindowToken() != null && mListener != null) { 70 mListener.onActionClicked(((ActionViewHolder) v.getTag(R.id.action_title)).getAction()); 71 } 72 } 73 }; 74 DialogActionAdapter(Action.Listener listener, Action.OnFocusListener onFocusListener, List<Action> actions)75 public DialogActionAdapter(Action.Listener listener, Action.OnFocusListener onFocusListener, 76 List<Action> actions) { 77 super(); 78 mListener = listener; 79 mActions = new ArrayList<Action>(actions); 80 mActionOnKeyPressAnimator = new ActionOnKeyPressAnimator(listener, mActions); 81 mActionOnFocusAnimator = new ActionOnFocusAnimator(onFocusListener); 82 } 83 84 @Override onCreateViewHolder(ViewGroup parent, int viewType)85 public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 86 if (mInflater == null) { 87 mInflater = (LayoutInflater) parent.getContext().getSystemService( 88 Context.LAYOUT_INFLATER_SERVICE); 89 } 90 View v = mInflater.inflate(R.layout.lb_dialog_action_list_item, parent, false); 91 v.setTag(R.layout.lb_dialog_action_list_item, parent); 92 return new ActionViewHolder(v, mActionOnKeyPressAnimator, mActionOnFocusAnimator, mOnClickListener); 93 } 94 95 @Override onBindViewHolder(RecyclerView.ViewHolder baseHolder, int position)96 public void onBindViewHolder(RecyclerView.ViewHolder baseHolder, int position) { 97 ActionViewHolder holder = (ActionViewHolder) baseHolder; 98 99 if (position >= mActions.size()) { 100 return; 101 } 102 103 holder.init(mActions.get(position)); 104 } 105 106 @Override getItemCount()107 public int getItemCount() { 108 return mActions.size(); 109 } 110 getCount()111 public int getCount() { 112 return mActions.size(); 113 } 114 getItem(int position)115 public Action getItem(int position) { 116 return mActions.get(position); 117 } 118 setListener(Action.Listener listener)119 public void setListener(Action.Listener listener) { 120 mListener = listener; 121 mActionOnKeyPressAnimator.setListener(listener); 122 } 123 setOnFocusListener(Action.OnFocusListener onFocusListener)124 public void setOnFocusListener(Action.OnFocusListener onFocusListener) { 125 mActionOnFocusAnimator.setOnFocusListener(onFocusListener); 126 } 127 128 /** 129 * Used for serialization only. 130 */ getActions()131 public ArrayList<Action> getActions() { 132 return new ArrayList<Action>(mActions); 133 } 134 setActions(ArrayList<Action> actions)135 public void setActions(ArrayList<Action> actions) { 136 mActionOnFocusAnimator.unFocus(null); 137 mActions.clear(); 138 mActions.addAll(actions); 139 notifyDataSetChanged(); 140 } 141 registerDataSetObserver(DataSetObserver dataSetObserver)142 public void registerDataSetObserver(DataSetObserver dataSetObserver) { 143 } 144 setOnItemSelectedListener(OnItemSelectedListener listener)145 public void setOnItemSelectedListener(OnItemSelectedListener listener) { 146 } 147 148 private static class ActionViewHolder extends ViewHolder { 149 150 private final ActionOnKeyPressAnimator mActionOnKeyPressAnimator; 151 private final ActionOnFocusAnimator mActionOnFocusAnimator; 152 private final View.OnClickListener mViewOnClickListener; 153 private Action mAction; 154 155 private BitmapCallback mPendingBitmapCallback; 156 ActionViewHolder(View v, ActionOnKeyPressAnimator actionOnKeyPressAnimator, ActionOnFocusAnimator actionOnFocusAnimator, View.OnClickListener viewOnClickListener)157 public ActionViewHolder(View v, ActionOnKeyPressAnimator actionOnKeyPressAnimator, 158 ActionOnFocusAnimator actionOnFocusAnimator, 159 View.OnClickListener viewOnClickListener) { 160 super(v); 161 mActionOnKeyPressAnimator = actionOnKeyPressAnimator; 162 mActionOnFocusAnimator = actionOnFocusAnimator; 163 mViewOnClickListener = viewOnClickListener; 164 } 165 getAction()166 public Action getAction() { 167 return mAction; 168 } 169 init(Action action)170 public void init(Action action) { 171 mAction = action; 172 173 if (mPendingBitmapCallback != null) { 174 DrawableDownloader.getInstance( 175 itemView.getContext()).cancelDownload(mPendingBitmapCallback); 176 mPendingBitmapCallback = null; 177 } 178 TextView title = (TextView) itemView.findViewById(R.id.action_title); 179 TextView description = (TextView) itemView.findViewById(R.id.action_description); 180 description.setText(action.getDescription()); 181 description.setVisibility( 182 TextUtils.isEmpty(action.getDescription()) ? View.GONE : View.VISIBLE); 183 title.setText(action.getTitle()); 184 ImageView checkmarkView = (ImageView) itemView.findViewById(R.id.action_checkmark); 185 checkmarkView.setVisibility(action.isChecked() ? View.VISIBLE : View.INVISIBLE); 186 187 ImageView indicatorView = (ImageView) itemView.findViewById(R.id.action_icon); 188 View content = itemView.findViewById(R.id.action_content); 189 ViewGroup.LayoutParams contentLp = content.getLayoutParams(); 190 if (setIndicator(indicatorView, action)) { 191 contentLp.width = itemView.getContext().getResources() 192 .getDimensionPixelSize(R.dimen.lb_action_text_width); 193 } else { 194 contentLp.width = itemView.getContext().getResources() 195 .getDimensionPixelSize(R.dimen.lb_action_text_width_no_icon); 196 } 197 content.setLayoutParams(contentLp); 198 199 ImageView chevronView = (ImageView) itemView.findViewById(R.id.action_next_chevron); 200 chevronView.setVisibility(action.hasNext() ? View.VISIBLE : View.INVISIBLE); 201 202 final Resources res = itemView.getContext().getResources(); 203 if (action.hasMultilineDescription()) { 204 title.setMaxLines(res.getInteger(R.integer.lb_dialog_action_title_max_lines)); 205 description.setMaxHeight( 206 getDescriptionMaxHeight(itemView.getContext(), title)); 207 } else { 208 title.setMaxLines(res.getInteger(R.integer.lb_dialog_action_title_min_lines)); 209 description.setMaxLines( 210 res.getInteger(R.integer.lb_dialog_action_description_min_lines)); 211 } 212 213 itemView.setTag(R.id.action_title, this); 214 itemView.setOnKeyListener(mActionOnKeyPressAnimator); 215 itemView.setOnClickListener(mViewOnClickListener); 216 itemView.setOnFocusChangeListener(mActionOnFocusAnimator); 217 mActionOnFocusAnimator.unFocus(itemView); 218 } 219 setIndicator(final ImageView indicatorView, Action action)220 private boolean setIndicator(final ImageView indicatorView, Action action) { 221 222 Context context = indicatorView.getContext(); 223 Drawable indicator = action.getIndicator(context); 224 if (indicator != null) { 225 indicatorView.setImageDrawable(indicator); 226 indicatorView.setVisibility(View.VISIBLE); 227 } else { 228 Uri iconUri = action.getIconUri(); 229 if (iconUri != null) { 230 indicatorView.setVisibility(View.INVISIBLE); 231 232 mPendingBitmapCallback = new BitmapCallback() { 233 @Override 234 public void onBitmapRetrieved(Drawable bitmap) { 235 if (bitmap != null) { 236 indicatorView.setVisibility(View.VISIBLE); 237 indicatorView.setImageDrawable(bitmap); 238 fadeIn(indicatorView); 239 } 240 mPendingBitmapCallback = null; 241 } 242 }; 243 244 DrawableDownloader.getInstance(context).getBitmap( 245 new BitmapWorkerOptions.Builder( 246 context).resource(iconUri) 247 .width(indicatorView.getLayoutParams().width).build(), 248 mPendingBitmapCallback); 249 250 } else { 251 indicatorView.setVisibility(View.GONE); 252 return false; 253 } 254 } 255 return true; 256 } 257 fadeIn(View v)258 private void fadeIn(View v) { 259 v.setAlpha(0f); 260 ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(v, 261 "alpha", 1f); 262 alphaAnimator.setDuration( 263 v.getContext().getResources().getInteger( 264 android.R.integer.config_mediumAnimTime)); 265 alphaAnimator.start(); 266 } 267 268 /** 269 * @return the max height in pixels the description can be such that the 270 * action nicely takes up the entire screen. 271 */ getDescriptionMaxHeight(Context context, TextView title)272 private int getDescriptionMaxHeight(Context context, TextView title) { 273 final Resources res = context.getResources(); 274 final float verticalPadding = res.getDimension(R.dimen.lb_dialog_list_item_vertical_padding); 275 final int titleMaxLines = res.getInteger(R.integer.lb_dialog_action_title_max_lines); 276 final int displayHeight = ((WindowManager) context.getSystemService( 277 Context.WINDOW_SERVICE)).getDefaultDisplay().getHeight(); 278 279 // The 2 multiplier on the title height calculation is a 280 // conservative estimate for font padding which can not be 281 // calculated at this stage since the view hasn't been rendered yet. 282 return (int) (displayHeight - 283 2 * verticalPadding - 2 * titleMaxLines * title.getLineHeight()); 284 } 285 286 } 287 288 private static class ActionOnFocusAnimator implements View.OnFocusChangeListener { 289 290 private boolean mResourcesSet; 291 private float mUnselectedAlpha; 292 private float mSelectedTitleAlpha; 293 private float mDisabledTitleAlpha; 294 private float mSelectedDescriptionAlpha; 295 private float mDisabledDescriptionAlpha; 296 private float mUnselectedDescriptionAlpha; 297 private float mSelectedChevronAlpha; 298 private float mDisabledChevronAlpha; 299 private int mAnimationDuration; 300 private Action.OnFocusListener mOnFocusListener; 301 private View mSelectedView; 302 ActionOnFocusAnimator(Action.OnFocusListener onFocusListener)303 ActionOnFocusAnimator(Action.OnFocusListener onFocusListener) { 304 mOnFocusListener = onFocusListener; 305 } 306 setOnFocusListener(Action.OnFocusListener onFocusListener)307 public void setOnFocusListener(Action.OnFocusListener onFocusListener) { 308 mOnFocusListener = onFocusListener; 309 } 310 unFocus(View v)311 public void unFocus(View v) { 312 changeFocus((v != null) ? v : mSelectedView, false, false); 313 } 314 315 @Override onFocusChange(View v, boolean hasFocus)316 public void onFocusChange(View v, boolean hasFocus) { 317 if (hasFocus) { 318 mSelectedView = v; 319 changeFocus(v, true /* hasFocus */, true /* shouldAnimate */); 320 if (mOnFocusListener != null) { 321 // We still call onActionFocused so that listeners can clear 322 // state if they want. 323 mOnFocusListener.onActionFocused( 324 ((ActionViewHolder) v.getTag(R.id.action_title)).getAction()); 325 } 326 } else { 327 if (mSelectedView == v) { 328 mSelectedView = null; 329 } 330 changeFocus(v, false /* hasFocus */, true /* shouldAnimate */); 331 } 332 } 333 changeFocus(View v, boolean hasFocus, boolean shouldAnimate)334 private void changeFocus(View v, boolean hasFocus, boolean shouldAnimate) { 335 if (v == null) { 336 return; 337 } 338 339 if (!mResourcesSet) { 340 mResourcesSet = true; 341 final Resources res = v.getContext().getResources(); 342 343 mAnimationDuration = res.getInteger(R.integer.lb_dialog_animation_duration); 344 mUnselectedAlpha = 345 Float.valueOf(res.getString(R.string.lb_dialog_list_item_unselected_text_alpha)); 346 347 mSelectedTitleAlpha = 348 Float.valueOf(res.getString(R.string.lb_dialog_list_item_selected_title_text_alpha)); 349 mDisabledTitleAlpha = 350 Float.valueOf(res.getString(R.string.lb_dialog_list_item_disabled_title_text_alpha)); 351 352 mSelectedDescriptionAlpha = 353 Float.valueOf( 354 res.getString(R.string.lb_dialog_list_item_selected_description_text_alpha)); 355 mUnselectedDescriptionAlpha = 356 Float.valueOf( 357 res.getString(R.string.lb_dialog_list_item_unselected_description_text_alpha)); 358 mDisabledDescriptionAlpha = 359 Float.valueOf( 360 res.getString(R.string.lb_dialog_list_item_disabled_description_text_alpha)); 361 362 mSelectedChevronAlpha = 363 Float.valueOf( 364 res.getString(R.string.lb_dialog_list_item_selected_chevron_background_alpha)); 365 mDisabledChevronAlpha = 366 Float.valueOf( 367 res.getString(R.string.lb_dialog_list_item_disabled_chevron_background_alpha)); 368 } 369 370 Action action = ((ActionViewHolder) v.getTag(R.id.action_title)).getAction(); 371 372 float titleAlpha = action.isEnabled() && !action.infoOnly() 373 ? (hasFocus ? mSelectedTitleAlpha : mUnselectedAlpha) : mDisabledTitleAlpha; 374 float descriptionAlpha = (!hasFocus || action.infoOnly()) ? mUnselectedDescriptionAlpha 375 : (action.isEnabled() ? mSelectedDescriptionAlpha : mDisabledDescriptionAlpha); 376 float chevronAlpha = action.hasNext() && !action.infoOnly() 377 ? (action.isEnabled() ? mSelectedChevronAlpha : mDisabledChevronAlpha) : 0; 378 379 TextView title = (TextView) v.findViewById(R.id.action_title); 380 setAlpha(title, shouldAnimate, titleAlpha); 381 382 TextView description = (TextView) v.findViewById(R.id.action_description); 383 setAlpha(description, shouldAnimate, descriptionAlpha); 384 385 ImageView checkmark = (ImageView) v.findViewById(R.id.action_checkmark); 386 setAlpha(checkmark, shouldAnimate, titleAlpha); 387 388 ImageView icon = (ImageView) v.findViewById(R.id.action_icon); 389 setAlpha(icon, shouldAnimate, titleAlpha); 390 391 ImageView chevron = (ImageView) v.findViewById(R.id.action_next_chevron); 392 setAlpha(chevron, shouldAnimate, chevronAlpha); 393 } 394 setAlpha(View view, boolean shouldAnimate, float alpha)395 private void setAlpha(View view, boolean shouldAnimate, float alpha) { 396 if (shouldAnimate) { 397 view.animate().alpha(alpha) 398 .setDuration(mAnimationDuration) 399 .setInterpolator(new DecelerateInterpolator(2F)) 400 .start(); 401 } else { 402 view.setAlpha(alpha); 403 } 404 } 405 } 406 407 private static class ActionOnKeyPressAnimator implements View.OnKeyListener { 408 409 private static final int SELECT_ANIM_DURATION = 100; 410 private static final int SELECT_ANIM_DELAY = 0; 411 private static final float SELECT_ANIM_SELECTED_ALPHA = 0.2f; 412 private static final float SELECT_ANIM_UNSELECTED_ALPHA = 1.0f; 413 private static final float CHECKMARK_ANIM_UNSELECTED_ALPHA = 0.0f; 414 private static final float CHECKMARK_ANIM_SELECTED_ALPHA = 1.0f; 415 416 private final List<Action> mActions; 417 private boolean mKeyPressed = false; 418 private Action.Listener mListener; 419 ActionOnKeyPressAnimator(Action.Listener listener, List<Action> actions)420 public ActionOnKeyPressAnimator(Action.Listener listener, 421 List<Action> actions) { 422 mListener = listener; 423 mActions = actions; 424 } 425 setListener(Action.Listener listener)426 public void setListener(Action.Listener listener) { 427 mListener = listener; 428 } 429 playSound(Context context, int soundEffect)430 private void playSound(Context context, int soundEffect) { 431 AudioManager manager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); 432 manager.playSoundEffect(soundEffect); 433 } 434 435 /** 436 * Now only handles KEYCODE_ENTER and KEYCODE_NUMPAD_ENTER key event. 437 */ 438 @Override onKey(View v, int keyCode, KeyEvent event)439 public boolean onKey(View v, int keyCode, KeyEvent event) { 440 if (v == null) { 441 return false; 442 } 443 boolean handled = false; 444 Action action = ((ActionViewHolder) v.getTag(R.id.action_title)).getAction(); 445 switch (keyCode) { 446 case KeyEvent.KEYCODE_DPAD_CENTER: 447 case KeyEvent.KEYCODE_NUMPAD_ENTER: 448 case KeyEvent.KEYCODE_BUTTON_X: 449 case KeyEvent.KEYCODE_BUTTON_Y: 450 case KeyEvent.KEYCODE_ENTER: 451 452 if (!action.isEnabled() || action.infoOnly()) { 453 if (v.isSoundEffectsEnabled() 454 && event.getAction() == KeyEvent.ACTION_DOWN) { 455 // TODO: requires API 19 456 //playSound(v.getContext(), AudioManager.FX_KEYPRESS_INVALID); 457 } 458 return true; 459 } 460 461 switch (event.getAction()) { 462 case KeyEvent.ACTION_DOWN: 463 if (!mKeyPressed) { 464 mKeyPressed = true; 465 466 if (v.isSoundEffectsEnabled()) { 467 playSound(v.getContext(), AudioManager.FX_KEY_CLICK); 468 } 469 470 if (DEBUG) { 471 Log.d(TAG, "Enter Key down"); 472 } 473 474 prepareAndAnimateView(v, SELECT_ANIM_UNSELECTED_ALPHA, 475 SELECT_ANIM_SELECTED_ALPHA, SELECT_ANIM_DURATION, 476 SELECT_ANIM_DELAY, null, mKeyPressed); 477 handled = true; 478 } 479 break; 480 case KeyEvent.ACTION_UP: 481 if (mKeyPressed) { 482 mKeyPressed = false; 483 484 if (DEBUG) { 485 Log.d(TAG, "Enter Key up"); 486 } 487 488 prepareAndAnimateView(v, SELECT_ANIM_SELECTED_ALPHA, 489 SELECT_ANIM_UNSELECTED_ALPHA, SELECT_ANIM_DURATION, 490 SELECT_ANIM_DELAY, null, mKeyPressed); 491 handled = true; 492 } 493 break; 494 default: 495 break; 496 } 497 break; 498 default: 499 break; 500 } 501 return handled; 502 } 503 prepareAndAnimateView(final View v, float initAlpha, float destAlpha, int duration, int delay, Interpolator interpolator, final boolean pressed)504 private void prepareAndAnimateView(final View v, float initAlpha, float destAlpha, 505 int duration, 506 int delay, Interpolator interpolator, final boolean pressed) { 507 if (v != null && v.getWindowToken() != null) { 508 final Action action = ((ActionViewHolder) v.getTag(R.id.action_title)).getAction(); 509 510 if (!pressed) { 511 fadeCheckmarks(v, action, duration, delay, interpolator); 512 } 513 514 v.setAlpha(initAlpha); 515 v.setLayerType(View.LAYER_TYPE_HARDWARE, null); 516 v.buildLayer(); 517 v.animate().alpha(destAlpha).setDuration(duration).setStartDelay(delay); 518 if (interpolator != null) { 519 v.animate().setInterpolator(interpolator); 520 } 521 v.animate().setListener(new AnimatorListenerAdapter() { 522 @Override 523 public void onAnimationEnd(Animator animation) { 524 525 v.setLayerType(View.LAYER_TYPE_NONE, null); 526 if (!pressed) { 527 if (mListener != null) { 528 mListener.onActionClicked(action); 529 } 530 } 531 } 532 }); 533 v.animate().start(); 534 } 535 } 536 fadeCheckmarks(final View v, final Action action, int duration, int delay, Interpolator interpolator)537 private void fadeCheckmarks(final View v, final Action action, int duration, int delay, 538 Interpolator interpolator) { 539 int actionCheckSetId = action.getCheckSetId(); 540 if (actionCheckSetId != Action.NO_CHECK_SET) { 541 ViewGroup parent = (ViewGroup) v.getTag(R.layout.lb_dialog_action_list_item); 542 // Find any actions that are checked and are in the same group 543 // as the selected action. Fade their checkmarks out. 544 for (int i = 0, size = mActions.size(); i < size; i++) { 545 Action a = mActions.get(i); 546 if (a != action && a.getCheckSetId() == actionCheckSetId && a.isChecked()) { 547 a.setChecked(false); 548 View viewToAnimateOut = parent.getChildAt(i); 549 if (viewToAnimateOut != null) { 550 final View checkView = viewToAnimateOut.findViewById( 551 R.id.action_checkmark); 552 checkView.animate().alpha(CHECKMARK_ANIM_UNSELECTED_ALPHA) 553 .setDuration(duration).setStartDelay(delay); 554 if (interpolator != null) { 555 checkView.animate().setInterpolator(interpolator); 556 } 557 checkView.animate().setListener(new AnimatorListenerAdapter() { 558 @Override 559 public void onAnimationEnd(Animator animation) { 560 checkView.setVisibility(View.INVISIBLE); 561 } 562 }); 563 } 564 } 565 } 566 567 // If we we'ren't already checked, fade our checkmark in. 568 if (!action.isChecked()) { 569 action.setChecked(true); 570 final View checkView = v.findViewById(R.id.action_checkmark); 571 checkView.setVisibility(View.VISIBLE); 572 checkView.setAlpha(CHECKMARK_ANIM_UNSELECTED_ALPHA); 573 checkView.animate().alpha(CHECKMARK_ANIM_SELECTED_ALPHA).setDuration(duration) 574 .setStartDelay(delay); 575 if (interpolator != null) { 576 checkView.animate().setInterpolator(interpolator); 577 } 578 checkView.animate().setListener(null); 579 } 580 } 581 } 582 } 583 } 584