Home | History | Annotate | Download | only in ui
      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.content.Context;
     29 import android.content.SharedPreferences;
     30 import android.content.res.Resources;
     31 import android.graphics.Point;
     32 import android.hardware.display.DisplayManager;
     33 import android.os.Handler;
     34 import android.preference.PreferenceManager;
     35 import android.util.Log;
     36 import android.util.Property;
     37 import android.view.Display;
     38 import android.view.Gravity;
     39 import android.view.View;
     40 import android.view.ViewGroup;
     41 import android.view.ViewGroup.LayoutParams;
     42 import android.view.ViewGroup.MarginLayoutParams;
     43 import android.view.animation.AnimationUtils;
     44 import android.widget.FrameLayout;
     45 
     46 import com.android.tv.R;
     47 import com.android.tv.TvOptionsManager;
     48 import com.android.tv.data.DisplayMode;
     49 import com.android.tv.util.TvSettings;
     50 import 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  */
     57 public 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