blob: 5ad89bfac32983e4d9a8f032f645b71595e53feb [file] [log] [blame]
/*
* 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>() {
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;
}
}