Merge "Add edu text to TV PiP" into tm-dev am: 49f5f057b6 am: 8973dc6a65

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/17378167

Change-Id: I101a1b69a759462debb4c81449e3db6e9c488b8e
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Galia Peycheva
2022-04-05 06:39:21 +00:00
committed by Automerger Merge Worker
12 changed files with 421 additions and 44 deletions

View 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>

View File

@@ -21,22 +21,36 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:gravity="center|top"> 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 <ScrollView
android:id="@+id/tv_pip_menu_scroll" android:id="@+id/tv_pip_menu_scroll"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:gravity="center_horizontal" 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:scrollbars="none"
android:layout_margin="@dimen/pip_menu_outer_space"
android:visibility="gone"/> android:visibility="gone"/>
<HorizontalScrollView <HorizontalScrollView
android:id="@+id/tv_pip_menu_horizontal_scroll" android:id="@+id/tv_pip_menu_horizontal_scroll"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="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:gravity="center_vertical"
android:scrollbars="none" android:scrollbars="none">
android:layout_margin="@dimen/pip_menu_outer_space">
<LinearLayout <LinearLayout
android:id="@+id/tv_pip_menu_action_buttons" android:id="@+id/tv_pip_menu_action_buttons"
@@ -89,6 +103,41 @@
</LinearLayout> </LinearLayout>
</HorizontalScrollView> </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 <View
android:id="@+id/tv_pip_menu_frame" android:id="@+id/tv_pip_menu_frame"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@@ -37,5 +37,12 @@
<dimen name="pip_menu_arrow_elevation">5dp</dimen> <dimen name="pip_menu_arrow_elevation">5dp</dimen>
<dimen name="pip_menu_elevation">1dp</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> </resources>

View File

@@ -24,4 +24,7 @@
<color name="tv_pip_menu_icon_bg_unfocused">#990E0E0F</color> <color name="tv_pip_menu_icon_bg_unfocused">#990E0E0F</color>
<color name="tv_pip_menu_focus_border">#E8EAED</color> <color name="tv_pip_menu_focus_border">#E8EAED</color>
<color name="tv_pip_menu_background">#1E232C</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> </resources>

View File

@@ -47,4 +47,13 @@
<item name="android:layout_width">96dp</item> <item name="android:layout_width">96dp</item>
<item name="android:layout_height">48dp</item> <item name="android:layout_height">48dp</item>
</style> </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> </resources>

View File

@@ -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;
}
}

View File

