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