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 4c262249dfa25..3e8de454bcff6 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, @@ -371,10 +371,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); } /** @@ -411,7 +411,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal @Override public void closeEduText() { - updatePinnedStackBounds(mEduTextWindowExitAnimationDurationMs, false); + updatePinnedStackBounds(mEduTextWindowExitAnimationDuration, false); } private void registerSessionListenerForCurrentUser() { @@ -453,27 +453,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) { @@ -487,8 +490,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();