1 /* 2 * Copyright (C) 2016 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 */ 14 package androidx.leanback.app; 15 16 import android.animation.Animator; 17 import android.animation.AnimatorInflater; 18 import android.animation.TimeInterpolator; 19 import android.animation.ValueAnimator; 20 import android.animation.ValueAnimator.AnimatorUpdateListener; 21 import android.content.Context; 22 import android.graphics.Color; 23 import android.graphics.drawable.ColorDrawable; 24 import android.os.Bundle; 25 import android.os.Handler; 26 import android.os.Message; 27 import android.util.Log; 28 import android.view.InputEvent; 29 import android.view.KeyEvent; 30 import android.view.LayoutInflater; 31 import android.view.MotionEvent; 32 import android.view.View; 33 import android.view.ViewGroup; 34 import android.view.animation.AccelerateInterpolator; 35 36 import androidx.annotation.NonNull; 37 import androidx.annotation.Nullable; 38 import androidx.annotation.RestrictTo; 39 import androidx.fragment.app.Fragment; 40 import androidx.leanback.R; 41 import androidx.leanback.animation.LogAccelerateInterpolator; 42 import androidx.leanback.animation.LogDecelerateInterpolator; 43 import androidx.leanback.media.PlaybackGlueHost; 44 import androidx.leanback.widget.ArrayObjectAdapter; 45 import androidx.leanback.widget.BaseOnItemViewClickedListener; 46 import androidx.leanback.widget.BaseOnItemViewSelectedListener; 47 import androidx.leanback.widget.ClassPresenterSelector; 48 import androidx.leanback.widget.ItemAlignmentFacet; 49 import androidx.leanback.widget.ItemBridgeAdapter; 50 import androidx.leanback.widget.ObjectAdapter; 51 import androidx.leanback.widget.PlaybackRowPresenter; 52 import androidx.leanback.widget.PlaybackSeekDataProvider; 53 import androidx.leanback.widget.PlaybackSeekUi; 54 import androidx.leanback.widget.Presenter; 55 import androidx.leanback.widget.PresenterSelector; 56 import androidx.leanback.widget.Row; 57 import androidx.leanback.widget.RowPresenter; 58 import androidx.leanback.widget.SparseArrayObjectAdapter; 59 import androidx.leanback.widget.VerticalGridView; 60 import androidx.recyclerview.widget.RecyclerView; 61 62 /** 63 * A fragment for displaying playback controls and related content. 64 * 65 * <p> 66 * A PlaybackSupportFragment renders the elements of its {@link ObjectAdapter} as a set 67 * of rows in a vertical list. The Adapter's {@link PresenterSelector} must maintain subclasses 68 * of {@link RowPresenter}. 69 * </p> 70 * <p> 71 * A playback row is a row rendered by {@link PlaybackRowPresenter}. 72 * App can call {@link #setPlaybackRow(Row)} to set playback row for the first element of adapter. 73 * App can call {@link #setPlaybackRowPresenter(PlaybackRowPresenter)} to set presenter for it. 74 * {@link #setPlaybackRow(Row)} and {@link #setPlaybackRowPresenter(PlaybackRowPresenter)} are 75 * optional, app can pass playback row and PlaybackRowPresenter in the adapter using 76 * {@link #setAdapter(ObjectAdapter)}. 77 * </p> 78 * <p> 79 * Auto hide controls upon playing: best practice is calling 80 * {@link #setControlsOverlayAutoHideEnabled(boolean)} upon play/pause. The auto hiding timer will 81 * be cancelled upon {@link #tickle()} triggered by input event. 82 * </p> 83 */ 84 public class PlaybackSupportFragment extends Fragment { 85 static final String BUNDLE_CONTROL_VISIBLE_ON_CREATEVIEW = "controlvisible_oncreateview"; 86 87 /** 88 * No background. 89 */ 90 public static final int BG_NONE = 0; 91 92 /** 93 * A dark translucent background. 94 */ 95 public static final int BG_DARK = 1; 96 PlaybackGlueHost.HostCallback mHostCallback; 97 98 PlaybackSeekUi.Client mSeekUiClient; 99 boolean mInSeek; 100 ProgressBarManager mProgressBarManager = new ProgressBarManager(); 101 102 /** 103 * Resets the focus on the button in the middle of control row. 104 * @hide 105 */ 106 @RestrictTo(RestrictTo.Scope.LIBRARY) 107 public void resetFocus() { 108 ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder) getVerticalGridView() 109 .findViewHolderForAdapterPosition(0); 110 if (vh != null && vh.getPresenter() instanceof PlaybackRowPresenter) { 111 ((PlaybackRowPresenter) vh.getPresenter()).onReappear( 112 (RowPresenter.ViewHolder) vh.getViewHolder()); 113 } 114 } 115 116 private class SetSelectionRunnable implements Runnable { 117 int mPosition; 118 boolean mSmooth = true; 119 120 @Override 121 public void run() { 122 if (mRowsSupportFragment == null) { 123 return; 124 } 125 mRowsSupportFragment.setSelectedPosition(mPosition, mSmooth); 126 } 127 } 128 129 /** 130 * A light translucent background. 131 */ 132 public static final int BG_LIGHT = 2; 133 RowsSupportFragment mRowsSupportFragment; 134 ObjectAdapter mAdapter; 135 PlaybackRowPresenter mPresenter; 136 Row mRow; 137 BaseOnItemViewSelectedListener mExternalItemSelectedListener; 138 BaseOnItemViewClickedListener mExternalItemClickedListener; 139 BaseOnItemViewClickedListener mPlaybackItemClickedListener; 140 141 private final BaseOnItemViewClickedListener mOnItemViewClickedListener = 142 new BaseOnItemViewClickedListener() { 143 @Override 144 public void onItemClicked(Presenter.ViewHolder itemViewHolder, 145 Object item, 146 RowPresenter.ViewHolder rowViewHolder, 147 Object row) { 148 if (mPlaybackItemClickedListener != null 149 && rowViewHolder instanceof PlaybackRowPresenter.ViewHolder) { 150 mPlaybackItemClickedListener.onItemClicked( 151 itemViewHolder, item, rowViewHolder, row); 152 } 153 if (mExternalItemClickedListener != null) { 154 mExternalItemClickedListener.onItemClicked( 155 itemViewHolder, item, rowViewHolder, row); 156 } 157 } 158 }; 159 160 private final BaseOnItemViewSelectedListener mOnItemViewSelectedListener = 161 new BaseOnItemViewSelectedListener() { 162 @Override 163 public void onItemSelected(Presenter.ViewHolder itemViewHolder, 164 Object item, 165 RowPresenter.ViewHolder rowViewHolder, 166 Object row) { 167 if (mExternalItemSelectedListener != null) { 168 mExternalItemSelectedListener.onItemSelected( 169 itemViewHolder, item, rowViewHolder, row); 170 } 171 } 172 }; 173 174 private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable(); 175 176 public ObjectAdapter getAdapter() { 177 return mAdapter; 178 } 179 180 /** 181 * Listener allowing the application to receive notification of fade in and/or fade out 182 * completion events. 183 * @hide 184 */ 185 @RestrictTo(RestrictTo.Scope.LIBRARY) 186 public static class OnFadeCompleteListener { 187 public void onFadeInComplete() { 188 } 189 190 public void onFadeOutComplete() { 191 } 192 } 193 194 private static final String TAG = "PlaybackSupportFragment"; 195 private static final boolean DEBUG = false; 196 private static final int ANIMATION_MULTIPLIER = 1; 197 198 private static final int START_FADE_OUT = 1; 199 200 // Fading status 201 private static final int IDLE = 0; 202 private static final int ANIMATING = 1; 203 204 int mPaddingBottom; 205 int mOtherRowsCenterToBottom; 206 View mRootView; 207 View mBackgroundView; 208 int mBackgroundType = BG_DARK; 209 int mBgDarkColor; 210 int mBgLightColor; 211 int mShowTimeMs; 212 int mMajorFadeTranslateY, mMinorFadeTranslateY; 213 int mAnimationTranslateY; 214 OnFadeCompleteListener mFadeCompleteListener; 215 View.OnKeyListener mInputEventHandler; 216 boolean mFadingEnabled = true; 217 boolean mControlVisibleBeforeOnCreateView = true; 218 boolean mControlVisible = true; 219 int mBgAlpha; 220 ValueAnimator mBgFadeInAnimator, mBgFadeOutAnimator; 221 ValueAnimator mControlRowFadeInAnimator, mControlRowFadeOutAnimator; 222 ValueAnimator mOtherRowFadeInAnimator, mOtherRowFadeOutAnimator; 223 224 private final Animator.AnimatorListener mFadeListener = 225 new Animator.AnimatorListener() { 226 @Override 227 public void onAnimationStart(Animator animation) { 228 enableVerticalGridAnimations(false); 229 } 230 231 @Override 232 public void onAnimationRepeat(Animator animation) { 233 } 234 235 @Override 236 public void onAnimationCancel(Animator animation) { 237 } 238 239 @Override 240 public void onAnimationEnd(Animator animation) { 241 if (DEBUG) Log.v(TAG, "onAnimationEnd " + mBgAlpha); 242 if (mBgAlpha > 0) { 243 enableVerticalGridAnimations(true); 244 if (mFadeCompleteListener != null) { 245 mFadeCompleteListener.onFadeInComplete(); 246 } 247 } else { 248 VerticalGridView verticalView = getVerticalGridView(); 249 // reset focus to the primary actions only if the selected row was the controls row 250 if (verticalView != null && verticalView.getSelectedPosition() == 0) { 251 ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder) 252 verticalView.findViewHolderForAdapterPosition(0); 253 if (vh != null && vh.getPresenter() instanceof PlaybackRowPresenter) { 254 ((PlaybackRowPresenter)vh.getPresenter()).onReappear( 255 (RowPresenter.ViewHolder) vh.getViewHolder()); 256 } 257 } 258 if (mFadeCompleteListener != null) { 259 mFadeCompleteListener.onFadeOutComplete(); 260 } 261 } 262 } 263 }; 264 265 public PlaybackSupportFragment() { 266 mProgressBarManager.setInitialDelay(500); 267 } 268 269 VerticalGridView getVerticalGridView() { 270 if (mRowsSupportFragment == null) { 271 return null; 272 } 273 return mRowsSupportFragment.getVerticalGridView(); 274 } 275 276 private final Handler mHandler = new Handler() { 277 @Override 278 public void handleMessage(Message message) { 279 if (message.what == START_FADE_OUT && mFadingEnabled) { 280 hideControlsOverlay(true); 281 } 282 } 283 }; 284 285 private final VerticalGridView.OnTouchInterceptListener mOnTouchInterceptListener = 286 new VerticalGridView.OnTouchInterceptListener() { 287 @Override 288 public boolean onInterceptTouchEvent(MotionEvent event) { 289 return onInterceptInputEvent(event); 290 } 291 }; 292 293 private final VerticalGridView.OnKeyInterceptListener mOnKeyInterceptListener = 294 new VerticalGridView.OnKeyInterceptListener() { 295 @Override 296 public boolean onInterceptKeyEvent(KeyEvent event) { 297 return onInterceptInputEvent(event); 298 } 299 }; 300 301 private void setBgAlpha(int alpha) { 302 mBgAlpha = alpha; 303 if (mBackgroundView != null) { 304 mBackgroundView.getBackground().setAlpha(alpha); 305 } 306 } 307 308 private void enableVerticalGridAnimations(boolean enable) { 309 if (getVerticalGridView() != null) { 310 getVerticalGridView().setAnimateChildLayout(enable); 311 } 312 } 313 314 /** 315 * Enables or disables auto hiding controls overlay after a short delay fragment is resumed. 316 * If enabled and fragment is resumed, the view will fade out after a time period. 317 * {@link #tickle()} will kill the timer, next time fragment is resumed, 318 * the timer will be started again if {@link #isControlsOverlayAutoHideEnabled()} is true. 319 */ 320 public void setControlsOverlayAutoHideEnabled(boolean enabled) { 321 if (DEBUG) Log.v(TAG, "setControlsOverlayAutoHideEnabled " + enabled); 322 if (enabled != mFadingEnabled) { 323 mFadingEnabled = enabled; 324 if (isResumed() && getView().hasFocus()) { 325 showControlsOverlay(true); 326 if (enabled) { 327 // StateGraph 7->2 5->2 328 startFadeTimer(); 329 } else { 330 // StateGraph 4->5 2->5 331 stopFadeTimer(); 332 } 333 } else { 334 // StateGraph 6->1 1->6 335 } 336 } 337 } 338 339 /** 340 * Returns true if controls will be auto hidden after a delay when fragment is resumed. 341 */ 342 public boolean isControlsOverlayAutoHideEnabled() { 343 return mFadingEnabled; 344 } 345 346 /** 347 * @deprecated Uses {@link #setControlsOverlayAutoHideEnabled(boolean)} 348 */ 349 @Deprecated 350 public void setFadingEnabled(boolean enabled) { 351 setControlsOverlayAutoHideEnabled(enabled); 352 } 353 354 /** 355 * @deprecated Uses {@link #isControlsOverlayAutoHideEnabled()} 356 */ 357 @Deprecated 358 public boolean isFadingEnabled() { 359 return isControlsOverlayAutoHideEnabled(); 360 } 361 362 /** 363 * Sets the listener to be called when fade in or out has completed. 364 * @hide 365 */ 366 @RestrictTo(RestrictTo.Scope.LIBRARY) 367 public void setFadeCompleteListener(OnFadeCompleteListener listener) { 368 mFadeCompleteListener = listener; 369 } 370 371 /** 372 * Returns the listener to be called when fade in or out has completed. 373 * @hide 374 */ 375 @RestrictTo(RestrictTo.Scope.LIBRARY) 376 public OnFadeCompleteListener getFadeCompleteListener() { 377 return mFadeCompleteListener; 378 } 379 380 /** 381 * Sets the input event handler. 382 */ 383 public final void setOnKeyInterceptListener(View.OnKeyListener handler) { 384 mInputEventHandler = handler; 385 } 386 387 /** 388 * Tickles the playback controls. Fades in the view if it was faded out. {@link #tickle()} will 389 * also kill the timer created by {@link #setControlsOverlayAutoHideEnabled(boolean)}. When 390 * next time fragment is resumed, the timer will be started again if 391 * {@link #isControlsOverlayAutoHideEnabled()} is true. In most cases app does not need call 392 * this method, tickling on input events is handled by the fragment. 393 */ 394 public void tickle() { 395 if (DEBUG) Log.v(TAG, "tickle enabled " + mFadingEnabled + " isResumed " + isResumed()); 396 //StateGraph 2->4 397 stopFadeTimer(); 398 showControlsOverlay(true); 399 } 400 401 private boolean onInterceptInputEvent(InputEvent event) { 402 final boolean controlsHidden = !mControlVisible; 403 if (DEBUG) Log.v(TAG, "onInterceptInputEvent hidden " + controlsHidden + " " + event); 404 boolean consumeEvent = false; 405 int keyCode = KeyEvent.KEYCODE_UNKNOWN; 406 int keyAction = 0; 407 408 if (event instanceof KeyEvent) { 409 keyCode = ((KeyEvent) event).getKeyCode(); 410 keyAction = ((KeyEvent) event).getAction(); 411 if (mInputEventHandler != null) { 412 consumeEvent = mInputEventHandler.onKey(getView(), keyCode, (KeyEvent) event); 413 } 414 } 415 416 switch (keyCode) { 417 case KeyEvent.KEYCODE_DPAD_CENTER: 418 case KeyEvent.KEYCODE_DPAD_DOWN: 419 case KeyEvent.KEYCODE_DPAD_UP: 420 case KeyEvent.KEYCODE_DPAD_LEFT: 421 case KeyEvent.KEYCODE_DPAD_RIGHT: 422 // Event may be consumed; regardless, if controls are hidden then these keys will 423 // bring up the controls. 424 if (controlsHidden) { 425 consumeEvent = true; 426 } 427 if (keyAction == KeyEvent.ACTION_DOWN) { 428 tickle(); 429 } 430 break; 431 case KeyEvent.KEYCODE_BACK: 432 case KeyEvent.KEYCODE_ESCAPE: 433 if (mInSeek) { 434 // when in seek, the SeekUi will handle the BACK. 435 return false; 436 } 437 // If controls are not hidden, back will be consumed to fade 438 // them out (even if the key was consumed by the handler). 439 if (!controlsHidden) { 440 consumeEvent = true; 441 442 if (((KeyEvent) event).getAction() == KeyEvent.ACTION_UP) { 443 hideControlsOverlay(true); 444 } 445 } 446 break; 447 default: 448 if (consumeEvent) { 449 if (keyAction == KeyEvent.ACTION_DOWN) { 450 tickle(); 451 } 452 } 453 } 454 return consumeEvent; 455 } 456 457 @Override 458 public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { 459 super.onViewCreated(view, savedInstanceState); 460 // controls view are initially visible, make it invisible 461 // if app has called hideControlsOverlay() before view created. 462 mControlVisible = true; 463 if (!mControlVisibleBeforeOnCreateView) { 464 showControlsOverlay(false, false); 465 mControlVisibleBeforeOnCreateView = true; 466 } 467 } 468 469 @Override 470 public void onResume() { 471 super.onResume(); 472 473 if (mControlVisible) { 474 //StateGraph: 6->5 1->2 475 if (mFadingEnabled) { 476 // StateGraph 1->2 477 startFadeTimer(); 478 } 479 } else { 480 //StateGraph: 6->7 1->3 481 } 482 getVerticalGridView().setOnTouchInterceptListener(mOnTouchInterceptListener); 483 getVerticalGridView().setOnKeyInterceptListener(mOnKeyInterceptListener); 484 if (mHostCallback != null) { 485 mHostCallback.onHostResume(); 486 } 487 } 488 489 private void stopFadeTimer() { 490 if (mHandler != null) { 491 mHandler.removeMessages(START_FADE_OUT); 492 } 493 } 494 495 private void startFadeTimer() { 496 if (mHandler != null) { 497 mHandler.removeMessages(START_FADE_OUT); 498 mHandler.sendEmptyMessageDelayed(START_FADE_OUT, mShowTimeMs); 499 } 500 } 501 502 private static ValueAnimator loadAnimator(Context context, int resId) { 503 ValueAnimator animator = (ValueAnimator) AnimatorInflater.loadAnimator(context, resId); 504 animator.setDuration(animator.getDuration() * ANIMATION_MULTIPLIER); 505 return animator; 506 } 507 508 private void loadBgAnimator() { 509 AnimatorUpdateListener listener = new AnimatorUpdateListener() { 510 @Override 511 public void onAnimationUpdate(ValueAnimator arg0) { 512 setBgAlpha((Integer) arg0.getAnimatedValue()); 513 } 514 }; 515 516 Context context = getContext(); 517 mBgFadeInAnimator = loadAnimator(context, R.animator.lb_playback_bg_fade_in); 518 mBgFadeInAnimator.addUpdateListener(listener); 519 mBgFadeInAnimator.addListener(mFadeListener); 520 521 mBgFadeOutAnimator = loadAnimator(context, R.animator.lb_playback_bg_fade_out); 522 mBgFadeOutAnimator.addUpdateListener(listener); 523 mBgFadeOutAnimator.addListener(mFadeListener); 524 } 525 526 private TimeInterpolator mLogDecelerateInterpolator = new LogDecelerateInterpolator(100, 0); 527 private TimeInterpolator mLogAccelerateInterpolator = new LogAccelerateInterpolator(100, 0); 528 529 private void loadControlRowAnimator() { 530 final AnimatorUpdateListener updateListener = new AnimatorUpdateListener() { 531 @Override 532 public void onAnimationUpdate(ValueAnimator arg0) { 533 if (getVerticalGridView() == null) { 534 return; 535 } 536 RecyclerView.ViewHolder vh = getVerticalGridView() 537 .findViewHolderForAdapterPosition(0); 538 if (vh == null) { 539 return; 540 } 541 View view = vh.itemView; 542 if (view != null) { 543 final float fraction = (Float) arg0.getAnimatedValue(); 544 if (DEBUG) Log.v(TAG, "fraction " + fraction); 545 view.setAlpha(fraction); 546 view.setTranslationY((float) mAnimationTranslateY * (1f - fraction)); 547 } 548 } 549 }; 550 551 Context context = getContext(); 552 mControlRowFadeInAnimator = loadAnimator(context, R.animator.lb_playback_controls_fade_in); 553 mControlRowFadeInAnimator.addUpdateListener(updateListener); 554 mControlRowFadeInAnimator.setInterpolator(mLogDecelerateInterpolator); 555 556 mControlRowFadeOutAnimator = loadAnimator(context, 557 R.animator.lb_playback_controls_fade_out); 558 mControlRowFadeOutAnimator.addUpdateListener(updateListener); 559 mControlRowFadeOutAnimator.setInterpolator(mLogAccelerateInterpolator); 560 } 561 562 private void loadOtherRowAnimator() { 563 final AnimatorUpdateListener updateListener = new AnimatorUpdateListener() { 564 @Override 565 public void onAnimationUpdate(ValueAnimator arg0) { 566 if (getVerticalGridView() == null) { 567 return; 568 } 569 final float fraction = (Float) arg0.getAnimatedValue(); 570 final int count = getVerticalGridView().getChildCount(); 571 for (int i = 0; i < count; i++) { 572 View view = getVerticalGridView().getChildAt(i); 573 if (getVerticalGridView().getChildAdapterPosition(view) > 0) { 574 view.setAlpha(fraction); 575 view.setTranslationY((float) mAnimationTranslateY * (1f - fraction)); 576 } 577 } 578 } 579 }; 580 581 Context context = getContext(); 582 mOtherRowFadeInAnimator = loadAnimator(context, R.animator.lb_playback_controls_fade_in); 583 mOtherRowFadeInAnimator.addUpdateListener(updateListener); 584 mOtherRowFadeInAnimator.setInterpolator(mLogDecelerateInterpolator); 585 586 mOtherRowFadeOutAnimator = loadAnimator(context, R.animator.lb_playback_controls_fade_out); 587 mOtherRowFadeOutAnimator.addUpdateListener(updateListener); 588 mOtherRowFadeOutAnimator.setInterpolator(new AccelerateInterpolator()); 589 } 590 591 /** 592 * Fades out the playback overlay immediately. 593 * @deprecated Call {@link #hideControlsOverlay(boolean)} 594 */ 595 @Deprecated 596 public void fadeOut() { 597 showControlsOverlay(false, false); 598 } 599 600 /** 601 * Show controls overlay. 602 * 603 * @param runAnimation True to run animation, false otherwise. 604 */ 605 public void showControlsOverlay(boolean runAnimation) { 606 showControlsOverlay(true, runAnimation); 607 } 608 609 /** 610 * Returns true if controls overlay is visible, false otherwise. 611 * 612 * @return True if controls overlay is visible, false otherwise. 613 * @see #showControlsOverlay(boolean) 614 * @see #hideControlsOverlay(boolean) 615 */ 616 public boolean isControlsOverlayVisible() { 617 return mControlVisible; 618 } 619 620 /** 621 * Hide controls overlay. 622 * 623 * @param runAnimation True to run animation, false otherwise. 624 */ 625 public void hideControlsOverlay(boolean runAnimation) { 626 showControlsOverlay(false, runAnimation); 627 } 628 629 /** 630 * if first animator is still running, reverse it; otherwise start second animator. 631 */ 632 static void reverseFirstOrStartSecond(ValueAnimator first, ValueAnimator second, 633 boolean runAnimation) { 634 if (first.isStarted()) { 635 first.reverse(); 636 if (!runAnimation) { 637 first.end(); 638 } 639 } else { 640 second.start(); 641 if (!runAnimation) { 642 second.end(); 643 } 644 } 645 } 646 647 /** 648 * End first or second animator if they are still running. 649 */ 650 static void endAll(ValueAnimator first, ValueAnimator second) { 651 if (first.isStarted()) { 652 first.end(); 653 } else if (second.isStarted()) { 654 second.end(); 655 } 656 } 657 658 /** 659 * Fade in or fade out rows and background. 660 * 661 * @param show True to fade in, false to fade out. 662 * @param animation True to run animation. 663 */ 664 void showControlsOverlay(boolean show, boolean animation) { 665 if (DEBUG) Log.v(TAG, "showControlsOverlay " + show); 666 if (getView() == null) { 667 mControlVisibleBeforeOnCreateView = show; 668 return; 669 } 670 // force no animation when fragment is not resumed 671 if (!isResumed()) { 672 animation = false; 673 } 674 if (show == mControlVisible) { 675 if (!animation) { 676 // End animation if needed 677 endAll(mBgFadeInAnimator, mBgFadeOutAnimator); 678 endAll(mControlRowFadeInAnimator, mControlRowFadeOutAnimator); 679 endAll(mOtherRowFadeInAnimator, mOtherRowFadeOutAnimator); 680 } 681 return; 682 } 683 // StateGraph: 7<->5 4<->3 2->3 684 mControlVisible = show; 685 if (!mControlVisible) { 686 // StateGraph 2->3 687 stopFadeTimer(); 688 } 689 690 mAnimationTranslateY = (getVerticalGridView() == null 691 || getVerticalGridView().getSelectedPosition() == 0) 692 ? mMajorFadeTranslateY : mMinorFadeTranslateY; 693 694 if (show) { 695 reverseFirstOrStartSecond(mBgFadeOutAnimator, mBgFadeInAnimator, animation); 696 reverseFirstOrStartSecond(mControlRowFadeOutAnimator, mControlRowFadeInAnimator, 697 animation); 698 reverseFirstOrStartSecond(mOtherRowFadeOutAnimator, mOtherRowFadeInAnimator, animation); 699 } else { 700 reverseFirstOrStartSecond(mBgFadeInAnimator, mBgFadeOutAnimator, animation); 701 reverseFirstOrStartSecond(mControlRowFadeInAnimator, mControlRowFadeOutAnimator, 702 animation); 703 reverseFirstOrStartSecond(mOtherRowFadeInAnimator, mOtherRowFadeOutAnimator, animation); 704 } 705 if (animation) { 706 getView().announceForAccessibility(getString(show 707 ? R.string.lb_playback_controls_shown 708 : R.string.lb_playback_controls_hidden)); 709 } 710 } 711 712 /** 713 * Sets the selected row position with smooth animation. 714 */ 715 public void setSelectedPosition(int position) { 716 setSelectedPosition(position, true); 717 } 718 719 /** 720 * Sets the selected row position. 721 */ 722 public void setSelectedPosition(int position, boolean smooth) { 723 mSetSelectionRunnable.mPosition = position; 724 mSetSelectionRunnable.mSmooth = smooth; 725 if (getView() != null && getView().getHandler() != null) { 726 getView().getHandler().post(mSetSelectionRunnable); 727 } 728 } 729 730 private void setupChildFragmentLayout() { 731 setVerticalGridViewLayout(mRowsSupportFragment.getVerticalGridView()); 732 } 733 734 void setVerticalGridViewLayout(VerticalGridView listview) { 735 if (listview == null) { 736 return; 737 } 738 739 // we set the base line of alignment to -paddingBottom 740 listview.setWindowAlignmentOffset(-mPaddingBottom); 741 listview.setWindowAlignmentOffsetPercent( 742 VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED); 743 744 // align other rows that arent the last to center of screen, since our baseline is 745 // -mPaddingBottom, we need subtract that from mOtherRowsCenterToBottom. 746 listview.setItemAlignmentOffset(mOtherRowsCenterToBottom - mPaddingBottom); 747 listview.setItemAlignmentOffsetPercent(50); 748 749 // Push last row to the bottom padding 750 // Padding affects alignment when last row is focused 751 listview.setPadding(listview.getPaddingLeft(), listview.getPaddingTop(), 752 listview.getPaddingRight(), mPaddingBottom); 753 listview.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_HIGH_EDGE); 754 } 755 756 @Override 757 public void onCreate(Bundle savedInstanceState) { 758 super.onCreate(savedInstanceState); 759 760 mOtherRowsCenterToBottom = getResources() 761 .getDimensionPixelSize(R.dimen.lb_playback_other_rows_center_to_bottom); 762 mPaddingBottom = 763 getResources().getDimensionPixelSize(R.dimen.lb_playback_controls_padding_bottom); 764 mBgDarkColor = 765 getResources().getColor(R.color.lb_playback_controls_background_dark); 766 mBgLightColor = 767 getResources().getColor(R.color.lb_playback_controls_background_light); 768 mShowTimeMs = 769 getResources().getInteger(R.integer.lb_playback_controls_show_time_ms); 770 mMajorFadeTranslateY = 771 getResources().getDimensionPixelSize(R.dimen.lb_playback_major_fade_translate_y); 772 mMinorFadeTranslateY = 773 getResources().getDimensionPixelSize(R.dimen.lb_playback_minor_fade_translate_y); 774 775 loadBgAnimator(); 776 loadControlRowAnimator(); 777 loadOtherRowAnimator(); 778 } 779 780 /** 781 * Sets the background type. 782 * 783 * @param type One of BG_LIGHT, BG_DARK, or BG_NONE. 784 */ 785 public void setBackgroundType(int type) { 786 switch (type) { 787 case BG_LIGHT: 788 case BG_DARK: 789 case BG_NONE: 790 if (type != mBackgroundType) { 791 mBackgroundType = type; 792 updateBackground(); 793 } 794 break; 795 default: 796 throw new IllegalArgumentException("Invalid background type"); 797 } 798 } 799 800 /** 801 * Returns the background type. 802 */ 803 public int getBackgroundType() { 804 return mBackgroundType; 805 } 806 807 private void updateBackground() { 808 if (mBackgroundView != null) { 809 int color = mBgDarkColor; 810 switch (mBackgroundType) { 811 case BG_DARK: 812 break; 813 case BG_LIGHT: 814 color = mBgLightColor; 815 break; 816 case BG_NONE: 817 color = Color.TRANSPARENT; 818 break; 819 } 820 mBackgroundView.setBackground(new ColorDrawable(color)); 821 setBgAlpha(mBgAlpha); 822 } 823 } 824 825 private final ItemBridgeAdapter.AdapterListener mAdapterListener = 826 new ItemBridgeAdapter.AdapterListener() { 827 @Override 828 public void onAttachedToWindow(ItemBridgeAdapter.ViewHolder vh) { 829 if (DEBUG) Log.v(TAG, "onAttachedToWindow " + vh.getViewHolder().view); 830 if (!mControlVisible) { 831 if (DEBUG) Log.v(TAG, "setting alpha to 0"); 832 vh.getViewHolder().view.setAlpha(0); 833 } 834 } 835 836 @Override 837 public void onCreate(ItemBridgeAdapter.ViewHolder vh) { 838 Presenter.ViewHolder viewHolder = vh.getViewHolder(); 839 if (viewHolder instanceof PlaybackSeekUi) { 840 ((PlaybackSeekUi) viewHolder).setPlaybackSeekUiClient(mChainedClient); 841 } 842 } 843 844 @Override 845 public void onDetachedFromWindow(ItemBridgeAdapter.ViewHolder vh) { 846 if (DEBUG) Log.v(TAG, "onDetachedFromWindow " + vh.getViewHolder().view); 847 // Reset animation state 848 vh.getViewHolder().view.setAlpha(1f); 849 vh.getViewHolder().view.setTranslationY(0); 850 vh.getViewHolder().view.setAlpha(1f); 851 } 852 853 @Override 854 public void onBind(ItemBridgeAdapter.ViewHolder vh) { 855 } 856 }; 857 858 @Override 859 public View onCreateView(LayoutInflater inflater, ViewGroup container, 860 Bundle savedInstanceState) { 861 mRootView = inflater.inflate(R.layout.lb_playback_fragment, container, false); 862 mBackgroundView = mRootView.findViewById(R.id.playback_fragment_background); 863 mRowsSupportFragment = (RowsSupportFragment) getChildFragmentManager().findFragmentById( 864 R.id.playback_controls_dock); 865 if (mRowsSupportFragment == null) { 866 mRowsSupportFragment = new RowsSupportFragment(); 867 getChildFragmentManager().beginTransaction() 868 .replace(R.id.playback_controls_dock, mRowsSupportFragment) 869 .commit(); 870 } 871 if (mAdapter == null) { 872 setAdapter(new ArrayObjectAdapter(new ClassPresenterSelector())); 873 } else { 874 mRowsSupportFragment.setAdapter(mAdapter); 875 } 876 mRowsSupportFragment.setOnItemViewSelectedListener(mOnItemViewSelectedListener); 877 mRowsSupportFragment.setOnItemViewClickedListener(mOnItemViewClickedListener); 878 879 mBgAlpha = 255; 880 updateBackground(); 881 mRowsSupportFragment.setExternalAdapterListener(mAdapterListener); 882 ProgressBarManager progressBarManager = getProgressBarManager(); 883 if (progressBarManager != null) { 884 progressBarManager.setRootView((ViewGroup) mRootView); 885 } 886 return mRootView; 887 } 888 889 /** 890 * Sets the {@link PlaybackGlueHost.HostCallback}. Implementor of this interface will 891 * take appropriate actions to take action when the hosting fragment starts/stops processing. 892 */ 893 public void setHostCallback(PlaybackGlueHost.HostCallback hostCallback) { 894 this.mHostCallback = hostCallback; 895 } 896 897 @Override 898 public void onStart() { 899 super.onStart(); 900 setupChildFragmentLayout(); 901 mRowsSupportFragment.setAdapter(mAdapter); 902 if (mHostCallback != null) { 903 mHostCallback.onHostStart(); 904 } 905 } 906 907 @Override 908 public void onStop() { 909 if (mHostCallback != null) { 910 mHostCallback.onHostStop(); 911 } 912 super.onStop(); 913 } 914 915 @Override 916 public void onPause() { 917 if (mHostCallback != null) { 918 mHostCallback.onHostPause(); 919 } 920 if (mHandler.hasMessages(START_FADE_OUT)) { 921 // StateGraph: 2->1 922 mHandler.removeMessages(START_FADE_OUT); 923 } else { 924 // StateGraph: 5->6, 7->6, 4->1, 3->1 925 } 926 super.onPause(); 927 } 928 929 /** 930 * This listener is called every time there is a selection in {@link RowsSupportFragment}. This can 931 * be used by users to take additional actions such as animations. 932 */ 933 public void setOnItemViewSelectedListener(final BaseOnItemViewSelectedListener listener) { 934 mExternalItemSelectedListener = listener; 935 } 936 937 /** 938 * This listener is called every time there is a click in {@link RowsSupportFragment}. This can 939 * be used by users to take additional actions such as animations. 940 */ 941 public void setOnItemViewClickedListener(final BaseOnItemViewClickedListener listener) { 942 mExternalItemClickedListener = listener; 943 } 944 945 /** 946 * Sets the {@link BaseOnItemViewClickedListener} that would be invoked for clicks 947 * only on {@link androidx.leanback.widget.PlaybackRowPresenter.ViewHolder}. 948 */ 949 public void setOnPlaybackItemViewClickedListener(final BaseOnItemViewClickedListener listener) { 950 mPlaybackItemClickedListener = listener; 951 } 952 953 @Override 954 public void onDestroyView() { 955 mRootView = null; 956 mBackgroundView = null; 957 super.onDestroyView(); 958 } 959 960 @Override 961 public void onDestroy() { 962 if (mHostCallback != null) { 963 mHostCallback.onHostDestroy(); 964 } 965 super.onDestroy(); 966 } 967 968 /** 969 * Sets the playback row for the playback controls. The row will be set as first element 970 * of adapter if the adapter is {@link ArrayObjectAdapter} or {@link SparseArrayObjectAdapter}. 971 * @param row The row that represents the playback. 972 */ 973 public void setPlaybackRow(Row row) { 974 this.mRow = row; 975 setupRow(); 976 setupPresenter(); 977 } 978 979 /** 980 * Sets the presenter for rendering the playback row set by {@link #setPlaybackRow(Row)}. If 981 * adapter does not set a {@link PresenterSelector}, {@link #setAdapter(ObjectAdapter)} will 982 * create a {@link ClassPresenterSelector} by default and map from the row object class to this 983 * {@link PlaybackRowPresenter}. 984 * 985 * @param presenter Presenter used to render {@link #setPlaybackRow(Row)}. 986 */ 987 public void setPlaybackRowPresenter(PlaybackRowPresenter presenter) { 988 this.mPresenter = presenter; 989 setupPresenter(); 990 setPlaybackRowPresenterAlignment(); 991 } 992 993 void setPlaybackRowPresenterAlignment() { 994 if (mAdapter != null && mAdapter.getPresenterSelector() != null) { 995 Presenter[] presenters = mAdapter.getPresenterSelector().getPresenters(); 996 if (presenters != null) { 997 for (int i = 0; i < presenters.length; i++) { 998 if (presenters[i] instanceof PlaybackRowPresenter 999 && presenters[i].getFacet(ItemAlignmentFacet.class) == null) { 1000 ItemAlignmentFacet itemAlignment = new ItemAlignmentFacet(); 1001 ItemAlignmentFacet.ItemAlignmentDef def = 1002 new ItemAlignmentFacet.ItemAlignmentDef(); 1003 def.setItemAlignmentOffset(0); 1004 def.setItemAlignmentOffsetPercent(100); 1005 itemAlignment.setAlignmentDefs(new ItemAlignmentFacet.ItemAlignmentDef[] 1006 {def}); 1007 presenters[i].setFacet(ItemAlignmentFacet.class, itemAlignment); 1008 } 1009 } 1010 } 1011 } 1012 } 1013 1014 /** 1015 * Updates the ui when the row data changes. 1016 */ 1017 public void notifyPlaybackRowChanged() { 1018 if (mAdapter == null) { 1019 return; 1020 } 1021 mAdapter.notifyItemRangeChanged(0, 1); 1022 } 1023 1024 /** 1025 * Sets the list of rows for the fragment. A default {@link ClassPresenterSelector} will be 1026 * created if {@link ObjectAdapter#getPresenterSelector()} is null. if user provides 1027 * {@link #setPlaybackRow(Row)} and {@link #setPlaybackRowPresenter(PlaybackRowPresenter)}, 1028 * the row and presenter will be set onto the adapter. 1029 * 1030 * @param adapter The adapter that contains related rows and optional playback row. 1031 */ 1032 public void setAdapter(ObjectAdapter adapter) { 1033 mAdapter = adapter; 1034 setupRow(); 1035 setupPresenter(); 1036 setPlaybackRowPresenterAlignment(); 1037 1038 if (mRowsSupportFragment != null) { 1039 mRowsSupportFragment.setAdapter(adapter); 1040 } 1041 } 1042 1043 private void setupRow() { 1044 if (mAdapter instanceof ArrayObjectAdapter && mRow != null) { 1045 ArrayObjectAdapter adapter = ((ArrayObjectAdapter) mAdapter); 1046 if (adapter.size() == 0) { 1047 adapter.add(mRow); 1048 } else { 1049 adapter.replace(0, mRow); 1050 } 1051 } else if (mAdapter instanceof SparseArrayObjectAdapter && mRow != null) { 1052 SparseArrayObjectAdapter adapter = ((SparseArrayObjectAdapter) mAdapter); 1053 adapter.set(0, mRow); 1054 } 1055 } 1056 1057 private void setupPresenter() { 1058 if (mAdapter != null && mRow != null && mPresenter != null) { 1059 PresenterSelector selector = mAdapter.getPresenterSelector(); 1060 if (selector == null) { 1061 selector = new ClassPresenterSelector(); 1062 ((ClassPresenterSelector) selector).addClassPresenter(mRow.getClass(), mPresenter); 1063 mAdapter.setPresenterSelector(selector); 1064 } else if (selector instanceof ClassPresenterSelector) { 1065 ((ClassPresenterSelector) selector).addClassPresenter(mRow.getClass(), mPresenter); 1066 } 1067 } 1068 } 1069 1070 final PlaybackSeekUi.Client mChainedClient = new PlaybackSeekUi.Client() { 1071 @Override 1072 public boolean isSeekEnabled() { 1073 return mSeekUiClient == null ? false : mSeekUiClient.isSeekEnabled(); 1074 } 1075 1076 @Override 1077 public void onSeekStarted() { 1078 if (mSeekUiClient != null) { 1079 mSeekUiClient.onSeekStarted(); 1080 } 1081 setSeekMode(true); 1082 } 1083 1084 @Override 1085 public PlaybackSeekDataProvider getPlaybackSeekDataProvider() { 1086 return mSeekUiClient == null ? null : mSeekUiClient.getPlaybackSeekDataProvider(); 1087 } 1088 1089 @Override 1090 public void onSeekPositionChanged(long pos) { 1091 if (mSeekUiClient != null) { 1092 mSeekUiClient.onSeekPositionChanged(pos); 1093 } 1094 } 1095 1096 @Override 1097 public void onSeekFinished(boolean cancelled) { 1098 if (mSeekUiClient != null) { 1099 mSeekUiClient.onSeekFinished(cancelled); 1100 } 1101 setSeekMode(false); 1102 } 1103 }; 1104 1105 /** 1106 * Interface to be implemented by UI widget to support PlaybackSeekUi. 1107 */ 1108 public void setPlaybackSeekUiClient(PlaybackSeekUi.Client client) { 1109 mSeekUiClient = client; 1110 } 1111 1112 /** 1113 * Show or hide other rows other than PlaybackRow. 1114 * @param inSeek True to make other rows visible, false to make other rows invisible. 1115 */ 1116 void setSeekMode(boolean inSeek) { 1117 if (mInSeek == inSeek) { 1118 return; 1119 } 1120 mInSeek = inSeek; 1121 getVerticalGridView().setSelectedPosition(0); 1122 if (mInSeek) { 1123 stopFadeTimer(); 1124 } 1125 // immediately fade in control row. 1126 showControlsOverlay(true); 1127 final int count = getVerticalGridView().getChildCount(); 1128 for (int i = 0; i < count; i++) { 1129 View view = getVerticalGridView().getChildAt(i); 1130 if (getVerticalGridView().getChildAdapterPosition(view) > 0) { 1131 view.setVisibility(mInSeek ? View.INVISIBLE : View.VISIBLE); 1132 } 1133 } 1134 } 1135 1136 /** 1137 * Called when size of the video changes. App may override. 1138 * @param videoWidth Intrinsic width of video 1139 * @param videoHeight Intrinsic height of video 1140 */ 1141 protected void onVideoSizeChanged(int videoWidth, int videoHeight) { 1142 } 1143 1144 /** 1145 * Called when media has start or stop buffering. App may override. The default initial state 1146 * is not buffering. 1147 * @param start True for buffering start, false otherwise. 1148 */ 1149 protected void onBufferingStateChanged(boolean start) { 1150 ProgressBarManager progressBarManager = getProgressBarManager(); 1151 if (progressBarManager != null) { 1152 if (start) { 1153 progressBarManager.show(); 1154 } else { 1155 progressBarManager.hide(); 1156 } 1157 } 1158 } 1159 1160 /** 1161 * Called when media has error. App may override. 1162 * @param errorCode Optional error code for specific implementation. 1163 * @param errorMessage Optional error message for specific implementation. 1164 */ 1165 protected void onError(int errorCode, CharSequence errorMessage) { 1166 } 1167 1168 /** 1169 * Returns the ProgressBarManager that will show or hide progress bar in 1170 * {@link #onBufferingStateChanged(boolean)}. 1171 * @return The ProgressBarManager that will show or hide progress bar in 1172 * {@link #onBufferingStateChanged(boolean)}. 1173 */ 1174 public ProgressBarManager getProgressBarManager() { 1175 return mProgressBarManager; 1176 } 1177 } 1178