Fix TV pip menu edu text hidden too soon

The edu text takes different amount of time to marquee through
depending on the device language. Previously we would marquee only once
and wait a constant amount of time (10.5s). This is not enough for
some languages.

In this CL we marquee the edu text twice and we wait until the text has
fully shown twice before hiding it away.

This CL moves the edu text logic in a TvPipMenuEduTextDrawer class.
It is responsible for showing the edu text, starting the text scroll
animation, scheduling when to close the edu text drawer and handling
close on demand (when the user interacts with the pip menu)

Bug: 233916208
Test: m && check edu text shows twice for different languages
Change-Id: I97efe446d4bfc71c3c91ca6d86626cb54364a79f
This commit is contained in:
Galia Peycheva
2022-08-24 16:00:45 +02:00
parent d50b929aef
commit 832fafb0bb
7 changed files with 382 additions and 189 deletions

View File

@@ -119,7 +119,7 @@
<!-- Temporarily extending the background to show an edu text hint for opening the menu -->
<FrameLayout
android:id="@+id/tv_pip_menu_edu_text_container"
android:id="@+id/tv_pip_menu_edu_text_drawer_placeholder"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/tv_pip"
@@ -127,23 +127,8 @@
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:clickable="false"
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>
android:paddingBottom="@dimen/pip_menu_border_width"
android:paddingTop="@dimen/pip_menu_border_width"/>
<!-- Frame around the PiP content + edu text hint - used to highlight open menu -->
<View

View File

@@ -41,8 +41,10 @@
<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>
<integer name="pip_edu_text_scroll_times">2</integer>
<integer name="pip_edu_text_non_scroll_show_duration">10500</integer>
<integer name="pip_edu_text_start_scroll_delay">2000</integer>
<integer name="pip_edu_text_window_exit_animation_duration">1000</integer>
<integer name="pip_edu_text_view_exit_animation_duration">300</integer>
</resources>

View File

@@ -42,8 +42,8 @@
<!-- Educative text instructing the user to double press the HOME button to access the pip
controls menu [CHAR LIMIT=50] -->
<string name="pip_edu_text"> Double press <annotation icon="home_icon"> HOME </annotation> for
controls </string>
<string name="pip_edu_text">Double press <annotation icon="home_icon">HOME</annotation> for
controls</string>
<!-- Accessibility announcement when opening the PiP menu. [CHAR LIMIT=NONE] -->
<string name="a11y_pip_menu_entered">Picture-in-Picture menu.</string>

View File

