1/* 2 * Copyright (C) 2015 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.ui; 18 19import android.animation.Animator; 20import android.animation.AnimatorListenerAdapter; 21import android.animation.ArgbEvaluator; 22import android.animation.ObjectAnimator; 23import android.animation.TimeInterpolator; 24import android.animation.TypeEvaluator; 25import android.animation.ValueAnimator; 26import android.animation.ValueAnimator.AnimatorUpdateListener; 27import android.annotation.SuppressLint; 28import android.content.Context; 29import android.content.SharedPreferences; 30import android.content.res.Resources; 31import android.graphics.Point; 32import android.hardware.display.DisplayManager; 33import android.os.Handler; 34import android.preference.PreferenceManager; 35import android.util.Log; 36import android.util.Property; 37import android.view.Display; 38import android.view.Gravity; 39import android.view.View; 40import android.view.ViewGroup; 41import android.view.ViewGroup.LayoutParams; 42import android.view.ViewGroup.MarginLayoutParams; 43import android.view.animation.AnimationUtils; 44import android.widget.FrameLayout; 45 46import com.android.tv.R; 47import com.android.tv.TvOptionsManager; 48import com.android.tv.data.DisplayMode; 49import com.android.tv.util.TvSettings; 50import com.android.tv.util.Utils; 51 52/** 53 * The TvViewUiManager is responsible for handling UI layouting and animation of main and PIP 54 * TvViews. It also control the settings regarding TvView UI such as display mode, PIP layout, 55 * and PIP size. 56 */ 57public class TvViewUiManager { 58 private static final String TAG = "TvViewManager"; 59 private static final boolean DEBUG = false; 60 61 private static final float DISPLAY_MODE_EPSILON = 0.001f; 62 private static final float DISPLAY_ASPECT_RATIO_EPSILON = 0.01f; 63 64 private final Context mContext; 65 private final Resources mResources; 66 private final FrameLayout mContentView; 67 private final TunableTvView mTvView; 68 private final TunableTvView mPipView; 69 private final TvOptionsManager mTvOptionsManager; 70 private final int mTvViewPapWidth; 71 private final int mTvViewShrunkenStartMargin; 72 private final int mTvViewShrunkenEndMargin; 73 private final int mTvViewPapStartMargin; 74 private final int mTvViewPapEndMargin; 75 private int mWindowWidth; 76 private int mWindowHeight; 77 private final int mPipViewHorizontalMargin; 78 private final int mPipViewTopMargin; 79 private final int mPipViewBottomMargin; 80 private final SharedPreferences mSharedPreferences; 81 private final TimeInterpolator mLinearOutSlowIn; 82 private final TimeInterpolator mFastOutLinearIn; 83 private final Handler mHandler = new Handler(); 84 private int mDisplayMode; 85 // Used to restore the previous state from ShrunkenTvView state. 86 private int mTvViewStartMarginBeforeShrunken; 87 private int mTvViewEndMarginBeforeShrunken; 88 private int mDisplayModeBeforeShrunken; 89 private boolean mIsUnderShrunkenTvView; 90 private int mTvViewStartMargin; 91 private int mTvViewEndMargin; 92 private int mPipLayout; 93 private int mPipSize; 94 private boolean mPipStarted; 95 private ObjectAnimator mTvViewAnimator; 96 private FrameLayout.LayoutParams mTvViewLayoutParams; 97 // TV view's position when the display mode is FULL. It is used to compute PIP location relative 98 // to TV view's position. 99 private MarginLayoutParams mTvViewFrame; 100 private MarginLayoutParams mLastAnimatedTvViewFrame; 101 private MarginLayoutParams mOldTvViewFrame; 102 private ObjectAnimator mBackgroundAnimator; 103 private int mBackgroundColor; 104 private int mAppliedDisplayedMode = DisplayMode.MODE_NOT_DEFINED; 105 private int mAppliedTvViewStartMargin; 106 private int mAppliedTvViewEndMargin; 107 private float mAppliedVideoDisplayAspectRatio; 108 109 public TvViewUiManager(Context context, TunableTvView tvView, TunableTvView pipView, 110 FrameLayout contentView, TvOptionsManager tvOptionManager) { 111 mContext = context; 112 mResources = mContext.getResources(); 113 mTvView = tvView; 114 mPipView = pipView; 115 mContentView = contentView; 116 mTvOptionsManager = tvOptionManager; 117 118 DisplayManager displayManager = (DisplayManager) mContext 119 .getSystemService(Context.DISPLAY_SERVICE); 120 Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY); 121 Point size = new Point(); 122 display.getSize(size); 123 mWindowWidth = size.x; 124 mWindowHeight = size.y; 125 126 // Have an assumption that PIP and TvView Shrinking happens only in full screen. 127 mTvViewShrunkenStartMargin = mResources 128 .getDimensionPixelOffset(R.dimen.shrunken_tvview_margin_start); 129 mTvViewShrunkenEndMargin = 130 mResources.getDimensionPixelOffset(R.dimen.shrunken_tvview_margin_end) 131 + mResources.getDimensionPixelSize(R.dimen.side_panel_width); 132 int papMarginHorizontal = mResources 133 .getDimensionPixelOffset(R.dimen.papview_margin_horizontal); 134 int papSpacing = mResources.getDimensionPixelOffset(R.dimen.papview_spacing); 135 mTvViewPapWidth = (mWindowWidth - papSpacing) / 2 - papMarginHorizontal; 136 mTvViewPapStartMargin = papMarginHorizontal + mTvViewPapWidth + papSpacing; 137 mTvViewPapEndMargin = papMarginHorizontal; 138 mTvViewFrame = createMarginLayoutParams(0, 0, 0, 0); 139 140 mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext); 141 142 mLinearOutSlowIn = AnimationUtils 143 .loadInterpolator(mContext, android.R.interpolator.linear_out_slow_in); 144 mFastOutLinearIn = AnimationUtils 145 .loadInterpolator(mContext, android.R.interpolator.fast_out_linear_in); 146 147 mPipViewHorizontalMargin = mResources 148 .getDimensionPixelOffset(R.dimen.pipview_margin_horizontal); 149 mPipViewTopMargin = mResources.getDimensionPixelOffset(R.dimen.pipview_margin_top); 150 mPipViewBottomMargin = mResources.getDimensionPixelOffset(R.dimen.pipview_margin_bottom); 151 mContentView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { 152 @Override 153 public void onLayoutChange(View v, int left, int top, int right, int bottom, 154 int oldLeft, int oldTop, int oldRight, int oldBottom) { 155 int windowWidth = right - left; 156 int windowHeight = bottom - top; 157 if (windowWidth > 0 && windowHeight > 0) { 158 if (mWindowWidth != windowWidth || mWindowHeight != windowHeight) { 159 mWindowWidth = windowWidth; 160 mWindowHeight = windowHeight; 161 applyDisplayMode(mTvView.getVideoDisplayAspectRatio(), false, true); 162 } 163 } 164 } 165 }); 166 } 167 168 /** 169 * Initializes animator in advance of using the animator to improve animation performance. 170 * For fast first tune, it is not expected to be called in Activity.onCreate, but called 171 * a few seconds later after onCreate. 172 */ 173 public void initAnimatorIfNeeded() { 174 initTvAnimatorIfNeeded(); 175 initBackgroundAnimatorIfNeeded(); 176 } 177 178 /** 179 * It is called when shrunken TvView is desired, such as EditChannelFragment and 180 * ChannelsLockedFragment. 181 */ 182 public void startShrunkenTvView() { 183 mIsUnderShrunkenTvView = true; 184 185 mTvViewStartMarginBeforeShrunken = mTvViewStartMargin; 186 mTvViewEndMarginBeforeShrunken = mTvViewEndMargin; 187 if (mPipStarted && getPipLayout() == TvSettings.PIP_LAYOUT_SIDE_BY_SIDE) { 188 float sidePanelWidth = mResources.getDimensionPixelOffset(R.dimen.side_panel_width); 189 float factor = 1.0f - sidePanelWidth / mWindowWidth; 190 int startMargin = (int) (mTvViewPapStartMargin * factor); 191 int endMargin = (int) (mTvViewPapEndMargin * factor + sidePanelWidth); 192 setTvViewMargin(startMargin, endMargin); 193 } else { 194 setTvViewMargin(mTvViewShrunkenStartMargin, mTvViewShrunkenEndMargin); 195 } 196 mDisplayModeBeforeShrunken = setDisplayMode(DisplayMode.MODE_NORMAL, false, true); 197 } 198 199 /** 200 * It is called when shrunken TvView is no longer desired, such as EditChannelFragment and 201 * ChannelsLockedFragment. 202 */ 203 public void endShrunkenTvView() { 204 mIsUnderShrunkenTvView = false; 205 setTvViewMargin(mTvViewStartMarginBeforeShrunken, mTvViewEndMarginBeforeShrunken); 206 setDisplayMode(mDisplayModeBeforeShrunken, false, true); 207 } 208 209 /** 210 * Returns true, if TvView is shrunken. 211 */ 212 public boolean isUnderShrunkenTvView() { 213 return mIsUnderShrunkenTvView; 214 } 215 216 /** 217 * Returns true, if {@code displayMode} is available now. If screen ratio is matched to 218 * video ratio, other display modes than {@link DisplayMode#MODE_NORMAL} are not available. 219 */ 220 public boolean isDisplayModeAvailable(int displayMode) { 221 if (displayMode == DisplayMode.MODE_NORMAL) { 222 return true; 223 } 224 225 int viewWidth = mContentView.getWidth(); 226 int viewHeight = mContentView.getHeight(); 227 228 float videoDisplayAspectRatio = mTvView.getVideoDisplayAspectRatio(); 229 if (viewWidth <= 0 || viewHeight <= 0 || videoDisplayAspectRatio <= 0f) { 230 Log.w(TAG, "Video size is currently unavailable"); 231 if (DEBUG) { 232 Log.d(TAG, "isDisplayModeAvailable: " 233 + "viewWidth=" + viewWidth 234 + ", viewHeight=" + viewHeight 235 + ", videoDisplayAspectRatio=" + videoDisplayAspectRatio 236 ); 237 } 238 return false; 239 } 240 241 float viewRatio = viewWidth / (float) viewHeight; 242 return Math.abs(viewRatio - videoDisplayAspectRatio) >= DISPLAY_MODE_EPSILON; 243 } 244 245 /** 246 * Returns a constant defined in DisplayMode. 247 */ 248 public int getDisplayMode() { 249 if (isDisplayModeAvailable(mDisplayMode)) { 250 return mDisplayMode; 251 } 252 return DisplayMode.MODE_NORMAL; 253 } 254 255 /** 256 * Sets the display mode to the given value. 257 * 258 * @return the previous display mode. 259 */ 260 public int setDisplayMode(int displayMode, boolean storeInPreference, boolean animate) { 261 int prev = mDisplayMode; 262 mDisplayMode = displayMode; 263 if (storeInPreference) { 264 mSharedPreferences.edit().putInt(TvSettings.PREF_DISPLAY_MODE, displayMode).apply(); 265 } 266 applyDisplayMode(mTvView.getVideoDisplayAspectRatio(), animate, false); 267 return prev; 268 } 269 270 /** 271 * Restores the display mode to the display mode stored in preference. 272 */ 273 public void restoreDisplayMode(boolean animate) { 274 int displayMode = mSharedPreferences 275 .getInt(TvSettings.PREF_DISPLAY_MODE, DisplayMode.MODE_NORMAL); 276 setDisplayMode(displayMode, false, animate); 277 } 278 279 /** 280 * Updates TvView. It is called when video resolution is updated. 281 */ 282 public void updateTvView() { 283 applyDisplayMode(mTvView.getVideoDisplayAspectRatio(), false, false); 284 if (mTvView.isVideoAvailable() && mTvView.isFadedOut()) { 285 mTvView.fadeIn(mResources.getInteger(R.integer.tvview_fade_in_duration), 286 mFastOutLinearIn, null); 287 } 288 } 289 290 /** 291 * Fades in TvView. 292 */ 293 public void fadeInTvView() { 294 if (mTvView.isFadedOut()) { 295 mTvView.fadeIn(mResources.getInteger(R.integer.tvview_fade_in_duration), 296 mFastOutLinearIn, null); 297 } 298 } 299 300 /** 301 * Fades out TvView. 302 */ 303 public void fadeOutTvView(Runnable postAction) { 304 if (!mTvView.isFadedOut()) { 305 mTvView.fadeOut(mResources.getInteger(R.integer.tvview_fade_out_duration), 306 mLinearOutSlowIn, postAction); 307 } 308 } 309 310 /** 311 * Returns the current PIP layout. The layout should be one of 312 * {@link TvSettings#PIP_LAYOUT_BOTTOM_RIGHT}, {@link TvSettings#PIP_LAYOUT_TOP_RIGHT}, 313 * {@link TvSettings#PIP_LAYOUT_TOP_LEFT}, {@link TvSettings#PIP_LAYOUT_BOTTOM_LEFT} and 314 * {@link TvSettings#PIP_LAYOUT_SIDE_BY_SIDE}. 315 */ 316 public int getPipLayout() { 317 return mPipLayout; 318 } 319 320 /** 321 * Sets the PIP layout. The layout should be one of 322 * {@link TvSettings#PIP_LAYOUT_BOTTOM_RIGHT}, {@link TvSettings#PIP_LAYOUT_TOP_RIGHT}, 323 * {@link TvSettings#PIP_LAYOUT_TOP_LEFT}, {@link TvSettings#PIP_LAYOUT_BOTTOM_LEFT} and 324 * {@link TvSettings#PIP_LAYOUT_SIDE_BY_SIDE}. 325 * 326 * @param storeInPreference if true, the stored value will be restored by 327 * {@link #restorePipLayout()}. 328 */ 329 public void setPipLayout(int pipLayout, boolean storeInPreference) { 330 mPipLayout = pipLayout; 331 if (storeInPreference) { 332 TvSettings.setPipLayout(mContext, pipLayout); 333 } 334 updatePipView(mTvViewFrame); 335 if (mPipLayout == TvSettings.PIP_LAYOUT_SIDE_BY_SIDE) { 336 setTvViewMargin(mTvViewPapStartMargin, mTvViewPapEndMargin); 337 setDisplayMode(DisplayMode.MODE_NORMAL, false, false); 338 } else { 339 setTvViewMargin(0, 0); 340 restoreDisplayMode(false); 341 } 342 mTvOptionsManager.onPipLayoutChanged(pipLayout); 343 } 344 345 /** 346 * Restores the PIP layout which {@link #setPipLayout} lastly stores. 347 */ 348 public void restorePipLayout() { 349 setPipLayout(TvSettings.getPipLayout(mContext), false); 350 } 351 352 /** 353 * Called when PIP is started. 354 */ 355 public void onPipStart() { 356 mPipStarted = true; 357 updatePipView(); 358 mPipView.setVisibility(View.VISIBLE); 359 } 360 361 /** 362 * Called when PIP is stopped. 363 */ 364 public void onPipStop() { 365 setTvViewMargin(0, 0); 366 mPipView.setVisibility(View.GONE); 367 mPipStarted = false; 368 } 369 370 /** 371 * Called when PIP is resumed. 372 */ 373 public void showPipForResume() { 374 mPipView.setVisibility(View.VISIBLE); 375 } 376 377 /** 378 * Called when PIP is paused. 379 */ 380 public void hidePipForPause() { 381 if (mPipLayout != TvSettings.PIP_LAYOUT_SIDE_BY_SIDE) { 382 mPipView.setVisibility(View.GONE); 383 } 384 } 385 386 /** 387 * Updates PIP view. It is usually called, when video resolution in PIP is updated. 388 */ 389 public void updatePipView() { 390 updatePipView(mTvViewFrame); 391 } 392 393 /** 394 * Returns the size of the PIP view. 395 */ 396 public int getPipSize() { 397 return mPipSize; 398 } 399 400 /** 401 * Sets PIP size and applies it immediately. 402 * 403 * @param pipSize PIP size. The value should be one of {@link TvSettings#PIP_SIZE_BIG} 404 * and {@link TvSettings#PIP_SIZE_SMALL}. 405 * @param storeInPreference if true, the stored value will be restored by 406 * {@link #restorePipSize()}. 407 */ 408 public void setPipSize(int pipSize, boolean storeInPreference) { 409 mPipSize = pipSize; 410 if (storeInPreference) { 411 TvSettings.setPipSize(mContext, pipSize); 412 } 413 updatePipView(mTvViewFrame); 414 mTvOptionsManager.onPipSizeChanged(pipSize); 415 } 416 417 /** 418 * Restores the PIP size which {@link #setPipSize} lastly stores. 419 */ 420 public void restorePipSize() { 421 setPipSize(TvSettings.getPipSize(mContext), false); 422 } 423 424 /** 425 * This margins will be applied when applyDisplayMode is called. 426 */ 427 private void setTvViewMargin(int tvViewStartMargin, int tvViewEndMargin) { 428 mTvViewStartMargin = tvViewStartMargin; 429 mTvViewEndMargin = tvViewEndMargin; 430 } 431 432 private boolean isTvViewFullScreen() { 433 return mTvViewStartMargin == 0 && mTvViewEndMargin == 0; 434 } 435 436 private void setBackgroundColor(int color, FrameLayout.LayoutParams targetLayoutParams, 437 boolean animate) { 438 if (animate) { 439 initBackgroundAnimatorIfNeeded(); 440 if (mBackgroundAnimator.isStarted()) { 441 // Cancel the current animation and start new one. 442 mBackgroundAnimator.cancel(); 443 } 444 445 int decorViewWidth = mContentView.getWidth(); 446 int decorViewHeight = mContentView.getHeight(); 447 boolean hasPillarBox = mTvView.getWidth() != decorViewWidth 448 || mTvView.getHeight() != decorViewHeight; 449 boolean willHavePillarBox = ((targetLayoutParams.width != LayoutParams.MATCH_PARENT) 450 && targetLayoutParams.width != decorViewWidth) || ( 451 (targetLayoutParams.height != LayoutParams.MATCH_PARENT) 452 && targetLayoutParams.height != decorViewHeight); 453 454 if (!isTvViewFullScreen() && !hasPillarBox) { 455 // If there is no pillar box, no animation is needed. 456 mContentView.setBackgroundColor(color); 457 } else if (!isTvViewFullScreen() || willHavePillarBox) { 458 mBackgroundAnimator.setIntValues(mBackgroundColor, color); 459 mBackgroundAnimator.setEvaluator(new ArgbEvaluator()); 460 mBackgroundAnimator.setInterpolator(mFastOutLinearIn); 461 mBackgroundAnimator.start(); 462 } 463 // In the 'else' case (TV activity is getting out of the shrunken tv view mode and will 464 // have a pillar box), we keep the background color and don't show the animation. 465 } else { 466 mContentView.setBackgroundColor(color); 467 } 468 mBackgroundColor = color; 469 } 470 471 private void setTvViewPosition(final FrameLayout.LayoutParams layoutParams, 472 MarginLayoutParams tvViewFrame, boolean animate) { 473 if (DEBUG) { 474 Log.d(TAG, "setTvViewPosition: w=" + layoutParams.width + " h=" + layoutParams.height 475 + " s=" + layoutParams.getMarginStart() + " t=" + layoutParams.topMargin 476 + " e=" + layoutParams.getMarginEnd() + " b=" + layoutParams.bottomMargin 477 + " animate=" + animate); 478 } 479 MarginLayoutParams oldTvViewFrame = mTvViewFrame; 480 mTvViewLayoutParams = layoutParams; 481 mTvViewFrame = tvViewFrame; 482 if (animate) { 483 initTvAnimatorIfNeeded(); 484 if (mTvViewAnimator.isStarted()) { 485 // Cancel the current animation and start new one. 486 mTvViewAnimator.cancel(); 487 mOldTvViewFrame = mLastAnimatedTvViewFrame; 488 } else { 489 mOldTvViewFrame = oldTvViewFrame; 490 } 491 mTvViewAnimator.setObjectValues(mTvView.getLayoutParams(), layoutParams); 492 mTvViewAnimator.setEvaluator(new TypeEvaluator<FrameLayout.LayoutParams>() { 493 FrameLayout.LayoutParams lp; 494 @Override 495 public FrameLayout.LayoutParams evaluate(float fraction, 496 FrameLayout.LayoutParams startValue, FrameLayout.LayoutParams endValue) { 497 if (lp == null) { 498 lp = new FrameLayout.LayoutParams(0, 0); 499 lp.gravity = startValue.gravity; 500 } 501 interpolateMarginsRelative(lp, startValue, endValue, fraction); 502 return lp; 503 } 504 }); 505 mTvViewAnimator 506 .setInterpolator(isTvViewFullScreen() ? mFastOutLinearIn : mLinearOutSlowIn); 507 mTvViewAnimator.start(); 508 } else { 509 if (mTvViewAnimator != null && mTvViewAnimator.isStarted()) { 510 // Continue the current animation. 511 // layoutParams will be applied when animation ends. 512 return; 513 } 514 // This block is also called when animation ends. 515 if (isTvViewFullScreen()) { 516 // When this layout is for full screen, fix the surface size after layout to make 517 // resize animation smooth. 518 mTvView.post(new Runnable() { 519 @Override 520 public void run() { 521 if (DEBUG) { 522 Log.d(TAG, "setFixedSize: w=" + layoutParams.width + " h=" 523 + layoutParams.height); 524 } 525 mTvView.setLayoutParams(layoutParams); 526 mTvView.setFixedSurfaceSize(layoutParams.width, layoutParams.height); 527 } 528 }); 529 } else { 530 mTvView.setLayoutParams(layoutParams); 531 } 532 updatePipView(mTvViewFrame); 533 } 534 } 535 536 /** 537 * The redlines assume that the ratio of the TV screen is 16:9. If the radio is not 16:9, the 538 * layout of PAP can be broken. 539 */ 540 @SuppressLint("RtlHardcoded") 541 private void updatePipView(MarginLayoutParams tvViewFrame) { 542 if (!mPipStarted) { 543 return; 544 } 545 int width; 546 int height; 547 int startMargin; 548 int endMargin; 549 int topMargin; 550 int bottomMargin; 551 int gravity; 552 553 if (mPipLayout == TvSettings.PIP_LAYOUT_SIDE_BY_SIDE) { 554 gravity = Gravity.CENTER_VERTICAL | Gravity.START; 555 height = tvViewFrame.height; 556 float videoDisplayAspectRatio = mPipView.getVideoDisplayAspectRatio(); 557 if (videoDisplayAspectRatio <= 0f) { 558 width = tvViewFrame.width; 559 } else { 560 width = (int) (height * videoDisplayAspectRatio); 561 if (width > tvViewFrame.width) { 562 width = tvViewFrame.width; 563 } 564 } 565 startMargin = mResources.getDimensionPixelOffset(R.dimen.papview_margin_horizontal) 566 * tvViewFrame.width / mTvViewPapWidth + (tvViewFrame.width - width) / 2; 567 endMargin = 0; 568 topMargin = 0; 569 bottomMargin = 0; 570 } else { 571 int tvViewWidth = tvViewFrame.width; 572 int tvViewHeight = tvViewFrame.height; 573 int tvStartMargin = tvViewFrame.getMarginStart(); 574 int tvEndMargin = tvViewFrame.getMarginEnd(); 575 int tvTopMargin = tvViewFrame.topMargin; 576 int tvBottomMargin = tvViewFrame.bottomMargin; 577 float horizontalScaleFactor = (float) tvViewWidth / mWindowWidth; 578 float verticalScaleFactor = (float) tvViewHeight / mWindowHeight; 579 580 int maxWidth; 581 if (mPipSize == TvSettings.PIP_SIZE_SMALL) { 582 maxWidth = (int) (mResources.getDimensionPixelSize(R.dimen.pipview_small_size_width) 583 * horizontalScaleFactor); 584 height = (int) (mResources.getDimensionPixelSize(R.dimen.pipview_small_size_height) 585 * verticalScaleFactor); 586 } else if (mPipSize == TvSettings.PIP_SIZE_BIG) { 587 maxWidth = (int) (mResources.getDimensionPixelSize(R.dimen.pipview_large_size_width) 588 * horizontalScaleFactor); 589 height = (int) (mResources.getDimensionPixelSize(R.dimen.pipview_large_size_height) 590 * verticalScaleFactor); 591 } else { 592 throw new IllegalArgumentException("Invalid PIP size: " + mPipSize); 593 } 594 float videoDisplayAspectRatio = mPipView.getVideoDisplayAspectRatio(); 595 if (videoDisplayAspectRatio <= 0f) { 596 width = maxWidth; 597 } else { 598 width = (int) (height * videoDisplayAspectRatio); 599 if (width > maxWidth) { 600 width = maxWidth; 601 } 602 } 603 604 startMargin = tvStartMargin + (int) (mPipViewHorizontalMargin * horizontalScaleFactor); 605 endMargin = tvEndMargin + (int) (mPipViewHorizontalMargin * horizontalScaleFactor); 606 topMargin = tvTopMargin + (int) (mPipViewTopMargin * verticalScaleFactor); 607 bottomMargin = tvBottomMargin + (int) (mPipViewBottomMargin * verticalScaleFactor); 608 609 switch (mPipLayout) { 610 case TvSettings.PIP_LAYOUT_TOP_LEFT: 611 gravity = Gravity.TOP | Gravity.LEFT; 612 break; 613 case TvSettings.PIP_LAYOUT_TOP_RIGHT: 614 gravity = Gravity.TOP | Gravity.RIGHT; 615 break; 616 case TvSettings.PIP_LAYOUT_BOTTOM_LEFT: 617 gravity = Gravity.BOTTOM | Gravity.LEFT; 618 break; 619 case TvSettings.PIP_LAYOUT_BOTTOM_RIGHT: 620 gravity = Gravity.BOTTOM | Gravity.RIGHT; 621 break; 622 default: 623 throw new IllegalArgumentException("Invalid PIP location: " + mPipLayout); 624 } 625 } 626 627 FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mPipView.getLayoutParams(); 628 if (lp.width != width || lp.height != height || lp.getMarginStart() != startMargin 629 || lp.getMarginEnd() != endMargin || lp.topMargin != topMargin 630 || lp.bottomMargin != bottomMargin || lp.gravity != gravity) { 631 lp.width = width; 632 lp.height = height; 633 lp.setMarginStart(startMargin); 634 lp.setMarginEnd(endMargin); 635 lp.topMargin = topMargin; 636 lp.bottomMargin = bottomMargin; 637 lp.gravity = gravity; 638 mPipView.setLayoutParams(lp); 639 } 640 } 641 642 private void initTvAnimatorIfNeeded() { 643 if (mTvViewAnimator != null) { 644 return; 645 } 646 647 // TvViewAnimator animates TvView by repeatedly re-layouting TvView. 648 // TvView includes a SurfaceView on which scale/translation effects do not work. Normally, 649 // SurfaceView can be animated by changing left/top/right/bottom directly using 650 // ObjectAnimator, although it would require calling getChildAt(0) against TvView (which is 651 // supposed to be opaque). More importantly, this method does not work in case of TvView, 652 // because TvView may request layout itself during animation and layout SurfaceView with 653 // its own parameters when TvInputService requests to do so. 654 mTvViewAnimator = new ObjectAnimator(); 655 mTvViewAnimator.setTarget(mTvView); 656 mTvViewAnimator.setProperty( 657 Property.of(FrameLayout.class, ViewGroup.LayoutParams.class, "layoutParams")); 658 mTvViewAnimator.setDuration(mResources.getInteger(R.integer.tvview_anim_duration)); 659 mTvViewAnimator.addListener(new AnimatorListenerAdapter() { 660 private boolean mCanceled = false; 661 662 @Override 663 public void onAnimationCancel(Animator animation) { 664 mCanceled = true; 665 } 666 667 @Override 668 public void onAnimationEnd(Animator animation) { 669 if (mCanceled) { 670 mCanceled = false; 671 return; 672 } 673 mHandler.post(new Runnable() { 674 @Override 675 public void run() { 676 setTvViewPosition(mTvViewLayoutParams, mTvViewFrame, false); 677 } 678 }); 679 } 680 }); 681 mTvViewAnimator.addUpdateListener(new AnimatorUpdateListener() { 682 @Override 683 public void onAnimationUpdate(ValueAnimator animator) { 684 float fraction = animator.getAnimatedFraction(); 685 mLastAnimatedTvViewFrame = new MarginLayoutParams(0, 0); 686 interpolateMarginsRelative(mLastAnimatedTvViewFrame, 687 mOldTvViewFrame, mTvViewFrame, fraction); 688 updatePipView(mLastAnimatedTvViewFrame); 689 } 690 }); 691 } 692 693 private void initBackgroundAnimatorIfNeeded() { 694 if (mBackgroundAnimator != null) { 695 return; 696 } 697 698 mBackgroundAnimator = new ObjectAnimator(); 699 mBackgroundAnimator.setTarget(mContentView); 700 mBackgroundAnimator.setPropertyName("backgroundColor"); 701 mBackgroundAnimator 702 .setDuration(mResources.getInteger(R.integer.tvactivity_background_anim_duration)); 703 mBackgroundAnimator.addListener(new AnimatorListenerAdapter() { 704 @Override 705 public void onAnimationEnd(Animator animation) { 706 mHandler.post(new Runnable() { 707 @Override 708 public void run() { 709 mContentView.setBackgroundColor(mBackgroundColor); 710 } 711 }); 712 } 713 }); 714 } 715 716 private void applyDisplayMode(float videoDisplayAspectRatio, boolean animate, 717 boolean forceUpdate) { 718 if (mAppliedDisplayedMode == mDisplayMode 719 && mAppliedTvViewStartMargin == mTvViewStartMargin 720 && mAppliedTvViewEndMargin == mTvViewEndMargin 721 && Math.abs(mAppliedVideoDisplayAspectRatio - videoDisplayAspectRatio) < 722 DISPLAY_ASPECT_RATIO_EPSILON) { 723 if (!forceUpdate) { 724 return; 725 } 726 } else { 727 mAppliedDisplayedMode = mDisplayMode; 728 mAppliedTvViewStartMargin = mTvViewStartMargin; 729 mAppliedTvViewEndMargin = mTvViewEndMargin; 730 mAppliedVideoDisplayAspectRatio = videoDisplayAspectRatio; 731 } 732 int availableAreaWidth = mWindowWidth - mTvViewStartMargin - mTvViewEndMargin; 733 int availableAreaHeight = availableAreaWidth * mWindowHeight / mWindowWidth; 734 FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(0, 0, 735 ((FrameLayout.LayoutParams) mTvView.getLayoutParams()).gravity); 736 int displayMode = mDisplayMode; 737 double availableAreaRatio = 0; 738 double videoRatio = 0; 739 if (availableAreaWidth <= 0 || availableAreaHeight <= 0) { 740 displayMode = DisplayMode.MODE_FULL; 741 Log.w(TAG, "Some resolution info is missing during applyDisplayMode. (" 742 + "availableAreaWidth=" + availableAreaWidth + ", availableAreaHeight=" 743 + availableAreaHeight + ")"); 744 } else { 745 availableAreaRatio = (double) availableAreaWidth / availableAreaHeight; 746 if (videoDisplayAspectRatio <= 0f) { 747 videoRatio = (double) mWindowWidth / mWindowHeight; 748 } else { 749 videoRatio = videoDisplayAspectRatio; 750 } 751 } 752 753 int tvViewFrameTop = (mWindowHeight - availableAreaHeight) / 2; 754 MarginLayoutParams tvViewFrame = createMarginLayoutParams( 755 mTvViewStartMargin, mTvViewEndMargin, tvViewFrameTop, tvViewFrameTop); 756 layoutParams.width = availableAreaWidth; 757 layoutParams.height = availableAreaHeight; 758 switch (displayMode) { 759 case DisplayMode.MODE_FULL: 760 layoutParams.width = availableAreaWidth; 761 layoutParams.height = availableAreaHeight; 762 break; 763 case DisplayMode.MODE_ZOOM: 764 if (videoRatio < availableAreaRatio) { 765 // Y axis will be clipped. 766 layoutParams.width = availableAreaWidth; 767 layoutParams.height = (int) (availableAreaWidth / videoRatio); 768 } else { 769 // X axis will be clipped. 770 layoutParams.width = (int) (availableAreaHeight * videoRatio); 771 layoutParams.height = availableAreaHeight; 772 } 773 break; 774 case DisplayMode.MODE_NORMAL: 775 if (videoRatio < availableAreaRatio) { 776 // X axis has black area. 777 layoutParams.width = (int) (availableAreaHeight * videoRatio); 778 layoutParams.height = availableAreaHeight; 779 } else { 780 // Y axis has black area. 781 layoutParams.width = availableAreaWidth; 782 layoutParams.height = (int) (availableAreaWidth / videoRatio); 783 } 784 break; 785 } 786 787 // FrameLayout has an issue with centering when left and right margins differ. 788 // So stick to Gravity.START | Gravity.CENTER_VERTICAL. 789 int marginStart = mTvViewStartMargin + (availableAreaWidth - layoutParams.width) / 2; 790 layoutParams.setMarginStart(marginStart); 791 // Set marginEnd as well because setTvViewPosition uses both start/end margin. 792 layoutParams.setMarginEnd(mWindowWidth - layoutParams.width - marginStart); 793 794 setBackgroundColor(Utils.getColor(mResources, isTvViewFullScreen() 795 ? R.color.tvactivity_background : R.color.tvactivity_background_on_shrunken_tvview), 796 layoutParams, animate); 797 setTvViewPosition(layoutParams, tvViewFrame, animate); 798 799 // Update the current display mode. 800 mTvOptionsManager.onDisplayModeChanged(displayMode); 801 } 802 803 private static int interpolate(int start, int end, float fraction) { 804 return (int) (start + (end - start) * fraction); 805 } 806 807 private static void interpolateMarginsRelative(MarginLayoutParams out, 808 MarginLayoutParams startValue, MarginLayoutParams endValue, float fraction) { 809 out.topMargin = interpolate(startValue.topMargin, endValue.topMargin, fraction); 810 out.bottomMargin = interpolate(startValue.bottomMargin, endValue.bottomMargin, fraction); 811 out.setMarginStart(interpolate(startValue.getMarginStart(), endValue.getMarginStart(), 812 fraction)); 813 out.setMarginEnd(interpolate(startValue.getMarginEnd(), endValue.getMarginEnd(), fraction)); 814 out.width = interpolate(startValue.width, endValue.width, fraction); 815 out.height = interpolate(startValue.height, endValue.height, fraction); 816 } 817 818 private MarginLayoutParams createMarginLayoutParams( 819 int startMargin, int endMargin, int topMargin, int bottomMargin) { 820 MarginLayoutParams lp = new MarginLayoutParams(0, 0); 821 lp.setMarginStart(startMargin); 822 lp.setMarginEnd(endMargin); 823 lp.topMargin = topMargin; 824 lp.bottomMargin = bottomMargin; 825 lp.width = mWindowWidth - startMargin - endMargin; 826 lp.height = mWindowHeight - topMargin - bottomMargin; 827 return lp; 828 } 829} 830