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 17package com.android.tv.settings.dialog; 18 19import android.animation.Animator; 20import android.animation.AnimatorListenerAdapter; 21import android.animation.ObjectAnimator; 22import android.content.Context; 23import android.content.res.Resources; 24import android.database.DataSetObserver; 25import android.graphics.drawable.Drawable; 26import android.media.AudioManager; 27import android.net.Uri; 28import android.support.v7.widget.RecyclerView; 29import android.support.v7.widget.RecyclerView.ViewHolder; 30import android.text.TextUtils; 31import android.util.Log; 32import android.view.KeyEvent; 33import android.view.LayoutInflater; 34import android.view.View; 35import android.view.ViewGroup; 36import android.view.WindowManager; 37import android.view.animation.DecelerateInterpolator; 38import android.view.animation.Interpolator; 39import android.widget.AdapterView.OnItemSelectedListener; 40import android.widget.ImageView; 41import android.widget.TextView; 42 43import com.android.tv.settings.R; 44import com.android.tv.settings.dialog.DialogFragment.Action; 45import com.android.tv.settings.widget.BitmapWorkerOptions; 46import com.android.tv.settings.widget.DrawableDownloader; 47import com.android.tv.settings.widget.DrawableDownloader.BitmapCallback; 48 49import java.util.ArrayList; 50import java.util.List; 51 52/** 53 * Adapter class which creates actions. 54 * 55 * @hide 56 */ 57class 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 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 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 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 107 public int getItemCount() { 108 return mActions.size(); 109 } 110 111 public int getCount() { 112 return mActions.size(); 113 } 114 115 public Action getItem(int position) { 116 return mActions.get(position); 117 } 118 119 public void setListener(Action.Listener listener) { 120 mListener = listener; 121 mActionOnKeyPressAnimator.setListener(listener); 122 } 123 124 public void setOnFocusListener(Action.OnFocusListener onFocusListener) { 125 mActionOnFocusAnimator.setOnFocusListener(onFocusListener); 126 } 127 128 /** 129 * Used for serialization only. 130 */ 131 public ArrayList<Action> getActions() { 132 return new ArrayList<Action>(mActions); 133 } 134 135 public void setActions(ArrayList<Action> actions) { 136 mActionOnFocusAnimator.unFocus(null); 137 mActions.clear(); 138 mActions.addAll(actions); 139 notifyDataSetChanged(); 140 } 141 142 public void registerDataSetObserver(DataSetObserver dataSetObserver) { 143 } 144 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 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 166 public Action getAction() { 167 return mAction; 168 } 169 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 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 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 */ 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 303 ActionOnFocusAnimator(Action.OnFocusListener onFocusListener) { 304 mOnFocusListener = onFocusListener; 305 } 306 307 public void setOnFocusListener(Action.OnFocusListener onFocusListener) { 308 mOnFocusListener = onFocusListener; 309 } 310 311 public void unFocus(View v) { 312 changeFocus((v != null) ? v : mSelectedView, false, false); 313 } 314 315 @Override 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 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 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 420 public ActionOnKeyPressAnimator(Action.Listener listener, 421 List<Action> actions) { 422 mListener = listener; 423 mActions = actions; 424 } 425 426 public void setListener(Action.Listener listener) { 427 mListener = listener; 428 } 429 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 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 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 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