@@ -151,7 +151,10 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm {
mKeepClearAlgorithm.setScreenSize(screenSize); mKeepClearAlgorithm.setScreenSize(screenSize);
mKeepClearAlgorithm.setMovementBounds(insetBounds); mKeepClearAlgorithm.setMovementBounds(insetBounds);
mKeepClearAlgorithm.setStashOffset(mTvPipBoundsState.getStashOffset()); mKeepClearAlgorithm.setStashOffset(mTvPipBoundsState.getStashOffset());
mKeepClearAlgorithm.setPipDecorInsets(mTvPipBoundsState.getPipMenuPermanentDecorInsets()); mKeepClearAlgorithm.setPipPermanentDecorInsets(
mTvPipBoundsState.getPipMenuPermanentDecorInsets());
mKeepClearAlgorithm.setPipTemporaryDecorInsets(
mTvPipBoundsState.getPipMenuTemporaryDecorInsets());
final Placement placement = mKeepClearAlgorithm.calculatePipPosition( final Placement placement = mKeepClearAlgorithm.calculatePipPosition(
pipSize, pipSize,

View File

@@ -62,6 +62,7 @@ public class TvPipBoundsState extends PipBoundsState {
private int mTvPipGravity; private int mTvPipGravity;
private @Nullable Size mTvExpandedSize; private @Nullable Size mTvExpandedSize;
private @NonNull Insets mPipMenuPermanentDecorInsets = Insets.NONE; private @NonNull Insets mPipMenuPermanentDecorInsets = Insets.NONE;
private @NonNull Insets mPipMenuTemporaryDecorInsets = Insets.NONE;
public TvPipBoundsState(@NonNull Context context) { public TvPipBoundsState(@NonNull Context context) {
super(context); super(context);
@@ -167,4 +168,12 @@ public class TvPipBoundsState extends PipBoundsState {
public @NonNull Insets getPipMenuPermanentDecorInsets() { public @NonNull Insets getPipMenuPermanentDecorInsets() {
return mPipMenuPermanentDecorInsets; return mPipMenuPermanentDecorInsets;
} }
public void setPipMenuTemporaryDecorInsets(@NonNull Insets temporaryDecorInsets) {
mPipMenuTemporaryDecorInsets = temporaryDecorInsets;
}
public @NonNull Insets getPipMenuTemporaryDecorInsets() {
return mPipMenuTemporaryDecorInsets;
}
} }

View File

@@ -115,6 +115,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
private int mPipForceCloseDelay; private int mPipForceCloseDelay;
private int mResizeAnimationDuration; private int mResizeAnimationDuration;
private int mEduTextWindowExitAnimationDurationMs;
public static Pip create( public static Pip create(
Context context, 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. * 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. * 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) { if (mState == STATE_NO_PIP) {
return; return;
} }
@@ -353,23 +358,26 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
mUnstashRunnable = null; mUnstashRunnable = null;
} }
if (!disallowStashing && placement.getUnstashDestinationBounds() != null) { if (!disallowStashing && placement.getUnstashDestinationBounds() != null) {
mUnstashRunnable = () -> movePinnedStackTo(placement.getUnstashDestinationBounds()); mUnstashRunnable = () -> {
movePinnedStackTo(placement.getUnstashDestinationBounds(), animationDuration);
};
mMainHandler.postAtTime(mUnstashRunnable, placement.getUnstashTime()); mMainHandler.postAtTime(mUnstashRunnable, placement.getUnstashTime());
} }
} }
/** Animates the PiP to the given bounds. */ /** Animates the PiP to the given bounds. */
private void movePinnedStackTo(Rect 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) { if (DEBUG) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: movePinnedStack() - new pip bounds: %s", TAG, bounds.toShortString()); "%s: movePinnedStack() - new pip bounds: %s", TAG, bounds.toShortString());
} }
mPipTaskOrganizer.scheduleAnimateResizePip(bounds, mPipTaskOrganizer.scheduleAnimateResizePip(bounds,
mResizeAnimationDuration, rect -> { animationDuration, rect -> {
if (DEBUG) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: movePinnedStack() animation done", TAG);
}
mTvPipMenuController.updateExpansionState(); mTvPipMenuController.updateExpansionState();
}); });
} }
@@ -408,6 +416,11 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
onPipDisappeared(); onPipDisappeared();
} }
@Override
public void closeEduText() {
updatePinnedStackBounds(mEduTextWindowExitAnimationDurationMs);
}
private void registerSessionListenerForCurrentUser() { private void registerSessionListenerForCurrentUser() {
mPipMediaController.registerSessionListenerForCurrentUser(); mPipMediaController.registerSessionListenerForCurrentUser();
} }
@@ -457,6 +470,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: onPipTransition_Started(), state=%s", TAG, stateToName(mState)); "%s: onPipTransition_Started(), state=%s", TAG, stateToName(mState));
} }
mTvPipMenuController.notifyPipAnimating(true);
} }
@Override @Override
@@ -465,6 +479,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: onPipTransition_Canceled(), state=%s", TAG, stateToName(mState)); "%s: onPipTransition_Canceled(), state=%s", TAG, stateToName(mState));
} }
mTvPipMenuController.notifyPipAnimating(false);
} }
@Override @Override
@@ -476,6 +491,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: onPipTransition_Finished(), state=%s", TAG, stateToName(mState)); "%s: onPipTransition_Finished(), state=%s", TAG, stateToName(mState));
} }
mTvPipMenuController.notifyPipAnimating(false);
} }
private void setState(@State int state) { private void setState(@State int state) {
@@ -491,6 +507,8 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
final Resources res = mContext.getResources(); final Resources res = mContext.getResources();
mResizeAnimationDuration = res.getInteger(R.integer.config_pipResizeAnimationDuration); mResizeAnimationDuration = res.getInteger(R.integer.config_pipResizeAnimationDuration);
mPipForceCloseDelay = res.getInteger(R.integer.config_pipForceCloseDelay); 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) { private void registerTaskStackListenerCallback(TaskStackListenerImpl taskStackListener) {

View File

@@ -95,8 +95,12 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) {
private var lastAreasOverlappingUnstashPosition: Set<Rect> = emptySet() private var lastAreasOverlappingUnstashPosition: Set<Rect> = emptySet()
private var lastStashTime: Long = Long.MIN_VALUE private var lastStashTime: Long = Long.MIN_VALUE
/** Spaces around the PiP that we should leave space for when placing the PiP */ /** Spaces around the PiP that we should leave space for when placing the PiP. Permanent PiP
private var pipDecorInsets = Insets.NONE * 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 * 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 transformedRestrictedAreas = transformAndFilterAreas(restrictedAreas)
val transformedUnrestrictedAreas = transformAndFilterAreas(unrestrictedAreas) val transformedUnrestrictedAreas = transformAndFilterAreas(unrestrictedAreas)
val pipSizeWithDecors = addDecors(pipSize) val pipSizeWithAllDecors = addDecors(pipSize)
val pipAnchorBounds = getNormalPipAnchorBounds(pipSizeWithDecors, transformedMovementBounds) val pipAnchorBoundsWithAllDecors =
getNormalPipAnchorBounds(pipSizeWithAllDecors, transformedMovementBounds)
val pipAnchorBoundsWithPermanentDecors = removeTemporaryDecors(pipAnchorBoundsWithAllDecors)
val result = calculatePipPositionTransformed( val result = calculatePipPositionTransformed(
pipAnchorBounds, pipAnchorBoundsWithPermanentDecors,
transformedRestrictedAreas, transformedRestrictedAreas,
transformedUnrestrictedAreas transformedUnrestrictedAreas
) )
val pipBounds = removeDecors(fromTransformedSpace(result.bounds)) val pipBounds = removePermanentDecors(fromTransformedSpace(result.bounds))
val anchorBounds = removeDecors(fromTransformedSpace(result.anchorBounds)) val anchorBounds = removePermanentDecors(fromTransformedSpace(result.anchorBounds))
val unstashedDestBounds = result.unstashDestinationBounds?.let { val unstashedDestBounds = result.unstashDestinationBounds?.let {
removeDecors(fromTransformedSpace(it)) removePermanentDecors(fromTransformedSpace(it))
} }
return Placement( return Placement(
@@ -458,9 +464,14 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) {
transformedMovementBounds = toTransformedSpace(movementBounds) transformedMovementBounds = toTransformedSpace(movementBounds)
} }
fun setPipDecorInsets(insets: Insets) { fun setPipPermanentDecorInsets(insets: Insets) {
if (pipDecorInsets == insets) return if (pipPermanentDecorInsets == insets) return
pipDecorInsets = insets 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 { private fun addDecors(size: Size): Size {
val bounds = Rect(0, 0, size.width, size.height) val bounds = Rect(0, 0, size.width, size.height)
bounds.inset(pipDecorInsets) bounds.inset(pipPermanentDecorInsets)
bounds.inset(pipTemporaryDecorInsets)
return Size(bounds.width(), bounds.height()) 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 { private fun removePermanentDecors(bounds: Rect): Rect {
val pipDecorReverseInsets = Insets.subtract(Insets.NONE, pipDecorInsets) 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) bounds.inset(pipDecorReverseInsets)
return bounds return bounds
} }

View File

@@ -63,6 +63,9 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
private final SystemWindows mSystemWindows; private final SystemWindows mSystemWindows;
private final TvPipBoundsState mTvPipBoundsState; private final TvPipBoundsState mTvPipBoundsState;
private final Handler mMainHandler; private final Handler mMainHandler;
private final int mPipMenuBorderWidth;
private final int mPipEduTextShowDurationMs;
private final int mPipEduTextHeight;
private Delegate mDelegate; private Delegate mDelegate;
private SurfaceControl mLeash; private SurfaceControl mLeash;
@@ -98,7 +101,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
if (DEBUG) e.printStackTrace(); if (DEBUG) e.printStackTrace();
} }
}; };
private final int mPipMenuSurfaceOuterSpace; private final Runnable mCloseEduTextRunnable = this::closeEduText;
public TvPipMenuController(Context context, TvPipBoundsState tvPipBoundsState, public TvPipMenuController(Context context, TvPipBoundsState tvPipBoundsState,
SystemWindows systemWindows, PipMediaController pipMediaController, SystemWindows systemWindows, PipMediaController pipMediaController,
@@ -122,13 +125,12 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
pipMediaController.addActionListener(this::onMediaActionsChanged); pipMediaController.addActionListener(this::onMediaActionsChanged);
mPipMenuSurfaceOuterSpace = context.getResources() mPipEduTextShowDurationMs = context.getResources()
.getDimensionPixelSize(R.dimen.pip_menu_outer_space); .getInteger(R.integer.pip_edu_text_show_duration_ms);
mPipEduTextHeight = context.getResources()
final int pipBorderWidth = context.getResources() .getDimensionPixelSize(R.dimen.pip_menu_edu_text_view_height);
mPipMenuBorderWidth = context.getResources()
.getDimensionPixelSize(R.dimen.pip_menu_border_width); .getDimensionPixelSize(R.dimen.pip_menu_border_width);
mTvPipBoundsState.setPipMenuPermanentDecorInsets(Insets.of(-pipBorderWidth,
-pipBorderWidth, -pipBorderWidth, -pipBorderWidth));
} }
void setDelegate(Delegate delegate) { void setDelegate(Delegate delegate) {
@@ -169,6 +171,11 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
attachPipBackgroundView(); attachPipBackgroundView();
attachPipMenuView(); attachPipMenuView();
mTvPipBoundsState.setPipMenuPermanentDecorInsets(Insets.of(-mPipMenuBorderWidth,
-mPipMenuBorderWidth, -mPipMenuBorderWidth, -mPipMenuBorderWidth));
mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.of(0, 0, 0, -mPipEduTextHeight));
mMainHandler.postDelayed(mCloseEduTextRunnable, mPipEduTextShowDurationMs);
} }
private void attachPipMenuView() { private void attachPipMenuView() {
@@ -204,6 +211,10 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
0 /* displayId */, SHELL_ROOT_LAYER_PIP); 0 /* displayId */, SHELL_ROOT_LAYER_PIP);
} }
void notifyPipAnimating(boolean animating) {
mPipMenuView.setEduTextActive(!animating);
}
void showMovementMenuOnly() { void showMovementMenuOnly() {
if (DEBUG) { if (DEBUG) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
@@ -228,6 +239,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
if (mPipMenuView == null) { if (mPipMenuView == null) {
return; return;
} }
maybeCloseEduText();
maybeUpdateMenuViewActions(); maybeUpdateMenuViewActions();
updateExpansionState(); 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) { void updateGravity(int gravity) {
mPipMenuView.showMovementHints(gravity); mPipMenuView.showMovementHints(gravity);
} }
@@ -250,9 +275,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
} }
private Rect calculateMenuSurfaceBounds(Rect pipBounds) { private Rect calculateMenuSurfaceBounds(Rect pipBounds) {
Rect menuBounds = new Rect(pipBounds); return mPipMenuView.getPipMenuContainerBounds(pipBounds);
menuBounds.inset(-mPipMenuSurfaceOuterSpace, -mPipMenuSurfaceOuterSpace);
return menuBounds;
} }
void closeMenu() { void closeMenu() {
@@ -321,6 +344,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
@Override @Override
public void detach() { public void detach() {
closeMenu(); closeMenu();
mMainHandler.removeCallbacks(mCloseEduTextRunnable);
detachPipMenu(); detachPipMenu();
mLeash = null; mLeash = null;
} }
@@ -598,6 +622,8 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
void onMenuClosed(); void onMenuClosed();
void closeEduText();
void closePip(); void closePip();
} }