@@ -127,7 +127,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
private int mPipForceCloseDelay;
private int mResizeAnimationDuration;
private int mEduTextWindowExitAnimationDurationMs;
private int mEduTextWindowExitAnimationDuration;
public static Pip create(
Context context,
@@ -377,10 +377,10 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
}
@Override
public void onPipTargetBoundsChange(Rect newTargetBounds, int animationDuration) {
mPipTaskOrganizer.scheduleAnimateResizePip(newTargetBounds,
public void onPipTargetBoundsChange(Rect targetBounds, int animationDuration) {
mPipTaskOrganizer.scheduleAnimateResizePip(targetBounds,
animationDuration, rect -> mTvPipMenuController.updateExpansionState());
mTvPipMenuController.onPipTransitionStarted(newTargetBounds);
mTvPipMenuController.onPipTransitionToTargetBoundsStarted(targetBounds);
}
/**
@@ -417,7 +417,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
@Override
public void closeEduText() {
updatePinnedStackBounds(mEduTextWindowExitAnimationDurationMs, false);
updatePinnedStackBounds(mEduTextWindowExitAnimationDuration, false);
}
private void registerSessionListenerForCurrentUser() {
@@ -459,27 +459,30 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
}
@Override
public void onPipTransitionStarted(int direction, Rect pipBounds) {
public void onPipTransitionStarted(int direction, Rect currentPipBounds) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: onPipTransition_Started(), state=%s", TAG, stateToName(mState));
mTvPipMenuController.notifyPipAnimating(true);
"%s: onPipTransition_Started(), state=%s, direction=%d",
TAG, stateToName(mState), direction);
}
@Override
public void onPipTransitionCanceled(int direction) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: onPipTransition_Canceled(), state=%s", TAG, stateToName(mState));
mTvPipMenuController.notifyPipAnimating(false);
mTvPipMenuController.onPipTransitionFinished(
PipAnimationController.isInPipDirection(direction));
}
@Override
public void onPipTransitionFinished(int direction) {
if (PipAnimationController.isInPipDirection(direction) && mState == STATE_NO_PIP) {
final boolean enterPipTransition = PipAnimationController.isInPipDirection(direction);
if (enterPipTransition && mState == STATE_NO_PIP) {
setState(STATE_PIP);
}
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: onPipTransition_Finished(), state=%s", TAG, stateToName(mState));
mTvPipMenuController.notifyPipAnimating(false);
"%s: onPipTransition_Finished(), state=%s, direction=%d",
TAG, stateToName(mState), direction);
mTvPipMenuController.onPipTransitionFinished(enterPipTransition);
}
private void setState(@State int state) {
@@ -493,8 +496,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);
mEduTextWindowExitAnimationDuration =
res.getInteger(R.integer.pip_edu_text_window_exit_animation_duration);
}
private void registerTaskStackListenerCallback(TaskStackListenerImpl taskStackListener) {

View File

@@ -60,9 +60,6 @@ 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;
@@ -85,8 +82,6 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
RectF mTmpDestinationRectF = new RectF();
Matrix mMoveTransform = new Matrix();
private final Runnable mCloseEduTextRunnable = this::closeEduText;
public TvPipMenuController(Context context, TvPipBoundsState tvPipBoundsState,
SystemWindows systemWindows, PipMediaController pipMediaController,
Handler mainHandler) {
@@ -109,12 +104,6 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
pipMediaController.addActionListener(this::onMediaActionsChanged);
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);
}
void setDelegate(Delegate delegate) {
@@ -152,15 +141,17 @@ 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);
int pipEduTextHeight = mContext.getResources()
.getDimensionPixelSize(R.dimen.pip_menu_edu_text_view_height);
int pipMenuBorderWidth = mContext.getResources()
.getDimensionPixelSize(R.dimen.pip_menu_border_width);
mTvPipBoundsState.setPipMenuPermanentDecorInsets(Insets.of(-pipMenuBorderWidth,
-pipMenuBorderWidth, -pipMenuBorderWidth, -pipMenuBorderWidth));
mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.of(0, 0, 0, -pipEduTextHeight));
}
private void attachPipMenuView() {
mPipMenuView = new TvPipMenuView(mContext);
mPipMenuView.setListener(this);
mPipMenuView = new TvPipMenuView(mContext, mMainHandler, this);
setUpViewSurfaceZOrder(mPipMenuView, 1);
addPipMenuViewToSystemWindows(mPipMenuView, MENU_WINDOW_TITLE);
maybeUpdateMenuViewActions();
@@ -192,11 +183,15 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
0 /* height */), 0 /* displayId */, SHELL_ROOT_LAYER_PIP);
}
void notifyPipAnimating(boolean animating) {
mPipMenuView.setEduTextActive(!animating);
if (!animating) {
mPipMenuView.onPipTransitionFinished(mTvPipBoundsState.isTvPipExpanded());
}
void onPipTransitionFinished(boolean enterTransition) {
// There is a race between when this is called and when the last frame of the pip transition
// is drawn. To ensure that view updates are applied only when the animation has fully drawn
// and the menu view has been fully remeasured and relaid out, we add a small delay here by
// posting on the handler.
mMainHandler.post(() -> {
mPipMenuView.onPipTransitionFinished(
enterTransition, mTvPipBoundsState.isTvPipExpanded());
});
}
void showMovementMenuOnly() {
@@ -219,7 +214,6 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
if (mPipMenuView == null) {
return;
}
maybeCloseEduText();
maybeUpdateMenuViewActions();
updateExpansionState();
@@ -232,25 +226,12 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
mPipMenuView.updateBounds(mTvPipBoundsState.getBounds());
}
void onPipTransitionStarted(Rect finishBounds) {
void onPipTransitionToTargetBoundsStarted(Rect targetBounds) {
if (mPipMenuView != null) {
mPipMenuView.onPipTransitionStarted(finishBounds);
mPipMenuView.onPipTransitionToTargetBoundsStarted(targetBounds);
}
}
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);
}
@@ -332,7 +313,6 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
@Override
public void detach() {
closeMenu();
mMainHandler.removeCallbacks(mCloseEduTextRunnable);
detachPipMenu();
mLeash = null;
}
@@ -578,6 +558,12 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
mDelegate.togglePipExpansion();
}
@Override
public void onCloseEduText() {
mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.NONE);
mDelegate.closeEduText();
}
interface Delegate {
void movePipToFullscreen();

View File

@@ -0,0 +1,280 @@
/*
* 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 static android.view.Gravity.BOTTOM;
import static android.view.Gravity.CENTER;
import static android.view.View.GONE;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE;
import android.animation.ValueAnimator;
import android.content.Context;
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.text.TextUtils;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.FrameLayout;
import android.widget.FrameLayout.LayoutParams;
import android.widget.TextView;
import androidx.annotation.NonNull;
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.R;
import java.util.Arrays;
/**
* The edu text drawer shows the user a hint for how to access the Picture-in-Picture menu.
* It displays a text in a drawer below the Picture-in-Picture window. The drawer has the same
* width as the Picture-in-Picture window. Depending on the Picture-in-Picture mode, there might
* not be enough space to fit the whole educational text in the available space. In such cases we
* apply a marquee animation to the TextView inside the drawer.
*
* The drawer is shown temporarily giving the user enough time to read it, after which it slides
* shut. We show the text for a duration calculated based on whether the text is marqueed or not.
*/
class TvPipMenuEduTextDrawer extends FrameLayout {
private static final String TAG = "TvPipMenuEduTextDrawer";
private static final float MARQUEE_DP_PER_SECOND = 30; // Copy of TextView.MARQUEE_DP_PER_SECOND
private static final int MARQUEE_RESTART_DELAY = 1200; // Copy of TextView.MARQUEE_DELAY
private final float mMarqueeAnimSpeed; // pixels per ms
private final Runnable mCloseDrawerRunnable = this::closeDrawer;
private final Runnable mStartScrollEduTextRunnable = this::startScrollEduText;
private final Handler mMainHandler;
private final Listener mListener;
private final TextView mEduTextView;
TvPipMenuEduTextDrawer(@NonNull Context context, Handler mainHandler, Listener listener) {
super(context, null, 0, 0);
mListener = listener;
mMainHandler = mainHandler;
// Taken from TextView.Marquee calculation
mMarqueeAnimSpeed =
(MARQUEE_DP_PER_SECOND * context.getResources().getDisplayMetrics().density) / 1000f;
mEduTextView = new TextView(mContext);
setupDrawer();
}
private void setupDrawer() {
final int eduTextHeight = mContext.getResources().getDimensionPixelSize(
R.dimen.pip_menu_edu_text_view_height);
final int marqueeRepeatLimit = mContext.getResources()
.getInteger(R.integer.pip_edu_text_scroll_times);
mEduTextView.setLayoutParams(
new LayoutParams(MATCH_PARENT, eduTextHeight, BOTTOM | CENTER));
mEduTextView.setGravity(CENTER);
mEduTextView.setClickable(false);
mEduTextView.setText(createEduTextString());
mEduTextView.setSingleLine();
mEduTextView.setTextAppearance(R.style.TvPipEduText);
mEduTextView.setEllipsize(TextUtils.TruncateAt.MARQUEE);
mEduTextView.setMarqueeRepeatLimit(marqueeRepeatLimit);
mEduTextView.setHorizontallyScrolling(true);
mEduTextView.setHorizontalFadingEdgeEnabled(true);
mEduTextView.setSelected(false);
addView(mEduTextView);
setLayoutParams(new LayoutParams(MATCH_PARENT, eduTextHeight, CENTER));
setClipChildren(true);
}
/**
* Initializes the edu text. Should only be called once when the PiP is entered
*/
void init() {
ProtoLog.i(WM_SHELL_PICTURE_IN_PICTURE, "%s: init()", TAG);
scheduleLifecycleEvents();
}
private void scheduleLifecycleEvents() {
final int startScrollDelay = mContext.getResources().getInteger(
R.integer.pip_edu_text_start_scroll_delay);
if (isEduTextMarqueed()) {
mMainHandler.postDelayed(mStartScrollEduTextRunnable, startScrollDelay);
}
mMainHandler.postDelayed(mCloseDrawerRunnable, startScrollDelay + getEduTextShowDuration());
mEduTextView.getViewTreeObserver().addOnWindowAttachListener(
new ViewTreeObserver.OnWindowAttachListener() {
@Override
public void onWindowAttached() {
}
@Override
public void onWindowDetached() {
mEduTextView.getViewTreeObserver().removeOnWindowAttachListener(this);
mMainHandler.removeCallbacks(mStartScrollEduTextRunnable);
mMainHandler.removeCallbacks(mCloseDrawerRunnable);
}
});
}
private int getEduTextShowDuration() {
int eduTextShowDuration;
if (isEduTextMarqueed()) {
// Calculate the time it takes to fully scroll the text once: time = distance / speed
final float singleMarqueeDuration =
getMarqueeAnimEduTextLineWidth() / mMarqueeAnimSpeed;
// The TextView adds a delay between each marquee repetition. Take that into account
final float durationFromStartToStart = singleMarqueeDuration + MARQUEE_RESTART_DELAY;
// Finally, multiply by the number of times we repeat the marquee animation
eduTextShowDuration =
(int) durationFromStartToStart * mEduTextView.getMarqueeRepeatLimit();
} else {
eduTextShowDuration = mContext.getResources()
.getInteger(R.integer.pip_edu_text_non_scroll_show_duration);
}
ProtoLog.d(WM_SHELL_PICTURE_IN_PICTURE, "%s: getEduTextShowDuration(), showDuration=%d",
TAG, eduTextShowDuration);
return eduTextShowDuration;
}
/**
* Returns true if the edu text width is bigger than the width of the text view, which indicates
* that the edu text will be marqueed
*/
private boolean isEduTextMarqueed() {
final int availableWidth = (int) mEduTextView.getWidth()
- mEduTextView.getCompoundPaddingLeft()
- mEduTextView.getCompoundPaddingRight();
return availableWidth < getEduTextWidth();
}
/**
* Returns the width of a single marquee repetition of the edu text in pixels.
* This is the width from the start of the edu text to the start of the next edu
* text when it is marqueed.
*
* This is calculated based on the TextView.Marquee#start calculations
*/
private float getMarqueeAnimEduTextLineWidth() {
// When the TextView has a marquee animation, it puts a gap between the text end and the
// start of the next edu text repetition. The space is equal to a third of the TextView
// width
final float gap = mEduTextView.getWidth() / 3.0f;
return getEduTextWidth() + gap;
}
private void startScrollEduText() {
ProtoLog.d(WM_SHELL_PICTURE_IN_PICTURE, "%s: startScrollEduText(), repeat=%d",
TAG, mEduTextView.getMarqueeRepeatLimit());
mEduTextView.setSelected(true);
}
/**
* Returns the width of the edu text irrespective of the TextView width
*/
private int getEduTextWidth() {
return (int) mEduTextView.getLayout().getLineWidth(0);
}
/**
* Closes the edu text drawer if it hasn't been closed yet
*/
void closeIfNeeded() {
if (mMainHandler.hasCallbacks(mCloseDrawerRunnable)) {
ProtoLog.d(WM_SHELL_PICTURE_IN_PICTURE,
"%s: close(), closing the edu text drawer because of user action", TAG);
mMainHandler.removeCallbacks(mCloseDrawerRunnable);
mCloseDrawerRunnable.run();
} else {
// Do nothing, the drawer has already been closed
}
}
private void closeDrawer() {
ProtoLog.i(WM_SHELL_PICTURE_IN_PICTURE, "%s: closeDrawer()", TAG);
final int eduTextFadeExitAnimationDuration = mContext.getResources().getInteger(
R.integer.pip_edu_text_view_exit_animation_duration);
final int eduTextSlideExitAnimationDuration = mContext.getResources().getInteger(
R.integer.pip_edu_text_window_exit_animation_duration);
// Start fading out the edu text
mEduTextView.animate()
.alpha(0f)
.setInterpolator(TvPipInterpolators.EXIT)
.setDuration(eduTextFadeExitAnimationDuration)
.start();
// Start animation to close the drawer by animating its height to 0
final ValueAnimator heightAnimation = ValueAnimator.ofInt(getHeight(), 0);
heightAnimation.setDuration(eduTextSlideExitAnimationDuration);
heightAnimation.setInterpolator(TvPipInterpolators.BROWSE);
heightAnimation.addUpdateListener(animator -> {
final ViewGroup.LayoutParams params = getLayoutParams();
params.height = (int) animator.getAnimatedValue();
setLayoutParams(params);
if (params.height == 0) {
setVisibility(GONE);
}
});
heightAnimation.start();
mListener.onCloseEduText();
}
/**
* Creates the educational text that will be displayed to the user. Here we replace the
* HOME annotation in the String with an icon
*/
private CharSequence createEduTextString() {
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);
}
});
return spannableString;
}
/**
* A listener for edu text drawer event states.
*/
interface Listener {
/**
* The edu text closing impacts the size of the Picture-in-Picture window and influences
* how it is positioned on the screen.
*/
void onCloseEduText();
}
}

