PlaybackOverlayFragment.java revision d20507e0f5ac7ad021f42ca87c294787246f0591
1/* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 * in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the License 10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 * or implied. See the License for the specific language governing permissions and limitations under 12 * the License. 13 */ 14package android.support.v17.leanback.app; 15 16import android.graphics.Color; 17import android.graphics.drawable.ColorDrawable; 18import android.animation.Animator; 19import android.animation.AnimatorInflater; 20import android.animation.TimeInterpolator; 21import android.animation.ValueAnimator; 22import android.view.animation.AccelerateInterpolator; 23import android.animation.ValueAnimator.AnimatorUpdateListener; 24import android.content.Context; 25import android.os.Bundle; 26import android.os.Handler; 27import android.os.Message; 28import android.support.v7.widget.RecyclerView; 29import android.support.v17.leanback.R; 30import android.support.v17.leanback.animation.LogAccelerateInterpolator; 31import android.support.v17.leanback.animation.LogDecelerateInterpolator; 32import android.support.v17.leanback.widget.Presenter; 33import android.support.v17.leanback.widget.ItemBridgeAdapter; 34import android.support.v17.leanback.widget.ObjectAdapter; 35import android.support.v17.leanback.widget.ObjectAdapter.DataObserver; 36import android.support.v17.leanback.widget.VerticalGridView; 37import android.support.v17.leanback.widget.PlaybackControlsRowPresenter; 38import android.util.Log; 39import android.view.KeyEvent; 40import android.view.LayoutInflater; 41import android.view.MotionEvent; 42import android.view.View; 43import android.view.ViewGroup; 44import android.view.ViewGroup.MarginLayoutParams; 45import android.view.animation.Interpolator; 46import android.view.animation.LinearInterpolator; 47 48 49/** 50 * A fragment for displaying playback controls and related content. 51 * The {@link android.support.v17.leanback.widget.PlaybackControlsRow} is expected to be 52 * at position 0 in the adapter. 53 */ 54public class PlaybackOverlayFragment extends DetailsFragment { 55 56 /** 57 * No background. 58 */ 59 public static final int BG_NONE = 0; 60 61 /** 62 * A dark translucent background. 63 */ 64 public static final int BG_DARK = 1; 65 66 /** 67 * A light translucent background. 68 */ 69 public static final int BG_LIGHT = 2; 70 71 public static class OnFadeCompleteListener { 72 public void onFadeInComplete() { 73 } 74 public void onFadeOutComplete() { 75 } 76 } 77 78 private static final String TAG = "PlaybackOverlayFragment"; 79 private static final boolean DEBUG = false; 80 private static final int ANIMATION_MULTIPLIER = 1; 81 82 private static int START_FADE_OUT = 1; 83 84 // Fading status 85 private static final int IDLE = 0; 86 private static final int IN = 1; 87 private static final int OUT = 2; 88 89 private int mAlignPosition; 90 private int mPaddingBottom; 91 private View mRootView; 92 private int mBackgroundType = BG_DARK; 93 private int mBgDarkColor; 94 private int mBgLightColor; 95 private int mShowTimeMs; 96 private int mMajorFadeTranslateY, mMinorFadeTranslateY; 97 private int mAnimationTranslateY; 98 private OnFadeCompleteListener mFadeCompleteListener; 99 private boolean mFadingEnabled = true; 100 private int mFadingStatus = IDLE; 101 private int mBgAlpha; 102 private ValueAnimator mBgFadeInAnimator, mBgFadeOutAnimator; 103 private ValueAnimator mControlRowFadeInAnimator, mControlRowFadeOutAnimator; 104 private ValueAnimator mDescriptionFadeInAnimator, mDescriptionFadeOutAnimator; 105 private ValueAnimator mOtherRowFadeInAnimator, mOtherRowFadeOutAnimator; 106 private boolean mTranslateAnimationEnabled; 107 private RecyclerView.ItemAnimator mItemAnimator; 108 109 private final Animator.AnimatorListener mFadeListener = 110 new Animator.AnimatorListener() { 111 @Override 112 public void onAnimationStart(Animator animation) { 113 enableVerticalGridAnimations(false); 114 } 115 @Override 116 public void onAnimationRepeat(Animator animation) { 117 } 118 @Override 119 public void onAnimationCancel(Animator animation) { 120 } 121 @Override 122 public void onAnimationEnd(Animator animation) { 123 if (DEBUG) Log.v(TAG, "onAnimationEnd " + mBgAlpha); 124 if (mBgAlpha > 0) { 125 enableVerticalGridAnimations(true); 126 startFadeTimer(); 127 if (mFadeCompleteListener != null) { 128 mFadeCompleteListener.onFadeInComplete(); 129 } 130 } else { 131 if (getVerticalGridView() != null) { 132 // Reset focus to the controls row 133 getVerticalGridView().setSelectedPosition(0); 134 } 135 if (mFadeCompleteListener != null) { 136 mFadeCompleteListener.onFadeOutComplete(); 137 } 138 } 139 mFadingStatus = IDLE; 140 } 141 }; 142 143 private final Handler mHandler = new Handler() { 144 @Override 145 public void handleMessage(Message message) { 146 if (message.what == START_FADE_OUT && mFadingEnabled) { 147 fade(false); 148 } 149 } 150 }; 151 152 private final VerticalGridView.OnTouchInterceptListener mOnTouchInterceptListener = 153 new VerticalGridView.OnTouchInterceptListener() { 154 public boolean onInterceptTouchEvent(MotionEvent event) { 155 return onInterceptInputEvent(); 156 } 157 }; 158 159 private final VerticalGridView.OnMotionInterceptListener mOnMotionInterceptListener = 160 new VerticalGridView.OnMotionInterceptListener() { 161 public boolean onInterceptMotionEvent(MotionEvent event) { 162 return onInterceptInputEvent(); 163 } 164 }; 165 166 private final VerticalGridView.OnKeyInterceptListener mOnKeyInterceptListener = 167 new VerticalGridView.OnKeyInterceptListener() { 168 public boolean onInterceptKeyEvent(KeyEvent event) { 169 return onInterceptInputEvent(); 170 } 171 }; 172 173 private void setBgAlpha(int alpha) { 174 mBgAlpha = alpha; 175 if (mRootView != null) { 176 mRootView.getBackground().setAlpha(alpha); 177 } 178 } 179 180 private void enableVerticalGridAnimations(boolean enable) { 181 if (getVerticalGridView() == null) { 182 return; 183 } 184 if (enable && mItemAnimator != null) { 185 getVerticalGridView().setItemAnimator(mItemAnimator); 186 } else if (!enable) { 187 mItemAnimator = getVerticalGridView().getItemAnimator(); 188 getVerticalGridView().setItemAnimator(null); 189 } 190 } 191 192 /** 193 * Enables or disables view fading. If enabled, 194 * the view will be faded in when the fragment starts, 195 * and will fade out after a time period. The timeout 196 * period is reset each time {@link #tickle} is called. 197 * 198 */ 199 public void setFadingEnabled(boolean enabled) { 200 if (DEBUG) Log.v(TAG, "setFadingEnabled " + enabled); 201 if (enabled != mFadingEnabled) { 202 mFadingEnabled = enabled; 203 if (isResumed()) { 204 if (mFadingEnabled) { 205 if (mFadingStatus == IDLE && !mHandler.hasMessages(START_FADE_OUT)) { 206 startFadeTimer(); 207 } 208 } else { 209 // Ensure fully opaque 210 mHandler.removeMessages(START_FADE_OUT); 211 fade(true); 212 } 213 } 214 } 215 } 216 217 /** 218 * Returns true if view fading is enabled. 219 */ 220 public boolean isFadingEnabled() { 221 return mFadingEnabled; 222 } 223 224 /** 225 * Sets the listener to be called when fade in or out has completed. 226 */ 227 public void setFadeCompleteListener(OnFadeCompleteListener listener) { 228 mFadeCompleteListener = listener; 229 } 230 231 /** 232 * Returns the listener to be called when fade in or out has completed. 233 */ 234 public OnFadeCompleteListener getFadeCompleteListener() { 235 return mFadeCompleteListener; 236 } 237 238 /** 239 * Tickles the playback controls. Fades in the view if it was faded out, 240 * otherwise resets the fade out timer. Tickling on input events is handled 241 * by the fragment. 242 */ 243 public void tickle() { 244 if (DEBUG) Log.v(TAG, "tickle enabled " + mFadingEnabled + " isResumed " + isResumed()); 245 if (!mFadingEnabled || !isResumed()) { 246 return; 247 } 248 if (mHandler.hasMessages(START_FADE_OUT)) { 249 // Restart the timer 250 startFadeTimer(); 251 } else { 252 fade(true); 253 } 254 } 255 256 private boolean onInterceptInputEvent() { 257 if (DEBUG) Log.v(TAG, "onInterceptInputEvent status " + mFadingStatus); 258 boolean consumeEvent = (mFadingStatus == IDLE && mBgAlpha == 0); 259 tickle(); 260 return consumeEvent; 261 } 262 263 @Override 264 public void onResume() { 265 super.onResume(); 266 if (mFadingEnabled) { 267 setBgAlpha(0); 268 fade(true); 269 } 270 getVerticalGridView().setOnTouchInterceptListener(mOnTouchInterceptListener); 271 getVerticalGridView().setOnMotionInterceptListener(mOnMotionInterceptListener); 272 getVerticalGridView().setOnKeyInterceptListener(mOnKeyInterceptListener); 273 } 274 275 private void startFadeTimer() { 276 if (mHandler != null) { 277 mHandler.removeMessages(START_FADE_OUT); 278 mHandler.sendEmptyMessageDelayed(START_FADE_OUT, mShowTimeMs); 279 } 280 } 281 282 private static ValueAnimator loadAnimator(Context context, int resId) { 283 ValueAnimator animator = (ValueAnimator) AnimatorInflater.loadAnimator(context, resId); 284 animator.setDuration(animator.getDuration() * ANIMATION_MULTIPLIER); 285 return animator; 286 } 287 288 private void loadBgAnimator() { 289 AnimatorUpdateListener listener = new AnimatorUpdateListener() { 290 @Override 291 public void onAnimationUpdate(ValueAnimator arg0) { 292 setBgAlpha((Integer) arg0.getAnimatedValue()); 293 } 294 }; 295 296 mBgFadeInAnimator = loadAnimator(getActivity(), R.animator.lb_playback_bg_fade_in); 297 mBgFadeInAnimator.addUpdateListener(listener); 298 mBgFadeInAnimator.addListener(mFadeListener); 299 300 mBgFadeOutAnimator = loadAnimator(getActivity(), R.animator.lb_playback_bg_fade_out); 301 mBgFadeOutAnimator.addUpdateListener(listener); 302 mBgFadeOutAnimator.addListener(mFadeListener); 303 } 304 305 private TimeInterpolator mLogDecelerateInterpolator = new LogDecelerateInterpolator(100,0); 306 private TimeInterpolator mLogAccelerateInterpolator = new LogAccelerateInterpolator(100,0); 307 308 private void loadControlRowAnimator() { 309 AnimatorUpdateListener listener = new AnimatorUpdateListener() { 310 @Override 311 public void onAnimationUpdate(ValueAnimator arg0) { 312 if (getVerticalGridView() == null) { 313 return; 314 } 315 RecyclerView.ViewHolder vh = getVerticalGridView().findViewHolderForPosition(0); 316 if (vh != null) { 317 final float fraction = (Float) arg0.getAnimatedValue(); 318 if (DEBUG) Log.v(TAG, "fraction " + fraction); 319 vh.itemView.setAlpha(fraction); 320 vh.itemView.setTranslationY((float) mAnimationTranslateY * (1f - fraction)); 321 } 322 } 323 }; 324 325 mControlRowFadeInAnimator = loadAnimator( 326 getActivity(), R.animator.lb_playback_controls_fade_in); 327 mControlRowFadeInAnimator.addUpdateListener(listener); 328 mControlRowFadeInAnimator.setInterpolator(mLogDecelerateInterpolator); 329 330 mControlRowFadeOutAnimator = loadAnimator( 331 getActivity(), R.animator.lb_playback_controls_fade_out); 332 mControlRowFadeOutAnimator.addUpdateListener(listener); 333 mControlRowFadeOutAnimator.setInterpolator(mLogAccelerateInterpolator); 334 } 335 336 private void loadOtherRowAnimator() { 337 AnimatorUpdateListener listener = new AnimatorUpdateListener() { 338 @Override 339 public void onAnimationUpdate(ValueAnimator arg0) { 340 if (getVerticalGridView() == null) { 341 return; 342 } 343 final float fraction = (Float) arg0.getAnimatedValue(); 344 final int count = getVerticalGridView().getChildCount(); 345 for (int i = 0; i < count; i++) { 346 View view = getVerticalGridView().getChildAt(i); 347 if (getVerticalGridView().getChildPosition(view) > 0) { 348 view.setAlpha(fraction); 349 view.setTranslationY((float) mAnimationTranslateY * (1f - fraction)); 350 } 351 } 352 } 353 }; 354 355 mOtherRowFadeInAnimator = loadAnimator( 356 getActivity(), R.animator.lb_playback_controls_fade_in); 357 mOtherRowFadeInAnimator.addUpdateListener(listener); 358 mOtherRowFadeInAnimator.setInterpolator(mLogDecelerateInterpolator); 359 360 mOtherRowFadeOutAnimator = loadAnimator( 361 getActivity(), R.animator.lb_playback_controls_fade_out); 362 mOtherRowFadeOutAnimator.addUpdateListener(listener); 363 mOtherRowFadeOutAnimator.setInterpolator(new AccelerateInterpolator()); 364 } 365 366 private void loadDescriptionAnimator() { 367 AnimatorUpdateListener listener = new AnimatorUpdateListener() { 368 @Override 369 public void onAnimationUpdate(ValueAnimator arg0) { 370 if (getVerticalGridView() == null) { 371 return; 372 } 373 ItemBridgeAdapter.ViewHolder adapterVh = (ItemBridgeAdapter.ViewHolder) 374 getVerticalGridView().findViewHolderForPosition(0); 375 if (adapterVh != null && adapterVh.getViewHolder() 376 instanceof PlaybackControlsRowPresenter.ViewHolder) { 377 final Presenter.ViewHolder vh = ((PlaybackControlsRowPresenter.ViewHolder) 378 adapterVh.getViewHolder()).mDescriptionViewHolder; 379 vh.view.setAlpha((Float) arg0.getAnimatedValue()); 380 } 381 } 382 }; 383 384 mDescriptionFadeInAnimator = loadAnimator( 385 getActivity(), R.animator.lb_playback_description_fade_in); 386 mDescriptionFadeInAnimator.addUpdateListener(listener); 387 mDescriptionFadeInAnimator.setInterpolator(mLogDecelerateInterpolator); 388 389 mDescriptionFadeOutAnimator = loadAnimator( 390 getActivity(), R.animator.lb_playback_description_fade_out); 391 mDescriptionFadeOutAnimator.addUpdateListener(listener); 392 } 393 394 private void fade(boolean fadeIn) { 395 if (DEBUG) Log.v(TAG, "fade " + fadeIn); 396 if (getView() == null) { 397 return; 398 } 399 if ((fadeIn && mFadingStatus == IN) || (!fadeIn && mFadingStatus == OUT)) { 400 if (DEBUG) Log.v(TAG, "requested fade in progress"); 401 return; 402 } 403 if ((fadeIn && mBgAlpha == 255) || (!fadeIn && mBgAlpha == 0)) { 404 if (DEBUG) Log.v(TAG, "fade is no-op"); 405 return; 406 } 407 408 mAnimationTranslateY = getVerticalGridView().getSelectedPosition() == 0 ? 409 mMajorFadeTranslateY : mMinorFadeTranslateY; 410 411 if (mFadingStatus == IDLE) { 412 if (fadeIn) { 413 mBgFadeInAnimator.start(); 414 mControlRowFadeInAnimator.start(); 415 mOtherRowFadeInAnimator.start(); 416 mDescriptionFadeInAnimator.start(); 417 } else { 418 mBgFadeOutAnimator.start(); 419 mControlRowFadeOutAnimator.start(); 420 mOtherRowFadeOutAnimator.start(); 421 mDescriptionFadeOutAnimator.start(); 422 } 423 } else { 424 if (fadeIn) { 425 mBgFadeOutAnimator.reverse(); 426 mControlRowFadeOutAnimator.reverse(); 427 mOtherRowFadeOutAnimator.reverse(); 428 mDescriptionFadeOutAnimator.reverse(); 429 } else { 430 mBgFadeInAnimator.reverse(); 431 mControlRowFadeInAnimator.reverse(); 432 mOtherRowFadeInAnimator.reverse(); 433 mDescriptionFadeInAnimator.reverse(); 434 } 435 } 436 437 // If fading in while control row is focused, set initial translationY so 438 // views slide in from below. 439 if (fadeIn && mFadingStatus == IDLE) { 440 final int count = getVerticalGridView().getChildCount(); 441 for (int i = 0; i < count; i++) { 442 getVerticalGridView().getChildAt(i).setTranslationY(mAnimationTranslateY); 443 } 444 } 445 446 mFadingStatus = fadeIn ? IN : OUT; 447 } 448 449 /** 450 * Sets the list of rows for the fragment. 451 */ 452 @Override 453 public void setAdapter(ObjectAdapter adapter) { 454 if (getAdapter() != null) { 455 getAdapter().unregisterObserver(mObserver); 456 } 457 super.setAdapter(adapter); 458 if (adapter != null) { 459 adapter.registerObserver(mObserver); 460 } 461 } 462 463 @Override 464 void setVerticalGridViewLayout(VerticalGridView listview) { 465 if (listview == null) { 466 return; 467 } 468 // Padding affects alignment when last row is focused 469 // (last is first when there's only one row). 470 setBottomPadding(listview, mPaddingBottom); 471 472 // Item alignment affects focused row that isn't the last. 473 listview.setItemAlignmentOffset(mAlignPosition); 474 listview.setItemAlignmentOffsetPercent(100); 475 476 // Push rows to the bottom. 477 listview.setWindowAlignmentOffset(0); 478 listview.setWindowAlignmentOffsetPercent(100); 479 listview.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_HIGH_EDGE); 480 } 481 482 private static void setBottomPadding(View view, int padding) { 483 view.setPadding(view.getPaddingLeft(), view.getPaddingTop(), 484 view.getPaddingRight(), padding); 485 } 486 487 @Override 488 public void onCreate(Bundle savedInstanceState) { 489 super.onCreate(savedInstanceState); 490 491 mAlignPosition = 492 getResources().getDimensionPixelSize(R.dimen.lb_playback_controls_align_bottom); 493 mPaddingBottom = 494 getResources().getDimensionPixelSize(R.dimen.lb_playback_controls_padding_bottom); 495 mBgDarkColor = 496 getResources().getColor(R.color.lb_playback_controls_background_dark); 497 mBgLightColor = 498 getResources().getColor(R.color.lb_playback_controls_background_light); 499 mShowTimeMs = 500 getResources().getInteger(R.integer.lb_playback_controls_show_time_ms); 501 mMajorFadeTranslateY = 502 getResources().getDimensionPixelSize(R.dimen.lb_playback_major_fade_translate_y); 503 mMinorFadeTranslateY = 504 getResources().getDimensionPixelSize(R.dimen.lb_playback_minor_fade_translate_y); 505 506 loadBgAnimator(); 507 loadControlRowAnimator(); 508 loadOtherRowAnimator(); 509 loadDescriptionAnimator(); 510 } 511 512 /** 513 * Sets the background type. 514 * 515 * @param type One of BG_LIGHT, BG_DARK, or BG_NONE. 516 */ 517 public void setBackgroundType(int type) { 518 switch (type) { 519 case BG_LIGHT: 520 case BG_DARK: 521 case BG_NONE: 522 if (type != mBackgroundType) { 523 mBackgroundType = type; 524 updateBackground(); 525 } 526 break; 527 default: 528 throw new IllegalArgumentException("Invalid background type"); 529 } 530 } 531 532 /** 533 * Returns the background type. 534 */ 535 public int getBackgroundType() { 536 return mBackgroundType; 537 } 538 539 private void updateBackground() { 540 if (mRootView != null) { 541 int color = mBgDarkColor; 542 switch (mBackgroundType) { 543 case BG_DARK: break; 544 case BG_LIGHT: color = mBgLightColor; break; 545 case BG_NONE: color = Color.TRANSPARENT; break; 546 } 547 mRootView.setBackground(new ColorDrawable(color)); 548 } 549 } 550 551 private void updateControlsBottomSpace(ItemBridgeAdapter.ViewHolder vh) { 552 // Add extra space between rows 0 and 1 553 if (vh == null && getVerticalGridView() != null) { 554 vh = (ItemBridgeAdapter.ViewHolder) 555 getVerticalGridView().findViewHolderForPosition(0); 556 } 557 if (vh != null && vh.getPresenter() instanceof PlaybackControlsRowPresenter) { 558 final int adapterSize = getAdapter() == null ? 0 : getAdapter().size(); 559 ((PlaybackControlsRowPresenter) vh.getPresenter()).showBottomSpace( 560 (PlaybackControlsRowPresenter.ViewHolder) vh.getViewHolder(), 561 adapterSize > 1); 562 } 563 } 564 565 private final ItemBridgeAdapter.AdapterListener mAdapterListener = 566 new ItemBridgeAdapter.AdapterListener() { 567 @Override 568 public void onAttachedToWindow(ItemBridgeAdapter.ViewHolder vh) { 569 if (DEBUG) Log.v(TAG, "onAttachedToWindow " + vh.getViewHolder().view); 570 if ((mFadingStatus == IDLE && mBgAlpha == 0) || mFadingStatus == OUT) { 571 if (DEBUG) Log.v(TAG, "setting alpha to 0"); 572 vh.getViewHolder().view.setAlpha(0); 573 } 574 } 575 @Override 576 public void onDetachedFromWindow(ItemBridgeAdapter.ViewHolder vh) { 577 if (DEBUG) Log.v(TAG, "onDetachedFromWindow " + vh.getViewHolder().view); 578 // Reset animation state 579 vh.getViewHolder().view.setAlpha(1f); 580 vh.getViewHolder().view.setTranslationY(0); 581 if (vh.getViewHolder() instanceof PlaybackControlsRowPresenter.ViewHolder) { 582 ((PlaybackControlsRowPresenter.ViewHolder) vh.getViewHolder()) 583 .mDescriptionViewHolder.view.setAlpha(1f); 584 } 585 } 586 @Override 587 public void onBind(ItemBridgeAdapter.ViewHolder vh) { 588 if (vh.getPosition() == 0) { 589 updateControlsBottomSpace(vh); 590 } 591 } 592 }; 593 594 @Override 595 public View onCreateView(LayoutInflater inflater, ViewGroup container, 596 Bundle savedInstanceState) { 597 mRootView = super.onCreateView(inflater, container, savedInstanceState); 598 mBgAlpha = 255; 599 updateBackground(); 600 getRowsFragment().setExternalAdapterListener(mAdapterListener); 601 return mRootView; 602 } 603 604 @Override 605 public void onDestroyView() { 606 mRootView = null; 607 super.onDestroyView(); 608 } 609 610 private final DataObserver mObserver = new DataObserver() { 611 public void onChanged() { 612 updateControlsBottomSpace(null); 613 } 614 }; 615} 616