View File

@@ -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_DPAD_UP;
import static android.view.KeyEvent.KEYCODE_ENTER; import static android.view.KeyEvent.KEYCODE_ENTER;
import android.animation.ValueAnimator;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.app.RemoteAction; import android.app.RemoteAction;
import android.content.Context; import android.content.Context;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Handler; 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.util.AttributeSet;
import android.view.Gravity; import android.view.Gravity;
import android.view.KeyEvent; import android.view.KeyEvent;
@@ -40,6 +46,7 @@ import android.view.ViewRootImpl;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@@ -49,6 +56,7 @@ import com.android.wm.shell.R;
import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.protolog.ShellProtoLogGroup;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
@@ -67,6 +75,15 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
private final LinearLayout mActionButtonsContainer; private final LinearLayout mActionButtonsContainer;
private final View mMenuFrameView; private final View mMenuFrameView;
private final List<TvPipMenuActionButton> mAdditionalButtons = new ArrayList<>(); 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 mArrowUp;
private final ImageView mArrowRight; private final ImageView mArrowRight;
@@ -76,7 +93,7 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
private final ViewGroup mScrollView; private final ViewGroup mScrollView;
private final ViewGroup mHorizontalScrollView; private final ViewGroup mHorizontalScrollView;
private Rect mCurrentBounds; private Rect mCurrentPipBounds;
private final TvPipMenuActionButton mExpandButton; private final TvPipMenuActionButton mExpandButton;
private final TvPipMenuActionButton mCloseButton; 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); mHorizontalScrollView = findViewById(R.id.tv_pip_menu_horizontal_scroll);
mMenuFrameView = findViewById(R.id.tv_pip_menu_frame); 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); mArrowUp = findViewById(R.id.tv_pip_menu_arrow_up);
mArrowRight = findViewById(R.id.tv_pip_menu_arrow_right); mArrowRight = findViewById(R.id.tv_pip_menu_arrow_right);
mArrowDown = findViewById(R.id.tv_pip_menu_arrow_down); mArrowDown = findViewById(R.id.tv_pip_menu_arrow_down);
mArrowLeft = findViewById(R.id.tv_pip_menu_arrow_left); 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() mPipMenuFadeAnimationDuration = context.getResources()
.getInteger(R.integer.pip_menu_fade_animation_duration); .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) { void initEduText() {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, final SpannedString eduText = (SpannedString) getResources().getText(R.string.pip_edu_text);
"%s: update menu layout: %s", TAG, updatedBounds.toShortString()); final SpannableString spannableString = new SpannableString(eduText);
boolean previouslyVertical = Arrays.stream(eduText.getSpans(0, eduText.length(), Annotation.class)).findFirst()
mCurrentBounds != null && mCurrentBounds.height() > mCurrentBounds.width(); .ifPresent(annotation -> {
boolean vertical = updatedBounds.height() > updatedBounds.width(); 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 (previouslyVertical == vertical) {
if (DEBUG) { if (DEBUG) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, 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); 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) { void setListener(@Nullable Listener listener) {
mListener = listener; mListener = listener;
} }