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 DialogActionAdapter(Action.Listener listener, Action.OnFocusListener onFocusListener, List<Action> actions)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 onCreateViewHolder(ViewGroup parent, int viewType)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 onBindViewHolder(RecyclerView.ViewHolder baseHolder, int position)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 getItemCount()109 public int getItemCount() { 110 return mActions.size(); 111 } 112 getCount()113 public int getCount() { 114 return mActions.size(); 115 } 116 getItem(int position)117 public Action getItem(int position) { 118 return mActions.get(position); 119 } 120 setListener(Action.Listener listener)121 public void setListener(Action.Listener listener) { 122 mListener = listener; 123 mActionOnKeyPressAnimator.setListener(listener); 124 } 125 setOnFocusListener(Action.OnFocusListener onFocusListener)126 public void setOnFocusListener(Action.OnFocusListener onFocusListener) { 127 mActionOnFocusAnimator.setOnFocusListener(onFocusListener); 128 } 129 130 /** 131 * Used for serialization only. 132 */ getActions()133 public ArrayList<Action> getActions() { 134 return new ArrayList<Action>(mActions); 135 } 136 setActions(ArrayList<Action> actions)137 public void setActions(ArrayList<Action> actions) { 138 mActionOnFocusAnimator.unFocus(null); 139 mActions.clear(); 140 mActions.addAll(actions); 141 notifyDataSetChanged(); 142 } 143 registerDataSetObserver(DataSetObserver dataSetObserver)144 public void registerDataSetObserver(DataSetObserver dataSetObserver) { 145 } 146 setOnItemSelectedListener(OnItemSelectedListener listener)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 ActionViewHolder(View v, ActionOnKeyPressAnimator actionOnKeyPressAnimator, ActionOnFocusAnimator actionOnFocusAnimator, View.OnClickListener viewOnClickListener)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 getAction()168 public Action getAction() { 169 return mAction; 170 } 171 init(Action action)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 setIndicator(final ImageView indicatorView, Action action)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 fadeIn(View v)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 */ getDescriptionMaxHeight(Context context, TextView title)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 ActionOnFocusAnimator(Action.OnFocusListener onFocusListener)305 ActionOnFocusAnimator(Action.OnFocusListener onFocusListener) { 306 mOnFocusListener = onFocusListener; 307 } 308 setOnFocusListener(Action.OnFocusListener onFocusListener)309 public void setOnFocusListener(Action.OnFocusListener onFocusListener) { 310 mOnFocusListener = onFocusListener; 311 } 312 unFocus(View v)313 public void unFocus(View v) { 314 changeFocus((v != null) ? v : mSelectedView, false, false); 315 } 316 317 @Override onFocusChange(View v, boolean hasFocus)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 changeFocus(View v, boolean hasFocus, boolean shouldAnimate)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 setAlpha(View view, boolean shouldAnimate, float alpha)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 ActionOnKeyPressAnimator(Action.Listener listener, List<Action> actions)422 public ActionOnKeyPressAnimator(Action.Listener listener, 423 List<Action> actions) { 424 mListener = listener; 425 mActions = actions; 426 } 427 setListener(Action.Listener listener)428 public void setListener(Action.Listener listener) { 429 mListener = listener; 430 } 431 playSound(Context context, int soundEffect)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 onKey(View v, int keyCode, KeyEvent event)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 prepareAndAnimateView(final View v, float initAlpha, float destAlpha, int duration, int delay, Interpolator interpolator, final boolean pressed)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 fadeCheckmarks(final View v, final Action action, int duration, int delay, Interpolator interpolator)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