Loading libs/WindowManager/Shell/res/layout/tv_pip_menu.xml +3 −18 Original line number Diff line number Diff line Loading @@ -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" Loading @@ -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:paddingTop="@dimen/pip_menu_border_width"/> <!-- Frame around the PiP content + edu text hint - used to highlight open menu --> <View Loading libs/WindowManager/Shell/res/values-tvdpi/dimen.xml +5 −3 Original line number Diff line number Diff line Loading @@ -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> libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java +17 −14 Original line number Diff line number Diff line Loading @@ -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, Loading Loading @@ -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); } /** Loading Loading @@ -411,7 +411,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal @Override public void closeEduText() { updatePinnedStackBounds(mEduTextWindowExitAnimationDurationMs, false); updatePinnedStackBounds(mEduTextWindowExitAnimationDuration, false); } private void registerSessionListenerForCurrentUser() { Loading Loading @@ -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) { Loading @@ -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) { Loading libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java +25 −39 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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) { Loading @@ -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) { Loading Loading @@ -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(); Loading Loading @@ -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() { Loading @@ -219,7 +214,6 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis if (mPipMenuView == null) { return; } maybeCloseEduText(); maybeUpdateMenuViewActions(); updateExpansionState(); Loading @@ -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); } } private void maybeCloseEduText() { if (mMainHandler.hasCallbacks(mCloseEduTextRunnable)) { mMainHandler.removeCallbacks(mCloseEduTextRunnable); mCloseEduTextRunnable.run(); mPipMenuView.onPipTransitionToTargetBoundsStarted(targetBounds); } } private void closeEduText() { mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.NONE); mPipMenuView.hideEduText(); mDelegate.closeEduText(); } void updateGravity(int gravity) { mPipMenuView.showMovementHints(gravity); } Loading Loading @@ -332,7 +313,6 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis @Override public void detach() { closeMenu(); mMainHandler.removeCallbacks(mCloseEduTextRunnable); detachPipMenu(); mLeash = null; } Loading Loading @@ -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(); Loading libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java 0 → 100644 +280 −0 Original line number Diff line number Diff line /* * 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(); } } Loading
libs/WindowManager/Shell/res/layout/tv_pip_menu.xml +3 −18 Original line number Diff line number Diff line Loading @@ -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" Loading @@ -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:paddingTop="@dimen/pip_menu_border_width"/> <!-- Frame around the PiP content + edu text hint - used to highlight open menu --> <View Loading
libs/WindowManager/Shell/res/values-tvdpi/dimen.xml +5 −3 Original line number Diff line number Diff line Loading @@ -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>
libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java +17 −14 Original line number Diff line number Diff line Loading @@ -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, Loading Loading @@ -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); } /** Loading Loading @@ -411,7 +411,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal @Override public void closeEduText() { updatePinnedStackBounds(mEduTextWindowExitAnimationDurationMs, false); updatePinnedStackBounds(mEduTextWindowExitAnimationDuration, false); } private void registerSessionListenerForCurrentUser() { Loading Loading @@ -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) { Loading @@ -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) { Loading
libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java +25 −39 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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) { Loading @@ -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) { Loading Loading @@ -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(); Loading Loading @@ -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() { Loading @@ -219,7 +214,6 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis if (mPipMenuView == null) { return; } maybeCloseEduText(); maybeUpdateMenuViewActions(); updateExpansionState(); Loading @@ -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); } } private void maybeCloseEduText() { if (mMainHandler.hasCallbacks(mCloseEduTextRunnable)) { mMainHandler.removeCallbacks(mCloseEduTextRunnable); mCloseEduTextRunnable.run(); mPipMenuView.onPipTransitionToTargetBoundsStarted(targetBounds); } } private void closeEduText() { mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.NONE); mPipMenuView.hideEduText(); mDelegate.closeEduText(); } void updateGravity(int gravity) { mPipMenuView.showMovementHints(gravity); } Loading Loading @@ -332,7 +313,6 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis @Override public void detach() { closeMenu(); mMainHandler.removeCallbacks(mCloseEduTextRunnable); detachPipMenu(); mLeash = null; } Loading Loading @@ -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(); Loading
libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java 0 → 100644 +280 −0 Original line number Diff line number Diff line /* * 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(); } }