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