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
This commit is contained in:
45
libs/WindowManager/Shell/res/drawable/home_icon.xml
Normal file
45
libs/WindowManager/Shell/res/drawable/home_icon.xml
Normal file
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:gravity="center">
|
||||
<item android:gravity="center">
|
||||
<shape
|
||||
android:shape="oval">
|
||||
<stroke
|
||||
android:color="@color/tv_pip_edu_text_home_icon"
|
||||
android:width="1sp" />
|
||||
<solid android:color="@android:color/transparent" />
|
||||
<size
|
||||
android:width="@dimen/pip_menu_edu_text_home_icon_outline"
|
||||
android:height="@dimen/pip_menu_edu_text_home_icon_outline"/>
|
||||
</shape>
|
||||
</item>
|
||||
<item
|
||||
android:width="@dimen/pip_menu_edu_text_home_icon"
|
||||
android:height="@dimen/pip_menu_edu_text_home_icon"
|
||||
android:gravity="center">
|
||||
<vector
|
||||
android:width="24sp"
|
||||
android:height="24sp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@color/tv_pip_edu_text_home_icon"
|
||||
android:pathData="M12,3L4,9v12h5v-7h6v7h5V9z"/>
|
||||
</vector>
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -21,22 +21,36 @@
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center|top">
|
||||
|
||||
<View
|
||||
android:id="@+id/tv_pip"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="@dimen/pip_menu_outer_space"
|
||||
android:layout_marginStart="@dimen/pip_menu_outer_space"
|
||||
android:layout_marginEnd="@dimen/pip_menu_outer_space"/>
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/tv_pip_menu_scroll"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_horizontal"
|
||||
android:layout_alignTop="@+id/tv_pip"
|
||||
android:layout_alignStart="@+id/tv_pip"
|
||||
android:layout_alignEnd="@+id/tv_pip"
|
||||
android:layout_alignBottom="@+id/tv_pip"
|
||||
android:scrollbars="none"
|
||||
android:layout_margin="@dimen/pip_menu_outer_space"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<HorizontalScrollView
|
||||
android:id="@+id/tv_pip_menu_horizontal_scroll"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_alignTop="@+id/tv_pip"
|
||||
android:layout_alignStart="@+id/tv_pip"
|
||||
android:layout_alignEnd="@+id/tv_pip"
|
||||
android:layout_alignBottom="@+id/tv_pip"
|
||||
android:gravity="center_vertical"
|
||||
android:scrollbars="none"
|
||||
android:layout_margin="@dimen/pip_menu_outer_space">
|
||||
android:scrollbars="none">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/tv_pip_menu_action_buttons"
|
||||
@@ -89,6 +103,41 @@
|
||||
</LinearLayout>
|
||||
</HorizontalScrollView>
|
||||
|
||||
<View
|
||||
android:id="@+id/tv_pip_border"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="@dimen/pip_menu_outer_space_frame"
|
||||
android:layout_marginStart="@dimen/pip_menu_outer_space_frame"
|
||||
android:layout_marginEnd="@dimen/pip_menu_outer_space_frame"
|
||||
android:background="@drawable/tv_pip_menu_border"/>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/tv_pip_menu_edu_text_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_below="@+id/tv_pip"
|
||||
android:layout_alignBottom="@+id/tv_pip_menu_frame"
|
||||
android:layout_alignStart="@+id/tv_pip"
|
||||
android:layout_alignEnd="@+id/tv_pip"
|
||||
android:background="@color/tv_pip_menu_background"
|
||||
android:clipChildren="true">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_pip_menu_edu_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="@dimen/pip_menu_edu_text_view_height"
|
||||
android:layout_gravity="bottom|center"
|
||||
android:gravity="center"
|
||||
android:paddingBottom="@dimen/pip_menu_border_width"
|
||||
android:text="@string/pip_edu_text"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="marquee"
|
||||
android:marqueeRepeatLimit="1"
|
||||
android:scrollHorizontally="true"
|
||||
android:textAppearance="@style/TvPipEduText"/>
|
||||
</FrameLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/tv_pip_menu_frame"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -37,5 +37,12 @@
|
||||
<dimen name="pip_menu_arrow_elevation">5dp</dimen>
|
||||
|
||||
<dimen name="pip_menu_elevation">1dp</dimen>
|
||||
|
||||
<dimen name="pip_menu_edu_text_view_height">24dp</dimen>
|
||||
<dimen name="pip_menu_edu_text_home_icon">9sp</dimen>
|
||||
<dimen name="pip_menu_edu_text_home_icon_outline">14sp</dimen>
|
||||
<integer name="pip_edu_text_show_duration_ms">10500</integer>
|
||||
<integer name="pip_edu_text_window_exit_animation_duration_ms">1000</integer>
|
||||
<integer name="pip_edu_text_view_exit_animation_duration_ms">300</integer>
|
||||
</resources>
|
||||
|
||||
|
||||
@@ -24,4 +24,7 @@
|
||||
<color name="tv_pip_menu_icon_bg_unfocused">#990E0E0F</color>
|
||||
<color name="tv_pip_menu_focus_border">#E8EAED</color>
|
||||
<color name="tv_pip_menu_background">#1E232C</color>
|
||||
|
||||
<color name="tv_pip_edu_text">#99D2E3FC</color>
|
||||
<color name="tv_pip_edu_text_home_icon">#D2E3FC</color>
|
||||
</resources>
|
||||
|
||||
@@ -47,4 +47,13 @@
|
||||
<item name="android:layout_width">96dp</item>
|
||||
<item name="android:layout_height">48dp</item>
|
||||
</style>
|
||||
|
||||
<style name="TvPipEduText">
|
||||
<item name="android:fontFamily">@*android:string/config_headlineFontFamilyMedium</item>
|
||||
<item name="android:textAllCaps">true</item>
|
||||
<item name="android:textSize">10sp</item>
|
||||
<item name="android:lineSpacingExtra">4sp</item>
|
||||
<item name="android:lineHeight">16sp</item>
|
||||
<item name="android:textColor">@color/tv_pip_edu_text</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -95,8 +95,12 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) {
|
||||
private var lastAreasOverlappingUnstashPosition: Set<Rect> = 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
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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<TvPipMenuActionButton> 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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user