/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tv.ui; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ArgbEvaluator; import android.animation.ObjectAnimator; import android.animation.TimeInterpolator; import android.animation.TypeEvaluator; import android.animation.ValueAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; import android.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; import android.content.res.Resources; import android.graphics.Point; import android.hardware.display.DisplayManager; import android.os.Handler; import android.preference.PreferenceManager; import android.util.Log; import android.util.Property; import android.view.Display; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.view.ViewGroup.MarginLayoutParams; import android.view.animation.AnimationUtils; import android.widget.FrameLayout; import com.android.tv.R; import com.android.tv.TvOptionsManager; import com.android.tv.data.DisplayMode; import com.android.tv.util.TvSettings; import com.android.tv.util.Utils; /** * The TvViewUiManager is responsible for handling UI layouting and animation of main and PIP * TvViews. It also control the settings regarding TvView UI such as display mode, PIP layout, * and PIP size. */ public class TvViewUiManager { private static final String TAG = "TvViewManager"; private static final boolean DEBUG = false; private static final float DISPLAY_MODE_EPSILON = 0.001f; private static final float DISPLAY_ASPECT_RATIO_EPSILON = 0.01f; private final Context mContext; private final Resources mResources; private final FrameLayout mContentView; private final TunableTvView mTvView; private final TunableTvView mPipView; private final TvOptionsManager mTvOptionsManager; private final int mTvViewPapWidth; private final int mTvViewShrunkenStartMargin; private final int mTvViewShrunkenEndMargin; private final int mTvViewPapStartMargin; private final int mTvViewPapEndMargin; private int mWindowWidth; private int mWindowHeight; private final int mPipViewHorizontalMargin; private final int mPipViewTopMargin; private final int mPipViewBottomMargin; private final SharedPreferences mSharedPreferences; private final TimeInterpolator mLinearOutSlowIn; private final TimeInterpolator mFastOutLinearIn; private final Handler mHandler = new Handler(); private int mDisplayMode; // Used to restore the previous state from ShrunkenTvView state. private int mTvViewStartMarginBeforeShrunken; private int mTvViewEndMarginBeforeShrunken; private int mDisplayModeBeforeShrunken; private boolean mIsUnderShrunkenTvView; private int mTvViewStartMargin; private int mTvViewEndMargin; private int mPipLayout; private int mPipSize; private boolean mPipStarted; private ObjectAnimator mTvViewAnimator; private FrameLayout.LayoutParams mTvViewLayoutParams; // TV view's position when the display mode is FULL. It is used to compute PIP location relative // to TV view's position. private MarginLayoutParams mTvViewFrame; private MarginLayoutParams mLastAnimatedTvViewFrame; private MarginLayoutParams mOldTvViewFrame; private ObjectAnimator mBackgroundAnimator; private int mBackgroundColor; private int mAppliedDisplayedMode = DisplayMode.MODE_NOT_DEFINED; private int mAppliedTvViewStartMargin; private int mAppliedTvViewEndMargin; private float mAppliedVideoDisplayAspectRatio; public TvViewUiManager(Context context, TunableTvView tvView, TunableTvView pipView, FrameLayout contentView, TvOptionsManager tvOptionManager) { mContext = context; mResources = mContext.getResources(); mTvView = tvView; mPipView = pipView; mContentView = contentView; mTvOptionsManager = tvOptionManager; DisplayManager displayManager = (DisplayManager) mContext .getSystemService(Context.DISPLAY_SERVICE); Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY); Point size = new Point(); display.getSize(size); mWindowWidth = size.x; mWindowHeight = size.y; // Have an assumption that PIP and TvView Shrinking happens only in full screen. mTvViewShrunkenStartMargin = mResources .getDimensionPixelOffset(R.dimen.shrunken_tvview_margin_start); mTvViewShrunkenEndMargin = mResources.getDimensionPixelOffset(R.dimen.shrunken_tvview_margin_end) + mResources.getDimensionPixelSize(R.dimen.side_panel_width); int papMarginHorizontal = mResources .getDimensionPixelOffset(R.dimen.papview_margin_horizontal); int papSpacing = mResources.getDimensionPixelOffset(R.dimen.papview_spacing); mTvViewPapWidth = (mWindowWidth - papSpacing) / 2 - papMarginHorizontal; mTvViewPapStartMargin = papMarginHorizontal + mTvViewPapWidth + papSpacing; mTvViewPapEndMargin = papMarginHorizontal; mTvViewFrame = createMarginLayoutParams(0, 0, 0, 0); mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext); mLinearOutSlowIn = AnimationUtils .loadInterpolator(mContext, android.R.interpolator.linear_out_slow_in); mFastOutLinearIn = AnimationUtils .loadInterpolator(mContext, android.R.interpolator.fast_out_linear_in); mPipViewHorizontalMargin = mResources .getDimensionPixelOffset(R.dimen.pipview_margin_horizontal); mPipViewTopMargin = mResources.getDimensionPixelOffset(R.dimen.pipview_margin_top); mPipViewBottomMargin = mResources.getDimensionPixelOffset(R.dimen.pipview_margin_bottom); mContentView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { int windowWidth = right - left; int windowHeight = bottom - top; if (windowWidth > 0 && windowHeight > 0) { if (mWindowWidth != windowWidth || mWindowHeight != windowHeight) { mWindowWidth = windowWidth; mWindowHeight = windowHeight; applyDisplayMode(mTvView.getVideoDisplayAspectRatio(), false, true); } } } }); } /** * Initializes animator in advance of using the animator to improve animation performance. * For fast first tune, it is not expected to be called in Activity.onCreate, but called * a few seconds later after onCreate. */ public void initAnimatorIfNeeded() { initTvAnimatorIfNeeded(); initBackgroundAnimatorIfNeeded(); } /** * It is called when shrunken TvView is desired, such as EditChannelFragment and * ChannelsLockedFragment. */ public void startShrunkenTvView() { mIsUnderShrunkenTvView = true; mTvViewStartMarginBeforeShrunken = mTvViewStartMargin; mTvViewEndMarginBeforeShrunken = mTvViewEndMargin; if (mPipStarted && getPipLayout() == TvSettings.PIP_LAYOUT_SIDE_BY_SIDE) { float sidePanelWidth = mResources.getDimensionPixelOffset(R.dimen.side_panel_width); float factor = 1.0f - sidePanelWidth / mWindowWidth; int startMargin = (int) (mTvViewPapStartMargin * factor); int endMargin = (int) (mTvViewPapEndMargin * factor + sidePanelWidth); setTvViewMargin(startMargin, endMargin); } else { setTvViewMargin(mTvViewShrunkenStartMargin, mTvViewShrunkenEndMargin); } mDisplayModeBeforeShrunken = setDisplayMode(DisplayMode.MODE_NORMAL, false, true); } /** * It is called when shrunken TvView is no longer desired, such as EditChannelFragment and * ChannelsLockedFragment. */ public void endShrunkenTvView() { mIsUnderShrunkenTvView = false; setTvViewMargin(mTvViewStartMarginBeforeShrunken, mTvViewEndMarginBeforeShrunken); setDisplayMode(mDisplayModeBeforeShrunken, false, true); } /** * Returns true, if TvView is shrunken. */ public boolean isUnderShrunkenTvView() { return mIsUnderShrunkenTvView; } /** * Returns true, if {@code displayMode} is available now. If screen ratio is matched to * video ratio, other display modes than {@link DisplayMode#MODE_NORMAL} are not available. */ public boolean isDisplayModeAvailable(int displayMode) { if (displayMode == DisplayMode.MODE_NORMAL) { return true; } int viewWidth = mContentView.getWidth(); int viewHeight = mContentView.getHeight(); float videoDisplayAspectRatio = mTvView.getVideoDisplayAspectRatio(); if (viewWidth <= 0 || viewHeight <= 0 || videoDisplayAspectRatio <= 0f) { Log.w(TAG, "Video size is currently unavailable"); if (DEBUG) { Log.d(TAG, "isDisplayModeAvailable: " + "viewWidth=" + viewWidth + ", viewHeight=" + viewHeight + ", videoDisplayAspectRatio=" + videoDisplayAspectRatio ); } return false; } float viewRatio = viewWidth / (float) viewHeight; return Math.abs(viewRatio - videoDisplayAspectRatio) >= DISPLAY_MODE_EPSILON; } /** * Returns a constant defined in DisplayMode. */ public int getDisplayMode() { if (isDisplayModeAvailable(mDisplayMode)) { return mDisplayMode; } return DisplayMode.MODE_NORMAL; } /** * Sets the display mode to the given value. * * @return the previous display mode. */ public int setDisplayMode(int displayMode, boolean storeInPreference, boolean animate) { int prev = mDisplayMode; mDisplayMode = displayMode; if (storeInPreference) { mSharedPreferences.edit().putInt(TvSettings.PREF_DISPLAY_MODE, displayMode).apply(); } applyDisplayMode(mTvView.getVideoDisplayAspectRatio(), animate, false); return prev; } /** * Restores the display mode to the display mode stored in preference. */ public void restoreDisplayMode(boolean animate) { int displayMode = mSharedPreferences .getInt(TvSettings.PREF_DISPLAY_MODE, DisplayMode.MODE_NORMAL); setDisplayMode(displayMode, false, animate); } /** * Updates TvView. It is called when video resolution is updated. */ public void updateTvView() { applyDisplayMode(mTvView.getVideoDisplayAspectRatio(), false, false); if (mTvView.isVideoAvailable() && mTvView.isFadedOut()) { mTvView.fadeIn(mResources.getInteger(R.integer.tvview_fade_in_duration), mFastOutLinearIn, null); } } /** * Fades in TvView. */ public void fadeInTvView() { if (mTvView.isFadedOut()) { mTvView.fadeIn(mResources.getInteger(R.integer.tvview_fade_in_duration), mFastOutLinearIn, null); } } /** * Fades out TvView. */ public void fadeOutTvView(Runnable postAction) { if (!mTvView.isFadedOut()) { mTvView.fadeOut(mResources.getInteger(R.integer.tvview_fade_out_duration), mLinearOutSlowIn, postAction); } } /** * Returns the current PIP layout. The layout should be one of * {@link TvSettings#PIP_LAYOUT_BOTTOM_RIGHT}, {@link TvSettings#PIP_LAYOUT_TOP_RIGHT}, * {@link TvSettings#PIP_LAYOUT_TOP_LEFT}, {@link TvSettings#PIP_LAYOUT_BOTTOM_LEFT} and * {@link TvSettings#PIP_LAYOUT_SIDE_BY_SIDE}. */ public int getPipLayout() { return mPipLayout; } /** * Sets the PIP layout. The layout should be one of * {@link TvSettings#PIP_LAYOUT_BOTTOM_RIGHT}, {@link TvSettings#PIP_LAYOUT_TOP_RIGHT}, * {@link TvSettings#PIP_LAYOUT_TOP_LEFT}, {@link TvSettings#PIP_LAYOUT_BOTTOM_LEFT} and * {@link TvSettings#PIP_LAYOUT_SIDE_BY_SIDE}. * * @param storeInPreference if true, the stored value will be restored by * {@link #restorePipLayout()}. */ public void setPipLayout(int pipLayout, boolean storeInPreference) { mPipLayout = pipLayout; if (storeInPreference) { TvSettings.setPipLayout(mContext, pipLayout); } updatePipView(mTvViewFrame); if (mPipLayout == TvSettings.PIP_LAYOUT_SIDE_BY_SIDE) { setTvViewMargin(mTvViewPapStartMargin, mTvViewPapEndMargin); setDisplayMode(DisplayMode.MODE_NORMAL, false, false); } else { setTvViewMargin(0, 0); restoreDisplayMode(false); } mTvOptionsManager.onPipLayoutChanged(pipLayout); } /** * Restores the PIP layout which {@link #setPipLayout} lastly stores. */ public void restorePipLayout() { setPipLayout(TvSettings.getPipLayout(mContext), false); } /** * Called when PIP is started. */ public void onPipStart() { mPipStarted = true; updatePipView(); mPipView.setVisibility(View.VISIBLE); } /** * Called when PIP is stopped. */ public void onPipStop() { setTvViewMargin(0, 0); mPipView.setVisibility(View.GONE); mPipStarted = false; } /** * Called when PIP is resumed. */ public void showPipForResume() { mPipView.setVisibility(View.VISIBLE); } /** * Called when PIP is paused. */ public void hidePipForPause() { if (mPipLayout != TvSettings.PIP_LAYOUT_SIDE_BY_SIDE) { mPipView.setVisibility(View.GONE); } } /** * Updates PIP view. It is usually called, when video resolution in PIP is updated. */ public void updatePipView() { updatePipView(mTvViewFrame); } /** * Returns the size of the PIP view. */ public int getPipSize() { return mPipSize; } /** * Sets PIP size and applies it immediately. * * @param pipSize PIP size. The value should be one of {@link TvSettings#PIP_SIZE_BIG} * and {@link TvSettings#PIP_SIZE_SMALL}. * @param storeInPreference if true, the stored value will be restored by * {@link #restorePipSize()}. */ public void setPipSize(int pipSize, boolean storeInPreference) { mPipSize = pipSize; if (storeInPreference) { TvSettings.setPipSize(mContext, pipSize); } updatePipView(mTvViewFrame); mTvOptionsManager.onPipSizeChanged(pipSize); } /** * Restores the PIP size which {@link #setPipSize} lastly stores. */ public void restorePipSize() { setPipSize(TvSettings.getPipSize(mContext), false); } /** * This margins will be applied when applyDisplayMode is called. */ private void setTvViewMargin(int tvViewStartMargin, int tvViewEndMargin) { mTvViewStartMargin = tvViewStartMargin; mTvViewEndMargin = tvViewEndMargin; } private boolean isTvViewFullScreen() { return mTvViewStartMargin == 0 && mTvViewEndMargin == 0; } private void setBackgroundColor(int color, FrameLayout.LayoutParams targetLayoutParams, boolean animate) { if (animate) { initBackgroundAnimatorIfNeeded(); if (mBackgroundAnimator.isStarted()) { // Cancel the current animation and start new one. mBackgroundAnimator.cancel(); } int decorViewWidth = mContentView.getWidth(); int decorViewHeight = mContentView.getHeight(); boolean hasPillarBox = mTvView.getWidth() != decorViewWidth || mTvView.getHeight() != decorViewHeight; boolean willHavePillarBox = ((targetLayoutParams.width != LayoutParams.MATCH_PARENT) && targetLayoutParams.width != decorViewWidth) || ( (targetLayoutParams.height != LayoutParams.MATCH_PARENT) && targetLayoutParams.height != decorViewHeight); if (!isTvViewFullScreen() && !hasPillarBox) { // If there is no pillar box, no animation is needed. mContentView.setBackgroundColor(color); } else if (!isTvViewFullScreen() || willHavePillarBox) { mBackgroundAnimator.setIntValues(mBackgroundColor, color); mBackgroundAnimator.setEvaluator(new ArgbEvaluator()); mBackgroundAnimator.setInterpolator(mFastOutLinearIn); mBackgroundAnimator.start(); } // In the 'else' case (TV activity is getting out of the shrunken tv view mode and will // have a pillar box), we keep the background color and don't show the animation. } else { mContentView.setBackgroundColor(color); } mBackgroundColor = color; } private void setTvViewPosition(final FrameLayout.LayoutParams layoutParams, MarginLayoutParams tvViewFrame, boolean animate) { if (DEBUG) { Log.d(TAG, "setTvViewPosition: w=" + layoutParams.width + " h=" + layoutParams.height + " s=" + layoutParams.getMarginStart() + " t=" + layoutParams.topMargin + " e=" + layoutParams.getMarginEnd() + " b=" + layoutParams.bottomMargin + " animate=" + animate); } MarginLayoutParams oldTvViewFrame = mTvViewFrame; mTvViewLayoutParams = layoutParams; mTvViewFrame = tvViewFrame; if (animate) { initTvAnimatorIfNeeded(); if (mTvViewAnimator.isStarted()) { // Cancel the current animation and start new one. mTvViewAnimator.cancel(); mOldTvViewFrame = mLastAnimatedTvViewFrame; } else { mOldTvViewFrame = oldTvViewFrame; } mTvViewAnimator.setObjectValues(mTvView.getLayoutParams(), layoutParams); mTvViewAnimator.setEvaluator(new TypeEvaluator() { FrameLayout.LayoutParams lp; @Override public FrameLayout.LayoutParams evaluate(float fraction, FrameLayout.LayoutParams startValue, FrameLayout.LayoutParams endValue) { if (lp == null) { lp = new FrameLayout.LayoutParams(0, 0); lp.gravity = startValue.gravity; } interpolateMarginsRelative(lp, startValue, endValue, fraction); return lp; } }); mTvViewAnimator .setInterpolator(isTvViewFullScreen() ? mFastOutLinearIn : mLinearOutSlowIn); mTvViewAnimator.start(); } else { if (mTvViewAnimator != null && mTvViewAnimator.isStarted()) { // Continue the current animation. // layoutParams will be applied when animation ends. return; } // This block is also called when animation ends. if (isTvViewFullScreen()) { // When this layout is for full screen, fix the surface size after layout to make // resize animation smooth. mTvView.post(new Runnable() { @Override public void run() { if (DEBUG) { Log.d(TAG, "setFixedSize: w=" + layoutParams.width + " h=" + layoutParams.height); } mTvView.setLayoutParams(layoutParams); mTvView.setFixedSurfaceSize(layoutParams.width, layoutParams.height); } }); } else { mTvView.setLayoutParams(layoutParams); } updatePipView(mTvViewFrame); } } /** * The redlines assume that the ratio of the TV screen is 16:9. If the radio is not 16:9, the * layout of PAP can be broken. */ @SuppressLint("RtlHardcoded") private void updatePipView(MarginLayoutParams tvViewFrame) { if (!mPipStarted) { return; } int width; int height; int startMargin; int endMargin; int topMargin; int bottomMargin; int gravity; if (mPipLayout == TvSettings.PIP_LAYOUT_SIDE_BY_SIDE) { gravity = Gravity.CENTER_VERTICAL | Gravity.START; height = tvViewFrame.height; float videoDisplayAspectRatio = mPipView.getVideoDisplayAspectRatio(); if (videoDisplayAspectRatio <= 0f) { width = tvViewFrame.width; } else { width = (int) (height * videoDisplayAspectRatio); if (width > tvViewFrame.width) { width = tvViewFrame.width; } } startMargin = mResources.getDimensionPixelOffset(R.dimen.papview_margin_horizontal) * tvViewFrame.width / mTvViewPapWidth + (tvViewFrame.width - width) / 2; endMargin = 0; topMargin = 0; bottomMargin = 0; } else { int tvViewWidth = tvViewFrame.width; int tvViewHeight = tvViewFrame.height; int tvStartMargin = tvViewFrame.getMarginStart(); int tvEndMargin = tvViewFrame.getMarginEnd(); int tvTopMargin = tvViewFrame.topMargin; int tvBottomMargin = tvViewFrame.bottomMargin; float horizontalScaleFactor = (float) tvViewWidth / mWindowWidth; float verticalScaleFactor = (float) tvViewHeight / mWindowHeight; int maxWidth; if (mPipSize == TvSettings.PIP_SIZE_SMALL) { maxWidth = (int) (mResources.getDimensionPixelSize(R.dimen.pipview_small_size_width) * horizontalScaleFactor); height = (int) (mResources.getDimensionPixelSize(R.dimen.pipview_small_size_height) * verticalScaleFactor); } else if (mPipSize == TvSettings.PIP_SIZE_BIG) { maxWidth = (int) (mResources.getDimensionPixelSize(R.dimen.pipview_large_size_width) * horizontalScaleFactor); height = (int) (mResources.getDimensionPixelSize(R.dimen.pipview_large_size_height) * verticalScaleFactor); } else { throw new IllegalArgumentException("Invalid PIP size: " + mPipSize); } float videoDisplayAspectRatio = mPipView.getVideoDisplayAspectRatio(); if (videoDisplayAspectRatio <= 0f) { width = maxWidth; } else { width = (int) (height * videoDisplayAspectRatio); if (width > maxWidth) { width = maxWidth; } } startMargin = tvStartMargin + (int) (mPipViewHorizontalMargin * horizontalScaleFactor); endMargin = tvEndMargin + (int) (mPipViewHorizontalMargin * horizontalScaleFactor); topMargin = tvTopMargin + (int) (mPipViewTopMargin * verticalScaleFactor); bottomMargin = tvBottomMargin + (int) (mPipViewBottomMargin * verticalScaleFactor); switch (mPipLayout) { case TvSettings.PIP_LAYOUT_TOP_LEFT: gravity = Gravity.TOP | Gravity.LEFT; break; case TvSettings.PIP_LAYOUT_TOP_RIGHT: gravity = Gravity.TOP | Gravity.RIGHT; break; case TvSettings.PIP_LAYOUT_BOTTOM_LEFT: gravity = Gravity.BOTTOM | Gravity.LEFT; break; case TvSettings.PIP_LAYOUT_BOTTOM_RIGHT: gravity = Gravity.BOTTOM | Gravity.RIGHT; break; default: throw new IllegalArgumentException("Invalid PIP location: " + mPipLayout); } } FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mPipView.getLayoutParams(); if (lp.width != width || lp.height != height || lp.getMarginStart() != startMargin || lp.getMarginEnd() != endMargin || lp.topMargin != topMargin || lp.bottomMargin != bottomMargin || lp.gravity != gravity) { lp.width = width; lp.height = height; lp.setMarginStart(startMargin); lp.setMarginEnd(endMargin); lp.topMargin = topMargin; lp.bottomMargin = bottomMargin; lp.gravity = gravity; mPipView.setLayoutParams(lp); } } private void initTvAnimatorIfNeeded() { if (mTvViewAnimator != null) { return; } // TvViewAnimator animates TvView by repeatedly re-layouting TvView. // TvView includes a SurfaceView on which scale/translation effects do not work. Normally, // SurfaceView can be animated by changing left/top/right/bottom directly using // ObjectAnimator, although it would require calling getChildAt(0) against TvView (which is // supposed to be opaque). More importantly, this method does not work in case of TvView, // because TvView may request layout itself during animation and layout SurfaceView with // its own parameters when TvInputService requests to do so. mTvViewAnimator = new ObjectAnimator(); mTvViewAnimator.setTarget(mTvView); mTvViewAnimator.setProperty( Property.of(FrameLayout.class, ViewGroup.LayoutParams.class, "layoutParams")); mTvViewAnimator.setDuration(mResources.getInteger(R.integer.tvview_anim_duration)); mTvViewAnimator.addListener(new AnimatorListenerAdapter() { private boolean mCanceled = false; @Override public void onAnimationCancel(Animator animation) { mCanceled = true; } @Override public void onAnimationEnd(Animator animation) { if (mCanceled) { mCanceled = false; return; } mHandler.post(new Runnable() { @Override public void run() { setTvViewPosition(mTvViewLayoutParams, mTvViewFrame, false); } }); } }); mTvViewAnimator.addUpdateListener(new AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animator) { float fraction = animator.getAnimatedFraction(); mLastAnimatedTvViewFrame = new MarginLayoutParams(0, 0); interpolateMarginsRelative(mLastAnimatedTvViewFrame, mOldTvViewFrame, mTvViewFrame, fraction); updatePipView(mLastAnimatedTvViewFrame); } }); } private void initBackgroundAnimatorIfNeeded() { if (mBackgroundAnimator != null) { return; } mBackgroundAnimator = new ObjectAnimator(); mBackgroundAnimator.setTarget(mContentView); mBackgroundAnimator.setPropertyName("backgroundColor"); mBackgroundAnimator .setDuration(mResources.getInteger(R.integer.tvactivity_background_anim_duration)); mBackgroundAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mHandler.post(new Runnable() { @Override public void run() { mContentView.setBackgroundColor(mBackgroundColor); } }); } }); } private void applyDisplayMode(float videoDisplayAspectRatio, boolean animate, boolean forceUpdate) { if (mAppliedDisplayedMode == mDisplayMode && mAppliedTvViewStartMargin == mTvViewStartMargin && mAppliedTvViewEndMargin == mTvViewEndMargin && Math.abs(mAppliedVideoDisplayAspectRatio - videoDisplayAspectRatio) < DISPLAY_ASPECT_RATIO_EPSILON) { if (!forceUpdate) { return; } } else { mAppliedDisplayedMode = mDisplayMode; mAppliedTvViewStartMargin = mTvViewStartMargin; mAppliedTvViewEndMargin = mTvViewEndMargin; mAppliedVideoDisplayAspectRatio = videoDisplayAspectRatio; } int availableAreaWidth = mWindowWidth - mTvViewStartMargin - mTvViewEndMargin; int availableAreaHeight = availableAreaWidth * mWindowHeight / mWindowWidth; FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(0, 0, ((FrameLayout.LayoutParams) mTvView.getLayoutParams()).gravity); int displayMode = mDisplayMode; double availableAreaRatio = 0; double videoRatio = 0; if (availableAreaWidth <= 0 || availableAreaHeight <= 0) { displayMode = DisplayMode.MODE_FULL; Log.w(TAG, "Some resolution info is missing during applyDisplayMode. (" + "availableAreaWidth=" + availableAreaWidth + ", availableAreaHeight=" + availableAreaHeight + ")"); } else { availableAreaRatio = (double) availableAreaWidth / availableAreaHeight; if (videoDisplayAspectRatio <= 0f) { videoRatio = (double) mWindowWidth / mWindowHeight; } else { videoRatio = videoDisplayAspectRatio; } } int tvViewFrameTop = (mWindowHeight - availableAreaHeight) / 2; MarginLayoutParams tvViewFrame = createMarginLayoutParams( mTvViewStartMargin, mTvViewEndMargin, tvViewFrameTop, tvViewFrameTop); layoutParams.width = availableAreaWidth; layoutParams.height = availableAreaHeight; switch (displayMode) { case DisplayMode.MODE_FULL: layoutParams.width = availableAreaWidth; layoutParams.height = availableAreaHeight; break; case DisplayMode.MODE_ZOOM: if (videoRatio < availableAreaRatio) { // Y axis will be clipped. layoutParams.width = availableAreaWidth; layoutParams.height = (int) (availableAreaWidth / videoRatio); } else { // X axis will be clipped. layoutParams.width = (int) (availableAreaHeight * videoRatio); layoutParams.height = availableAreaHeight; } break; case DisplayMode.MODE_NORMAL: if (videoRatio < availableAreaRatio) { // X axis has black area. layoutParams.width = (int) (availableAreaHeight * videoRatio); layoutParams.height = availableAreaHeight; } else { // Y axis has black area. layoutParams.width = availableAreaWidth; layoutParams.height = (int) (availableAreaWidth / videoRatio); } break; } // FrameLayout has an issue with centering when left and right margins differ. // So stick to Gravity.START | Gravity.CENTER_VERTICAL. int marginStart = mTvViewStartMargin + (availableAreaWidth - layoutParams.width) / 2; layoutParams.setMarginStart(marginStart); // Set marginEnd as well because setTvViewPosition uses both start/end margin. layoutParams.setMarginEnd(mWindowWidth - layoutParams.width - marginStart); setBackgroundColor(Utils.getColor(mResources, isTvViewFullScreen() ? R.color.tvactivity_background : R.color.tvactivity_background_on_shrunken_tvview), layoutParams, animate); setTvViewPosition(layoutParams, tvViewFrame, animate); // Update the current display mode. mTvOptionsManager.onDisplayModeChanged(displayMode); } private static int interpolate(int start, int end, float fraction) { return (int) (start + (end - start) * fraction); } private static void interpolateMarginsRelative(MarginLayoutParams out, MarginLayoutParams startValue, MarginLayoutParams endValue, float fraction) { out.topMargin = interpolate(startValue.topMargin, endValue.topMargin, fraction); out.bottomMargin = interpolate(startValue.bottomMargin, endValue.bottomMargin, fraction); out.setMarginStart(interpolate(startValue.getMarginStart(), endValue.getMarginStart(), fraction)); out.setMarginEnd(interpolate(startValue.getMarginEnd(), endValue.getMarginEnd(), fraction)); out.width = interpolate(startValue.width, endValue.width, fraction); out.height = interpolate(startValue.height, endValue.height, fraction); } private MarginLayoutParams createMarginLayoutParams( int startMargin, int endMargin, int topMargin, int bottomMargin) { MarginLayoutParams lp = new MarginLayoutParams(0, 0); lp.setMarginStart(startMargin); lp.setMarginEnd(endMargin); lp.topMargin = topMargin; lp.bottomMargin = bottomMargin; lp.width = mWindowWidth - startMargin - endMargin; lp.height = mWindowHeight - topMargin - bottomMargin; return lp; } }