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