From c24734e830a69a9745d43ca6b0f2faebc9a00375 Mon Sep 17 00:00:00 2001 From: Galia Peycheva Date: Fri, 18 Feb 2022 21:03:18 +0100 Subject: [PATCH] Add edu text to TV PiP The edu text is added to the bottom of the TvPipMenuView, below the pip content. This also makes the front and back pip menu surfaces change size while the edu text is showing. It is shown for 10 seconds and after that it is hidden by sliding upwards. This repositions the PiP window so that it is placed properly (centered in case of expanded pip). This change introduces temporary decor insets - this is for the edu text. Temporary decor insets don't interfere with the keep-clear area algorithm - they will not cause the pip to move if they intersect with a keep-clear area. They are only taken into account when calculating the anchor bounds. Permanent decor insets are also taken into account when calculating intersecting keep-clear areas Bug: 219884907 Test: manual - start pip and check that the edu text is displayed and goes away after 10 seconds Change-Id: Ie27c633c517903a4bf0b1544626bd00b621c19d4 --- .../Shell/res/drawable/home_icon.xml | 45 +++++++ .../Shell/res/layout/tv_pip_menu.xml | 55 +++++++- .../Shell/res/values-tvdpi/dimen.xml | 7 + .../Shell/res/values/colors_tv.xml | 3 + .../WindowManager/Shell/res/values/styles.xml | 9 ++ .../wm/shell/pip/tv/CenteredImageSpan.java | 76 +++++++++++ .../wm/shell/pip/tv/TvPipBoundsAlgorithm.java | 5 +- .../wm/shell/pip/tv/TvPipBoundsState.java | 9 ++ .../wm/shell/pip/tv/TvPipController.java | 32 ++++- .../shell/pip/tv/TvPipKeepClearAlgorithm.kt | 51 ++++--- .../wm/shell/pip/tv/TvPipMenuController.java | 46 +++++-- .../wm/shell/pip/tv/TvPipMenuView.java | 127 ++++++++++++++++-- 12 files changed, 421 insertions(+), 44 deletions(-) create mode 100644 libs/WindowManager/Shell/res/drawable/home_icon.xml create mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/CenteredImageSpan.java diff --git a/libs/WindowManager/Shell/res/drawable/home_icon.xml b/libs/WindowManager/Shell/res/drawable/home_icon.xml new file mode 100644 index 0000000000000..1669d0167e4bc --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/home_icon.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + diff --git a/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml index f84b8f642953f..dbd5a9b370ab6 100644 --- a/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml +++ b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml @@ -21,22 +21,36 @@ android:layout_height="match_parent" android:gravity="center|top"> + + + android:scrollbars="none"> + + + + + + + 5dp 1dp + + 24dp + 9sp + 14sp + 10500 + 1000 + 300 diff --git a/libs/WindowManager/Shell/res/values/colors_tv.xml b/libs/WindowManager/Shell/res/values/colors_tv.xml index 4e7fee8e2e1e3..fa90fe36b545a 100644 --- a/libs/WindowManager/Shell/res/values/colors_tv.xml +++ b/libs/WindowManager/Shell/res/values/colors_tv.xml @@ -24,4 +24,7 @@ #990E0E0F #E8EAED #1E232C + + #99D2E3FC + #D2E3FC diff --git a/libs/WindowManager/Shell/res/values/styles.xml b/libs/WindowManager/Shell/res/values/styles.xml index 7733201d2465f..19f7c3ef4364e 100644 --- a/libs/WindowManager/Shell/res/values/styles.xml +++ b/libs/WindowManager/Shell/res/values/styles.xml @@ -47,4 +47,13 @@ 96dp 48dp + + diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/CenteredImageSpan.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/CenteredImageSpan.java new file mode 100644 index 0000000000000..6efdd57bdc485 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/CenteredImageSpan.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.pip.tv; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.text.style.ImageSpan; + +/** An ImageSpan for a Drawable that is centered vertically in the line. */ +public class CenteredImageSpan extends ImageSpan { + + private Drawable mDrawable; + + public CenteredImageSpan(Drawable drawable) { + super(drawable); + } + + @Override + public int getSize( + Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fontMetrics) { + final Drawable drawable = getCachedDrawable(); + final Rect rect = drawable.getBounds(); + + if (fontMetrics != null) { + Paint.FontMetricsInt paintFontMetrics = paint.getFontMetricsInt(); + fontMetrics.ascent = paintFontMetrics.ascent; + fontMetrics.descent = paintFontMetrics.descent; + fontMetrics.top = paintFontMetrics.top; + fontMetrics.bottom = paintFontMetrics.bottom; + } + + return rect.right; + } + + @Override + public void draw( + Canvas canvas, + CharSequence text, + int start, + int end, + float x, + int top, + int y, + int bottom, + Paint paint) { + final Drawable drawable = getCachedDrawable(); + canvas.save(); + final int transY = (bottom - drawable.getBounds().bottom) / 2; + canvas.translate(x, transY); + drawable.draw(canvas); + canvas.restore(); + } + + private Drawable getCachedDrawable() { + if (mDrawable == null) { + mDrawable = getDrawable(); + } + return mDrawable; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java index 93f699e4645d4..21d5d401835d6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java @@ -151,7 +151,10 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm { mKeepClearAlgorithm.setScreenSize(screenSize); mKeepClearAlgorithm.setMovementBounds(insetBounds); mKeepClearAlgorithm.setStashOffset(mTvPipBoundsState.getStashOffset()); - mKeepClearAlgorithm.setPipDecorInsets(mTvPipBoundsState.getPipMenuPermanentDecorInsets()); + mKeepClearAlgorithm.setPipPermanentDecorInsets( + mTvPipBoundsState.getPipMenuPermanentDecorInsets()); + mKeepClearAlgorithm.setPipTemporaryDecorInsets( + mTvPipBoundsState.getPipMenuTemporaryDecorInsets()); final Placement placement = mKeepClearAlgorithm.calculatePipPosition( pipSize, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.java index cdb18bc180beb..ea074993bae18 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.java @@ -62,6 +62,7 @@ public class TvPipBoundsState extends PipBoundsState { private int mTvPipGravity; private @Nullable Size mTvExpandedSize; private @NonNull Insets mPipMenuPermanentDecorInsets = Insets.NONE; + private @NonNull Insets mPipMenuTemporaryDecorInsets = Insets.NONE; public TvPipBoundsState(@NonNull Context context) { super(context); @@ -167,4 +168,12 @@ public class TvPipBoundsState extends PipBoundsState { public @NonNull Insets getPipMenuPermanentDecorInsets() { return mPipMenuPermanentDecorInsets; } + + public void setPipMenuTemporaryDecorInsets(@NonNull Insets temporaryDecorInsets) { + mPipMenuTemporaryDecorInsets = temporaryDecorInsets; + } + + public @NonNull Insets getPipMenuTemporaryDecorInsets() { + return mPipMenuTemporaryDecorInsets; + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java index 46b8e60982739..0e1f5a24f4a73 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java @@ -115,6 +115,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal private int mPipForceCloseDelay; private int mResizeAnimationDuration; + private int mEduTextWindowExitAnimationDurationMs; public static Pip create( Context context, @@ -323,11 +324,15 @@ public class TvPipController implements PipTransitionController.PipTransitionCal } } + private void updatePinnedStackBounds() { + updatePinnedStackBounds(mResizeAnimationDuration); + } + /** * Update the PiP bounds based on the state of the PiP and keep clear areas. * Animates to the current PiP bounds, and schedules unstashing the PiP if necessary. */ - private void updatePinnedStackBounds() { + private void updatePinnedStackBounds(int animationDuration) { if (mState == STATE_NO_PIP) { return; } @@ -353,23 +358,26 @@ public class TvPipController implements PipTransitionController.PipTransitionCal mUnstashRunnable = null; } if (!disallowStashing && placement.getUnstashDestinationBounds() != null) { - mUnstashRunnable = () -> movePinnedStackTo(placement.getUnstashDestinationBounds()); + mUnstashRunnable = () -> { + movePinnedStackTo(placement.getUnstashDestinationBounds(), animationDuration); + }; mMainHandler.postAtTime(mUnstashRunnable, placement.getUnstashTime()); } } /** Animates the PiP to the given bounds. */ private void movePinnedStackTo(Rect bounds) { + movePinnedStackTo(bounds, mResizeAnimationDuration); + } + + /** Animates the PiP to the given bounds with the given animation duration. */ + private void movePinnedStackTo(Rect bounds, int animationDuration) { if (DEBUG) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: movePinnedStack() - new pip bounds: %s", TAG, bounds.toShortString()); } mPipTaskOrganizer.scheduleAnimateResizePip(bounds, - mResizeAnimationDuration, rect -> { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: movePinnedStack() animation done", TAG); - } + animationDuration, rect -> { mTvPipMenuController.updateExpansionState(); }); } @@ -408,6 +416,11 @@ public class TvPipController implements PipTransitionController.PipTransitionCal onPipDisappeared(); } + @Override + public void closeEduText() { + updatePinnedStackBounds(mEduTextWindowExitAnimationDurationMs); + } + private void registerSessionListenerForCurrentUser() { mPipMediaController.registerSessionListenerForCurrentUser(); } @@ -457,6 +470,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: onPipTransition_Started(), state=%s", TAG, stateToName(mState)); } + mTvPipMenuController.notifyPipAnimating(true); } @Override @@ -465,6 +479,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: onPipTransition_Canceled(), state=%s", TAG, stateToName(mState)); } + mTvPipMenuController.notifyPipAnimating(false); } @Override @@ -476,6 +491,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: onPipTransition_Finished(), state=%s", TAG, stateToName(mState)); } + mTvPipMenuController.notifyPipAnimating(false); } private void setState(@State int state) { @@ -491,6 +507,8 @@ public class TvPipController implements PipTransitionController.PipTransitionCal final Resources res = mContext.getResources(); mResizeAnimationDuration = res.getInteger(R.integer.config_pipResizeAnimationDuration); mPipForceCloseDelay = res.getInteger(R.integer.config_pipForceCloseDelay); + mEduTextWindowExitAnimationDurationMs = + res.getInteger(R.integer.pip_edu_text_window_exit_animation_duration_ms); } private void registerTaskStackListenerCallback(TaskStackListenerImpl taskStackListener) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithm.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithm.kt index 48f76288f1ec6..9ede4433a9783 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithm.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithm.kt @@ -95,8 +95,12 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) { private var lastAreasOverlappingUnstashPosition: Set = emptySet() private var lastStashTime: Long = Long.MIN_VALUE - /** Spaces around the PiP that we should leave space for when placing the PiP */ - private var pipDecorInsets = Insets.NONE + /** Spaces around the PiP that we should leave space for when placing the PiP. Permanent PiP + * decorations are relevant for calculating intersecting keep clear areas */ + private var pipPermanentDecorInsets = Insets.NONE + /** Spaces around the PiP that we should leave space for when placing the PiP. Temporary PiP + * decorations are not relevant for calculating intersecting keep clear areas */ + private var pipTemporaryDecorInsets = Insets.NONE /** * Calculates the position the PiP should be placed at, taking into consideration the @@ -125,19 +129,21 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) { val transformedRestrictedAreas = transformAndFilterAreas(restrictedAreas) val transformedUnrestrictedAreas = transformAndFilterAreas(unrestrictedAreas) - val pipSizeWithDecors = addDecors(pipSize) - val pipAnchorBounds = getNormalPipAnchorBounds(pipSizeWithDecors, transformedMovementBounds) + val pipSizeWithAllDecors = addDecors(pipSize) + val pipAnchorBoundsWithAllDecors = + getNormalPipAnchorBounds(pipSizeWithAllDecors, transformedMovementBounds) + val pipAnchorBoundsWithPermanentDecors = removeTemporaryDecors(pipAnchorBoundsWithAllDecors) val result = calculatePipPositionTransformed( - pipAnchorBounds, + pipAnchorBoundsWithPermanentDecors, transformedRestrictedAreas, transformedUnrestrictedAreas ) - val pipBounds = removeDecors(fromTransformedSpace(result.bounds)) - val anchorBounds = removeDecors(fromTransformedSpace(result.anchorBounds)) + val pipBounds = removePermanentDecors(fromTransformedSpace(result.bounds)) + val anchorBounds = removePermanentDecors(fromTransformedSpace(result.anchorBounds)) val unstashedDestBounds = result.unstashDestinationBounds?.let { - removeDecors(fromTransformedSpace(it)) + removePermanentDecors(fromTransformedSpace(it)) } return Placement( @@ -458,9 +464,14 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) { transformedMovementBounds = toTransformedSpace(movementBounds) } - fun setPipDecorInsets(insets: Insets) { - if (pipDecorInsets == insets) return - pipDecorInsets = insets + fun setPipPermanentDecorInsets(insets: Insets) { + if (pipPermanentDecorInsets == insets) return + pipPermanentDecorInsets = insets + } + + fun setPipTemporaryDecorInsets(insets: Insets) { + if (pipTemporaryDecorInsets == insets) return + pipTemporaryDecorInsets = insets } /** @@ -756,16 +767,26 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) { */ private fun addDecors(size: Size): Size { val bounds = Rect(0, 0, size.width, size.height) - bounds.inset(pipDecorInsets) + bounds.inset(pipPermanentDecorInsets) + bounds.inset(pipTemporaryDecorInsets) return Size(bounds.width(), bounds.height()) } /** - * Removes the space that was reserved for decorations around the pip + * Removes the space that was reserved for permanent decorations around the pip */ - private fun removeDecors(bounds: Rect): Rect { - val pipDecorReverseInsets = Insets.subtract(Insets.NONE, pipDecorInsets) + private fun removePermanentDecors(bounds: Rect): Rect { + val pipDecorReverseInsets = Insets.subtract(Insets.NONE, pipPermanentDecorInsets) + bounds.inset(pipDecorReverseInsets) + return bounds + } + + /** + * Removes the space that was reserved for temporary decorations around the pip + */ + private fun removeTemporaryDecors(bounds: Rect): Rect { + val pipDecorReverseInsets = Insets.subtract(Insets.NONE, pipTemporaryDecorInsets) bounds.inset(pipDecorReverseInsets) return bounds } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java index 3e7d10c3fcb8e..7b8dcf70cff01 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java @@ -63,6 +63,9 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis private final SystemWindows mSystemWindows; private final TvPipBoundsState mTvPipBoundsState; private final Handler mMainHandler; + private final int mPipMenuBorderWidth; + private final int mPipEduTextShowDurationMs; + private final int mPipEduTextHeight; private Delegate mDelegate; private SurfaceControl mLeash; @@ -98,7 +101,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis if (DEBUG) e.printStackTrace(); } }; - private final int mPipMenuSurfaceOuterSpace; + private final Runnable mCloseEduTextRunnable = this::closeEduText; public TvPipMenuController(Context context, TvPipBoundsState tvPipBoundsState, SystemWindows systemWindows, PipMediaController pipMediaController, @@ -122,13 +125,12 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis pipMediaController.addActionListener(this::onMediaActionsChanged); - mPipMenuSurfaceOuterSpace = context.getResources() - .getDimensionPixelSize(R.dimen.pip_menu_outer_space); - - final int pipBorderWidth = context.getResources() + mPipEduTextShowDurationMs = context.getResources() + .getInteger(R.integer.pip_edu_text_show_duration_ms); + mPipEduTextHeight = context.getResources() + .getDimensionPixelSize(R.dimen.pip_menu_edu_text_view_height); + mPipMenuBorderWidth = context.getResources() .getDimensionPixelSize(R.dimen.pip_menu_border_width); - mTvPipBoundsState.setPipMenuPermanentDecorInsets(Insets.of(-pipBorderWidth, - -pipBorderWidth, -pipBorderWidth, -pipBorderWidth)); } void setDelegate(Delegate delegate) { @@ -169,6 +171,11 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis attachPipBackgroundView(); attachPipMenuView(); + + mTvPipBoundsState.setPipMenuPermanentDecorInsets(Insets.of(-mPipMenuBorderWidth, + -mPipMenuBorderWidth, -mPipMenuBorderWidth, -mPipMenuBorderWidth)); + mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.of(0, 0, 0, -mPipEduTextHeight)); + mMainHandler.postDelayed(mCloseEduTextRunnable, mPipEduTextShowDurationMs); } private void attachPipMenuView() { @@ -204,6 +211,10 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis 0 /* displayId */, SHELL_ROOT_LAYER_PIP); } + void notifyPipAnimating(boolean animating) { + mPipMenuView.setEduTextActive(!animating); + } + void showMovementMenuOnly() { if (DEBUG) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, @@ -228,6 +239,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis if (mPipMenuView == null) { return; } + maybeCloseEduText(); maybeUpdateMenuViewActions(); updateExpansionState(); @@ -239,6 +251,19 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis } } + private void maybeCloseEduText() { + if (mMainHandler.hasCallbacks(mCloseEduTextRunnable)) { + mMainHandler.removeCallbacks(mCloseEduTextRunnable); + mCloseEduTextRunnable.run(); + } + } + + private void closeEduText() { + mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.NONE); + mPipMenuView.hideEduText(); + mDelegate.closeEduText(); + } + void updateGravity(int gravity) { mPipMenuView.showMovementHints(gravity); } @@ -250,9 +275,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis } private Rect calculateMenuSurfaceBounds(Rect pipBounds) { - Rect menuBounds = new Rect(pipBounds); - menuBounds.inset(-mPipMenuSurfaceOuterSpace, -mPipMenuSurfaceOuterSpace); - return menuBounds; + return mPipMenuView.getPipMenuContainerBounds(pipBounds); } void closeMenu() { @@ -321,6 +344,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis @Override public void detach() { closeMenu(); + mMainHandler.removeCallbacks(mCloseEduTextRunnable); detachPipMenu(); mLeash = null; } @@ -598,6 +622,8 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis void onMenuClosed(); + void closeEduText(); + void closePip(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java index f5c8871cb9914..5b0db8c865299 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java @@ -25,11 +25,17 @@ import static android.view.KeyEvent.KEYCODE_DPAD_RIGHT; import static android.view.KeyEvent.KEYCODE_DPAD_UP; import static android.view.KeyEvent.KEYCODE_ENTER; +import android.animation.ValueAnimator; import android.app.PendingIntent; import android.app.RemoteAction; import android.content.Context; import android.graphics.Rect; +import android.graphics.drawable.Drawable; import android.os.Handler; +import android.text.Annotation; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.SpannedString; import android.util.AttributeSet; import android.view.Gravity; import android.view.KeyEvent; @@ -40,6 +46,7 @@ import android.view.ViewRootImpl; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.LinearLayout; +import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -49,6 +56,7 @@ import com.android.wm.shell.R; import com.android.wm.shell.protolog.ShellProtoLogGroup; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Objects; @@ -67,6 +75,15 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { private final LinearLayout mActionButtonsContainer; private final View mMenuFrameView; private final List mAdditionalButtons = new ArrayList<>(); + private final View mPipFrameView; + private final View mPipView; + private final TextView mEduTextView; + private final View mEduTextContainerView; + private final int mPipMenuOuterSpace; + private final int mPipMenuBorderWidth; + private final int mEduTextFadeExitAnimationDurationMs; + private final int mEduTextSlideExitAnimationDurationMs; + private int mEduTextHeight; private final ImageView mArrowUp; private final ImageView mArrowRight; @@ -76,7 +93,7 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { private final ViewGroup mScrollView; private final ViewGroup mHorizontalScrollView; - private Rect mCurrentBounds; + private Rect mCurrentPipBounds; private final TvPipMenuActionButton mExpandButton; private final TvPipMenuActionButton mCloseButton; @@ -118,24 +135,86 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { mHorizontalScrollView = findViewById(R.id.tv_pip_menu_horizontal_scroll); mMenuFrameView = findViewById(R.id.tv_pip_menu_frame); + mPipFrameView = findViewById(R.id.tv_pip_border); + mPipView = findViewById(R.id.tv_pip); mArrowUp = findViewById(R.id.tv_pip_menu_arrow_up); mArrowRight = findViewById(R.id.tv_pip_menu_arrow_right); mArrowDown = findViewById(R.id.tv_pip_menu_arrow_down); mArrowLeft = findViewById(R.id.tv_pip_menu_arrow_left); + mEduTextView = findViewById(R.id.tv_pip_menu_edu_text); + mEduTextContainerView = findViewById(R.id.tv_pip_menu_edu_text_container); + mPipMenuFadeAnimationDuration = context.getResources() .getInteger(R.integer.pip_menu_fade_animation_duration); + mPipMenuOuterSpace = context.getResources() + .getDimensionPixelSize(R.dimen.pip_menu_outer_space); + mPipMenuBorderWidth = context.getResources() + .getDimensionPixelSize(R.dimen.pip_menu_border_width); + mEduTextHeight = context.getResources() + .getDimensionPixelSize(R.dimen.pip_menu_edu_text_view_height); + mEduTextFadeExitAnimationDurationMs = context.getResources() + .getInteger(R.integer.pip_edu_text_view_exit_animation_duration_ms); + mEduTextSlideExitAnimationDurationMs = context.getResources() + .getInteger(R.integer.pip_edu_text_window_exit_animation_duration_ms); + + initEduText(); } - void updateLayout(Rect updatedBounds) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: update menu layout: %s", TAG, updatedBounds.toShortString()); - boolean previouslyVertical = - mCurrentBounds != null && mCurrentBounds.height() > mCurrentBounds.width(); - boolean vertical = updatedBounds.height() > updatedBounds.width(); + void initEduText() { + final SpannedString eduText = (SpannedString) getResources().getText(R.string.pip_edu_text); + final SpannableString spannableString = new SpannableString(eduText); + Arrays.stream(eduText.getSpans(0, eduText.length(), Annotation.class)).findFirst() + .ifPresent(annotation -> { + final Drawable icon = + getResources().getDrawable(R.drawable.home_icon, mContext.getTheme()); + if (icon != null) { + icon.mutate(); + icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight()); + spannableString.setSpan(new CenteredImageSpan(icon), + eduText.getSpanStart(annotation), + eduText.getSpanEnd(annotation), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + }); + + mEduTextView.setText(spannableString); + } + + void setEduTextActive(boolean active) { + mEduTextView.setSelected(active); + } + + void hideEduText() { + final ValueAnimator heightAnimation = ValueAnimator.ofInt(mEduTextHeight, 0); + heightAnimation.setDuration(mEduTextSlideExitAnimationDurationMs); + heightAnimation.setInterpolator(TvPipInterpolators.BROWSE); + heightAnimation.addUpdateListener(animator -> { + mEduTextHeight = (int) animator.getAnimatedValue(); + }); + mEduTextView.animate() + .alpha(0f) + .setInterpolator(TvPipInterpolators.EXIT) + .setDuration(mEduTextFadeExitAnimationDurationMs) + .withEndAction(() -> { + mEduTextContainerView.setVisibility(GONE); + }).start(); + heightAnimation.start(); + } + + void updateLayout(Rect updatedPipBounds) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: update menu layout: %s", TAG, updatedPipBounds.toShortString()); + + boolean previouslyVertical = + mCurrentPipBounds != null && mCurrentPipBounds.height() > mCurrentPipBounds.width(); + boolean vertical = updatedPipBounds.height() > updatedPipBounds.width(); + + mCurrentPipBounds = updatedPipBounds; + + updatePipFrameBounds(); - mCurrentBounds = updatedBounds; if (previouslyVertical == vertical) { if (DEBUG) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, @@ -163,6 +242,38 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { mHorizontalScrollView.setVisibility(vertical ? GONE : VISIBLE); } + Rect getPipMenuContainerBounds(Rect pipBounds) { + final Rect menuUiBounds = new Rect(pipBounds); + menuUiBounds.inset(-mPipMenuOuterSpace, -mPipMenuOuterSpace); + menuUiBounds.bottom += mEduTextHeight; + return menuUiBounds; + } + + /** + * Update mPipFrameView's bounds according to the new pip window bounds. We can't + * make mPipFrameView match_parent, because the pip menu might contain other content around + * the pip window (e.g. edu text). + * TvPipMenuView needs to account for this so that it can draw a white border around the whole + * pip menu when it gains focus. + */ + private void updatePipFrameBounds() { + final ViewGroup.LayoutParams pipFrameParams = mPipFrameView.getLayoutParams(); + if (pipFrameParams != null) { + pipFrameParams.width = mCurrentPipBounds.width() + 2 * mPipMenuBorderWidth; + pipFrameParams.height = mCurrentPipBounds.height() + 2 * mPipMenuBorderWidth; + mPipFrameView.setLayoutParams(pipFrameParams); + } + + final ViewGroup.LayoutParams pipViewParams = mPipView.getLayoutParams(); + if (pipViewParams != null) { + pipViewParams.width = mCurrentPipBounds.width(); + pipViewParams.height = mCurrentPipBounds.height(); + mPipView.setLayoutParams(pipViewParams); + } + + + } + void setListener(@Nullable Listener listener) { mListener = listener; }