View File

@@ -25,18 +25,11 @@ 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;
import android.view.SurfaceControl;
@@ -49,7 +42,6 @@ import android.widget.HorizontalScrollView;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -61,7 +53,6 @@ import com.android.wm.shell.pip.PipUtils;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
@@ -74,21 +65,16 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
private static final int FIRST_CUSTOM_ACTION_POSITION = 3;
@Nullable
private Listener mListener;
private final Listener mListener;
private final LinearLayout mActionButtonsContainer;
private final View mMenuFrameView;
private final List<TvWindowMenuActionButton> mAdditionalButtons = new ArrayList<>();
private final View mPipFrameView;
private final View mPipView;
private final TextView mEduTextView;
private final View mEduTextContainerView;
private final TvPipMenuEduTextDrawer mEduTextDrawer;
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;
@@ -116,25 +102,17 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
private final int mResizeAnimationDuration;
private final AccessibilityManager mA11yManager;
private final Handler mMainHandler;
public TvPipMenuView(@NonNull Context context) {
this(context, null);
}
public TvPipMenuView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public TvPipMenuView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public TvPipMenuView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
public TvPipMenuView(@NonNull Context context, @NonNull Handler mainHandler,
@NonNull Listener listener) {
super(context, null, 0, 0);
inflate(context, R.layout.tv_pip_menu, this);
mMainHandler = mainHandler;
mListener = listener;
mA11yManager = context.getSystemService(AccessibilityManager.class);
mActionButtonsContainer = findViewById(R.id.tv_pip_menu_action_buttons);
@@ -166,9 +144,6 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
mArrowLeft = findViewById(R.id.tv_pip_menu_arrow_left);
mA11yDoneButton = findViewById(R.id.tv_pip_menu_done_button);
mEduTextView = findViewById(R.id.tv_pip_menu_edu_text);
mEduTextContainerView = findViewById(R.id.tv_pip_menu_edu_text_container);
mResizeAnimationDuration = context.getResources().getInteger(
R.integer.config_pipResizeAnimationDuration);
mPipMenuFadeAnimationDuration = context.getResources()
@@ -178,63 +153,18 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
.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();
mEduTextDrawer = new TvPipMenuEduTextDrawer(mContext, mainHandler, mListener);
((FrameLayout) findViewById(R.id.tv_pip_menu_edu_text_drawer_placeholder))
.addView(mEduTextDrawer);
}
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 onPipTransitionStarted(Rect finishBounds) {
void onPipTransitionToTargetBoundsStarted(Rect targetBounds) {
// Fade out content by fading in view on top.
if (mCurrentPipBounds != null && finishBounds != null) {
if (mCurrentPipBounds != null && targetBounds != null) {
boolean ratioChanged = PipUtils.aspectRatioChanged(
mCurrentPipBounds.width() / (float) mCurrentPipBounds.height(),
finishBounds.width() / (float) finishBounds.height());
targetBounds.width() / (float) targetBounds.height());
if (ratioChanged) {
mPipBackground.animate()
.alpha(1f)
@@ -245,11 +175,12 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
}
// Update buttons.
final boolean vertical = finishBounds.height() > finishBounds.width();
final boolean vertical = targetBounds.height() > targetBounds.width();
final boolean orientationChanged =
vertical != (mActionButtonsContainer.getOrientation() == LinearLayout.VERTICAL);
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: onPipTransitionStarted(), orientation changed %b", TAG, orientationChanged);
"%s: onPipTransitionToTargetBoundsStarted(), orientation changed %b",
TAG, orientationChanged);
if (!orientationChanged) {
return;
}
@@ -261,18 +192,18 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
.setInterpolator(TvPipInterpolators.EXIT)
.setDuration(mResizeAnimationDuration / 2)
.withEndAction(() -> {
changeButtonScrollOrientation(finishBounds);
updateButtonGravity(finishBounds);
changeButtonScrollOrientation(targetBounds);
updateButtonGravity(targetBounds);
// Only make buttons visible again in onPipTransitionFinished to keep in
// sync with PiP content alpha animation.
});
} else {
changeButtonScrollOrientation(finishBounds);
updateButtonGravity(finishBounds);
changeButtonScrollOrientation(targetBounds);
updateButtonGravity(targetBounds);
}
}
void onPipTransitionFinished(boolean isTvPipExpanded) {
void onPipTransitionFinished(boolean enterTransition, boolean isTvPipExpanded) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: onPipTransitionFinished()", TAG);
@@ -283,6 +214,10 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
.setInterpolator(TvPipInterpolators.ENTER)
.start();
if (enterTransition) {
mEduTextDrawer.init();
}
setIsExpanded(isTvPipExpanded);
// Update buttons.
@@ -409,7 +344,7 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
Rect getPipMenuContainerBounds(Rect pipBounds) {
final Rect menuUiBounds = new Rect(pipBounds);
menuUiBounds.inset(-mPipMenuOuterSpace, -mPipMenuOuterSpace);
menuUiBounds.bottom += mEduTextHeight;
menuUiBounds.bottom += mEduTextDrawer.getHeight();
return menuUiBounds;
}
@@ -438,10 +373,6 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
}
void setListener(@Nullable Listener listener) {
mListener = listener;
}
void setExpandedModeEnabled(boolean enabled) {
mExpandButton.setVisibility(enabled ? VISIBLE : GONE);
}
@@ -460,21 +391,19 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
*/
void showMoveMenu(int gravity) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showMoveMenu()", TAG);
mButtonMenuIsVisible = false;
mMoveMenuIsVisible = true;
showButtonsMenu(false);
showMovementHints(gravity);
setFrameHighlighted(true);
mHorizontalScrollView.setFocusable(false);
mScrollView.setFocusable(false);
mEduTextDrawer.closeIfNeeded();
}
void showButtonsMenu() {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: showButtonsMenu()", TAG);
mButtonMenuIsVisible = true;
mMoveMenuIsVisible = false;
showButtonsMenu(true);
hideMovementHints();
setFrameHighlighted(true);
@@ -501,8 +430,6 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: hideAllUserControls()", TAG);
mFocusedButton = null;
mButtonMenuIsVisible = false;
mMoveMenuIsVisible = false;
showButtonsMenu(false);
hideMovementHints();
setFrameHighlighted(false);
@@ -632,8 +559,6 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
@Override
public void onClick(View v) {
if (mListener == null) return;
final int id = v.getId();
if (id == R.id.tv_pip_menu_fullscreen_button) {
mListener.onFullscreenButtonClick();
@@ -662,7 +587,7 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (mListener != null && event.getAction() == ACTION_UP) {
if (event.getAction() == ACTION_UP) {
if (!mMoveMenuIsVisible) {
mFocusedButton = mActionButtonsContainer.getFocusedChild();
}
@@ -700,6 +625,11 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: showMovementHints(), position: %s", TAG, Gravity.toString(gravity));
if (mMoveMenuIsVisible) {
return;
}
mMoveMenuIsVisible = true;
animateAlphaTo(checkGravity(gravity, Gravity.BOTTOM) ? 1f : 0f, mArrowUp);
animateAlphaTo(checkGravity(gravity, Gravity.TOP) ? 1f : 0f, mArrowDown);
animateAlphaTo(checkGravity(gravity, Gravity.RIGHT) ? 1f : 0f, mArrowLeft);
@@ -714,9 +644,7 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
animateAlphaTo(a11yEnabled ? 1f : 0f, mA11yDoneButton);
if (a11yEnabled) {
mA11yDoneButton.setOnClickListener(v -> {
if (mListener != null) {
mListener.onExitMoveMode();
}
mListener.onExitMoveMode();
});
}
}
@@ -725,9 +653,7 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
arrowView.setClickable(enabled);
if (enabled) {
arrowView.setOnClickListener(v -> {
if (mListener != null) {
mListener.onPipMovement(keycode);
}
mListener.onPipMovement(keycode);
});
}
}
@@ -743,6 +669,11 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: hideMovementHints()", TAG);
if (!mMoveMenuIsVisible) {
return;
}
mMoveMenuIsVisible = false;
animateAlphaTo(0, mArrowUp);
animateAlphaTo(0, mArrowRight);
animateAlphaTo(0, mArrowDown);
@@ -756,19 +687,25 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
public void showButtonsMenu(boolean show) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: showUserActions: %b", TAG, show);
if (mButtonMenuIsVisible == show) {
return;
}
mButtonMenuIsVisible = show;
if (show) {
mActionButtonsContainer.setVisibility(VISIBLE);
refocusPreviousButton();
}
animateAlphaTo(show ? 1 : 0, mActionButtonsContainer);
animateAlphaTo(show ? 1 : 0, mDimLayer);
mEduTextDrawer.closeIfNeeded();
}
private void setFrameHighlighted(boolean highlighted) {
mMenuFrameView.setActivated(highlighted);
}
interface Listener {
interface Listener extends TvPipMenuEduTextDrawer.Listener {
void onBackPress();