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 
TvViewUiManager(Context context, TunableTvView tvView, TunableTvView pipView, FrameLayout contentView, TvOptionsManager tvOptionManager)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      */
initAnimatorIfNeeded()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      */
startShrunkenTvView()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      */
endShrunkenTvView()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      */
isUnderShrunkenTvView()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      */
isDisplayModeAvailable(int displayMode)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      */
getDisplayMode()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      */
setDisplayMode(int displayMode, boolean storeInPreference, boolean animate)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      */
restoreDisplayMode(boolean animate)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      */
updateTvView()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      */
fadeInTvView()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      */
fadeOutTvView(Runnable postAction)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      */
getPipLayout()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      */
setPipLayout(int pipLayout, boolean storeInPreference)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      */
restorePipLayout()348     public void restorePipLayout() {
349         setPipLayout(TvSettings.getPipLayout(mContext), false);
350     }
351 
352     /**
353      * Called when PIP is started.
354      */
onPipStart()355     public void onPipStart() {
356         mPipStarted = true;
357         updatePipView();
358         mPipView.setVisibility(View.VISIBLE);
359     }
360 
361     /**
362      * Called when PIP is stopped.
363      */
onPipStop()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      */
showPipForResume()373     public void showPipForResume() {
374         mPipView.setVisibility(View.VISIBLE);
375     }
376 
377     /**
378      * Called when PIP is paused.
379      */
hidePipForPause()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      */
updatePipView()389     public void updatePipView() {
390         updatePipView(mTvViewFrame);
391     }
392 
393     /**
394      * Returns the size of the PIP view.
395      */
getPipSize()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      */
setPipSize(int pipSize, boolean storeInPreference)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      */
restorePipSize()420     public void restorePipSize() {
421         setPipSize(TvSettings.getPipSize(mContext), false);
422     }
423 
424     /**
425      * This margins will be applied when applyDisplayMode is called.
426      */
setTvViewMargin(int tvViewStartMargin, int tvViewEndMargin)427     private void setTvViewMargin(int tvViewStartMargin, int tvViewEndMargin) {
428         mTvViewStartMargin = tvViewStartMargin;
429         mTvViewEndMargin = tvViewEndMargin;
430     }
431 
isTvViewFullScreen()432     private boolean isTvViewFullScreen() {
433         return mTvViewStartMargin == 0 && mTvViewEndMargin == 0;
434     }
435 
setBackgroundColor(int color, FrameLayout.LayoutParams targetLayoutParams, boolean animate)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 
setTvViewPosition(final FrameLayout.LayoutParams layoutParams, MarginLayoutParams tvViewFrame, boolean animate)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")
updatePipView(MarginLayoutParams tvViewFrame)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 
initTvAnimatorIfNeeded()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 
initBackgroundAnimatorIfNeeded()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 
applyDisplayMode(float videoDisplayAspectRatio, boolean animate, boolean forceUpdate)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 
interpolate(int start, int end, float fraction)803     private static int interpolate(int start, int end, float fraction) {
804         return (int) (start + (end - start) * fraction);
805     }
806 
interpolateMarginsRelative(MarginLayoutParams out, MarginLayoutParams startValue, MarginLayoutParams endValue, float fraction)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 
createMarginLayoutParams( int startMargin, int endMargin, int topMargin, int bottomMargin)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