From 832fafb0bbfa95c3646128ed7a8d15cf296ce08e Mon Sep 17 00:00:00 2001 From: Galia Peycheva Date: Wed, 24 Aug 2022 16:00:45 +0200 Subject: [PATCH] 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 --- .../Shell/res/layout/tv_pip_menu.xml | 21 +- .../Shell/res/values-tvdpi/dimen.xml | 8 +- .../Shell/res/values/strings_tv.xml | 4 +- .../wm/shell/pip/tv/TvPipController.java | 31 +- .../wm/shell/pip/tv/TvPipMenuController.java | 64 ++-- .../shell/pip/tv/TvPipMenuEduTextDrawer.java | 280 ++++++++++++++++++ .../wm/shell/pip/tv/TvPipMenuView.java | 163 ++++------ 7 files changed, 382 insertions(+), 189 deletions(-) create mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java diff --git a/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml index afd3aac9b4615..70755e6cc3cfc 100644 --- a/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml +++ b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml @@ -119,7 +119,7 @@ - - - + android:paddingBottom="@dimen/pip_menu_border_width" + android:paddingTop="@dimen/pip_menu_border_width"/> 24dp 9sp 14sp - 10500 - 1000 - 300 + 2 + 10500 + 2000 + 1000 + 300 diff --git a/libs/WindowManager/Shell/res/values/strings_tv.xml b/libs/WindowManager/Shell/res/values/strings_tv.xml index 2b7a13eac6ca1..8f806cf56c9b0 100644 --- a/libs/WindowManager/Shell/res/values/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values/strings_tv.xml @@ -42,8 +42,8 @@ - Double press HOME for - controls + Double press HOME for + controls Picture-in-Picture menu. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java index 85a544b2e8de7..8e122a813d011 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java @@ -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) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java index 176fdfee25129..ab7edbfaa4ca3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java @@ -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(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java new file mode 100644 index 0000000000000..6eef22562caa6 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java @@ -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(); + } + +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java index 9cd05b0eea75b..57e95c416b3c4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java @@ -25,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 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();