Merge "Fix TV pip menu edu text hidden too soon"
This commit is contained in:
committed by
Android (Google) Code Review
commit
69079b467d
@@ -119,7 +119,7 @@
|
||||
|
||||
<!-- Temporarily extending the background to show an edu text hint for opening the menu -->
|
||||
<FrameLayout
|
||||
android:id="@+id/tv_pip_menu_edu_text_container"
|
||||
android:id="@+id/tv_pip_menu_edu_text_drawer_placeholder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_below="@+id/tv_pip"
|
||||
@@ -127,23 +127,8 @@
|
||||
android:layout_alignStart="@+id/tv_pip"
|
||||
android:layout_alignEnd="@+id/tv_pip"
|
||||
android:background="@color/tv_pip_menu_background"
|
||||
android:clipChildren="true">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_pip_menu_edu_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="@dimen/pip_menu_edu_text_view_height"
|
||||
android:layout_gravity="bottom|center"
|
||||
android:gravity="center"
|
||||
android:clickable="false"
|
||||
android:paddingBottom="@dimen/pip_menu_border_width"
|
||||
android:text="@string/pip_edu_text"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="marquee"
|
||||
android:marqueeRepeatLimit="1"
|
||||
android:scrollHorizontally="true"
|
||||
android:textAppearance="@style/TvPipEduText"/>
|
||||
</FrameLayout>
|
||||
android:paddingBottom="@dimen/pip_menu_border_width"
|
||||
android:paddingTop="@dimen/pip_menu_border_width"/>
|
||||
|
||||
<!-- Frame around the PiP content + edu text hint - used to highlight open menu -->
|
||||
<View
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -42,8 +42,8 @@
|
||||
|
||||
<!-- Educative text instructing the user to double press the HOME button to access the pip
|
||||
controls menu [CHAR LIMIT=50] -->
|
||||
<string name="pip_edu_text"> Double press <annotation icon="home_icon"> HOME </annotation> for
|
||||
controls </string>
|
||||
<string name="pip_edu_text">Double press <annotation icon="home_icon">HOME</annotation> for
|
||||
controls</string>
|
||||
|
||||
<!-- Accessibility announcement when opening the PiP menu. [CHAR LIMIT=NONE] -->
|
||||
<string name="a11y_pip_menu_entered">Picture-in-Picture menu.</string>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -25,18 +25,11 @@ import static android.view.KeyEvent.KEYCODE_DPAD_RIGHT;
|
||||
import static android.view.KeyEvent.KEYCODE_DPAD_UP;
|
||||
import static android.view.KeyEvent.KEYCODE_ENTER;
|
||||
|
||||
import android.animation.ValueAnimator;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.RemoteAction;
|
||||
import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Handler;
|
||||
import android.text.Annotation;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannedString;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.Gravity;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.SurfaceControl;
|
||||
@@ -49,7 +42,6 @@ import android.widget.HorizontalScrollView;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -61,7 +53,6 @@ import com.android.wm.shell.pip.PipUtils;
|
||||
import com.android.wm.shell.protolog.ShellProtoLogGroup;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -74,21 +65,16 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
|
||||
|
||||
private static final int FIRST_CUSTOM_ACTION_POSITION = 3;
|
||||
|
||||
@Nullable
|
||||
private Listener mListener;
|
||||
private final Listener mListener;
|
||||
|
||||
private final LinearLayout mActionButtonsContainer;
|
||||
private final View mMenuFrameView;
|
||||
private final List<TvWindowMenuActionButton> mAdditionalButtons = new ArrayList<>();
|
||||
private final View mPipFrameView;
|
||||
private final View mPipView;
|
||||
private final TextView mEduTextView;
|
||||
private final View mEduTextContainerView;
|
||||
private final TvPipMenuEduTextDrawer mEduTextDrawer;
|
||||
private final int mPipMenuOuterSpace;
|
||||
private final int mPipMenuBorderWidth;
|
||||
private final int mEduTextFadeExitAnimationDurationMs;
|
||||
private final int mEduTextSlideExitAnimationDurationMs;
|
||||
private int mEduTextHeight;
|
||||
|
||||
private final ImageView mArrowUp;
|
||||
private final ImageView mArrowRight;
|
||||
@@ -116,25 +102,17 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
|
||||
private final int mResizeAnimationDuration;
|
||||
|
||||
private final AccessibilityManager mA11yManager;
|
||||
private final Handler mMainHandler;
|
||||
|
||||
public TvPipMenuView(@NonNull Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public TvPipMenuView(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public TvPipMenuView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
this(context, attrs, defStyleAttr, 0);
|
||||
}
|
||||
|
||||
public TvPipMenuView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
|
||||
int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
public TvPipMenuView(@NonNull Context context, @NonNull Handler mainHandler,
|
||||
@NonNull Listener listener) {
|
||||
super(context, null, 0, 0);
|
||||
|
||||
inflate(context, R.layout.tv_pip_menu, this);
|
||||
|
||||
mMainHandler = mainHandler;
|
||||
mListener = listener;
|
||||
|
||||
mA11yManager = context.getSystemService(AccessibilityManager.class);
|
||||
|
||||
mActionButtonsContainer = findViewById(R.id.tv_pip_menu_action_buttons);
|
||||
@@ -166,9 +144,6 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
|
||||
mArrowLeft = findViewById(R.id.tv_pip_menu_arrow_left);
|
||||
mA11yDoneButton = findViewById(R.id.tv_pip_menu_done_button);
|
||||
|
||||
mEduTextView = findViewById(R.id.tv_pip_menu_edu_text);
|
||||
mEduTextContainerView = findViewById(R.id.tv_pip_menu_edu_text_container);
|
||||
|
||||
mResizeAnimationDuration = context.getResources().getInteger(
|
||||
R.integer.config_pipResizeAnimationDuration);
|
||||
mPipMenuFadeAnimationDuration = context.getResources()
|
||||
@@ -178,63 +153,18 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
|
||||
.getDimensionPixelSize(R.dimen.pip_menu_outer_space);
|
||||
mPipMenuBorderWidth = context.getResources()
|
||||
.getDimensionPixelSize(R.dimen.pip_menu_border_width);
|
||||
mEduTextHeight = context.getResources()
|
||||
.getDimensionPixelSize(R.dimen.pip_menu_edu_text_view_height);
|
||||
mEduTextFadeExitAnimationDurationMs = context.getResources()
|
||||
.getInteger(R.integer.pip_edu_text_view_exit_animation_duration_ms);
|
||||
mEduTextSlideExitAnimationDurationMs = context.getResources()
|
||||
.getInteger(R.integer.pip_edu_text_window_exit_animation_duration_ms);
|
||||
|
||||
initEduText();
|
||||
mEduTextDrawer = new TvPipMenuEduTextDrawer(mContext, mainHandler, mListener);
|
||||
((FrameLayout) findViewById(R.id.tv_pip_menu_edu_text_drawer_placeholder))
|
||||
.addView(mEduTextDrawer);
|
||||
}
|
||||
|
||||
void initEduText() {
|
||||
final SpannedString eduText = (SpannedString) getResources().getText(R.string.pip_edu_text);
|
||||
final SpannableString spannableString = new SpannableString(eduText);
|
||||
Arrays.stream(eduText.getSpans(0, eduText.length(), Annotation.class)).findFirst()
|
||||
.ifPresent(annotation -> {
|
||||
final Drawable icon =
|
||||
getResources().getDrawable(R.drawable.home_icon, mContext.getTheme());
|
||||
if (icon != null) {
|
||||
icon.mutate();
|
||||
icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight());
|
||||
spannableString.setSpan(new CenteredImageSpan(icon),
|
||||
eduText.getSpanStart(annotation),
|
||||
eduText.getSpanEnd(annotation),
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
});
|
||||
|
||||
mEduTextView.setText(spannableString);
|
||||
}
|
||||
|
||||
void setEduTextActive(boolean active) {
|
||||
mEduTextView.setSelected(active);
|
||||
}
|
||||
|
||||
void hideEduText() {
|
||||
final ValueAnimator heightAnimation = ValueAnimator.ofInt(mEduTextHeight, 0);
|
||||
heightAnimation.setDuration(mEduTextSlideExitAnimationDurationMs);
|
||||
heightAnimation.setInterpolator(TvPipInterpolators.BROWSE);
|
||||
heightAnimation.addUpdateListener(animator -> {
|
||||
mEduTextHeight = (int) animator.getAnimatedValue();
|
||||
});
|
||||
mEduTextView.animate()
|
||||
.alpha(0f)
|
||||
.setInterpolator(TvPipInterpolators.EXIT)
|
||||
.setDuration(mEduTextFadeExitAnimationDurationMs)
|
||||
.withEndAction(() -> {
|
||||
mEduTextContainerView.setVisibility(GONE);
|
||||
}).start();
|
||||
heightAnimation.start();
|
||||
}
|
||||
|
||||
void onPipTransitionStarted(Rect finishBounds) {
|
||||
void onPipTransitionToTargetBoundsStarted(Rect targetBounds) {
|
||||
// Fade out content by fading in view on top.
|
||||
if (mCurrentPipBounds != null && finishBounds != null) {
|
||||
if (mCurrentPipBounds != null && targetBounds != null) {
|
||||
boolean ratioChanged = PipUtils.aspectRatioChanged(
|
||||
mCurrentPipBounds.width() / (float) mCurrentPipBounds.height(),
|
||||
finishBounds.width() / (float) finishBounds.height());
|
||||
targetBounds.width() / (float) targetBounds.height());
|
||||
if (ratioChanged) {
|
||||
mPipBackground.animate()
|
||||
.alpha(1f)
|
||||
@@ -245,11 +175,12 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
|
||||
}
|
||||
|
||||
// Update buttons.
|
||||
final boolean vertical = finishBounds.height() > finishBounds.width();
|
||||
final boolean vertical = targetBounds.height() > targetBounds.width();
|
||||
final boolean orientationChanged =
|
||||
vertical != (mActionButtonsContainer.getOrientation() == LinearLayout.VERTICAL);
|
||||
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
||||
"%s: onPipTransitionStarted(), orientation changed %b", TAG, orientationChanged);
|
||||
"%s: onPipTransitionToTargetBoundsStarted(), orientation changed %b",
|
||||
TAG, orientationChanged);
|
||||
if (!orientationChanged) {
|
||||
return;
|
||||
}
|
||||
@@ -261,18 +192,18 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
|
||||
.setInterpolator(TvPipInterpolators.EXIT)
|
||||
.setDuration(mResizeAnimationDuration / 2)
|
||||
.withEndAction(() -> {
|
||||
changeButtonScrollOrientation(finishBounds);
|
||||
updateButtonGravity(finishBounds);
|
||||
changeButtonScrollOrientation(targetBounds);
|
||||
updateButtonGravity(targetBounds);
|
||||
// Only make buttons visible again in onPipTransitionFinished to keep in
|
||||
// sync with PiP content alpha animation.
|
||||
});
|
||||
} else {
|
||||
changeButtonScrollOrientation(finishBounds);
|
||||
updateButtonGravity(finishBounds);
|
||||
changeButtonScrollOrientation(targetBounds);
|
||||
updateButtonGravity(targetBounds);
|
||||
}
|
||||
}
|
||||
|
||||
void onPipTransitionFinished(boolean isTvPipExpanded) {
|
||||
void onPipTransitionFinished(boolean enterTransition, boolean isTvPipExpanded) {
|
||||
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
||||
"%s: onPipTransitionFinished()", TAG);
|
||||
|
||||
@@ -283,6 +214,10 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
|
||||
.setInterpolator(TvPipInterpolators.ENTER)
|
||||
.start();
|
||||
|
||||
if (enterTransition) {
|
||||
mEduTextDrawer.init();
|
||||
}
|
||||
|
||||
setIsExpanded(isTvPipExpanded);
|
||||
|
||||
// Update buttons.
|
||||
@@ -409,7 +344,7 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
|
||||
Rect getPipMenuContainerBounds(Rect pipBounds) {
|
||||
final Rect menuUiBounds = new Rect(pipBounds);
|
||||
menuUiBounds.inset(-mPipMenuOuterSpace, -mPipMenuOuterSpace);
|
||||
menuUiBounds.bottom += mEduTextHeight;
|
||||
menuUiBounds.bottom += mEduTextDrawer.getHeight();
|
||||
return menuUiBounds;
|
||||
}
|
||||
|
||||
@@ -438,10 +373,6 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
|
||||
|
||||
}
|
||||
|
||||
void setListener(@Nullable Listener listener) {
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
void setExpandedModeEnabled(boolean enabled) {
|
||||
mExpandButton.setVisibility(enabled ? VISIBLE : GONE);
|
||||
}
|
||||
@@ -460,21 +391,19 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
|
||||
*/
|
||||
void showMoveMenu(int gravity) {
|
||||
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showMoveMenu()", TAG);
|
||||
mButtonMenuIsVisible = false;
|
||||
mMoveMenuIsVisible = true;
|
||||
showButtonsMenu(false);
|
||||
showMovementHints(gravity);
|
||||
setFrameHighlighted(true);
|
||||
|
||||
mHorizontalScrollView.setFocusable(false);
|
||||
mScrollView.setFocusable(false);
|
||||
|
||||
mEduTextDrawer.closeIfNeeded();
|
||||
}
|
||||
|
||||
void showButtonsMenu() {
|
||||
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
||||
"%s: showButtonsMenu()", TAG);
|
||||
mButtonMenuIsVisible = true;
|
||||
mMoveMenuIsVisible = false;
|
||||
showButtonsMenu(true);
|
||||
hideMovementHints();
|
||||
setFrameHighlighted(true);
|
||||
@@ -501,8 +430,6 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
|
||||
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
||||
"%s: hideAllUserControls()", TAG);
|
||||
mFocusedButton = null;
|
||||
mButtonMenuIsVisible = false;
|
||||
mMoveMenuIsVisible = false;
|
||||
showButtonsMenu(false);
|
||||
hideMovementHints();
|
||||
setFrameHighlighted(false);
|
||||
@@ -632,8 +559,6 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (mListener == null) return;
|
||||
|
||||
final int id = v.getId();
|
||||
if (id == R.id.tv_pip_menu_fullscreen_button) {
|
||||
mListener.onFullscreenButtonClick();
|
||||
@@ -662,7 +587,7 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
|
||||
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
if (mListener != null && event.getAction() == ACTION_UP) {
|
||||
if (event.getAction() == ACTION_UP) {
|
||||
if (!mMoveMenuIsVisible) {
|
||||
mFocusedButton = mActionButtonsContainer.getFocusedChild();
|
||||
}
|
||||
@@ -700,6 +625,11 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
|
||||
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
||||
"%s: showMovementHints(), position: %s", TAG, Gravity.toString(gravity));
|
||||
|
||||
if (mMoveMenuIsVisible) {
|
||||
return;
|
||||
}
|
||||
mMoveMenuIsVisible = true;
|
||||
|
||||
animateAlphaTo(checkGravity(gravity, Gravity.BOTTOM) ? 1f : 0f, mArrowUp);
|
||||
animateAlphaTo(checkGravity(gravity, Gravity.TOP) ? 1f : 0f, mArrowDown);
|
||||
animateAlphaTo(checkGravity(gravity, Gravity.RIGHT) ? 1f : 0f, mArrowLeft);
|
||||
@@ -714,9 +644,7 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
|
||||
animateAlphaTo(a11yEnabled ? 1f : 0f, mA11yDoneButton);
|
||||
if (a11yEnabled) {
|
||||
mA11yDoneButton.setOnClickListener(v -> {
|
||||
if (mListener != null) {
|
||||
mListener.onExitMoveMode();
|
||||
}
|
||||
mListener.onExitMoveMode();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -725,9 +653,7 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
|
||||
arrowView.setClickable(enabled);
|
||||
if (enabled) {
|
||||
arrowView.setOnClickListener(v -> {
|
||||
if (mListener != null) {
|
||||
mListener.onPipMovement(keycode);
|
||||
}
|
||||
mListener.onPipMovement(keycode);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -743,6 +669,11 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
|
||||
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
||||
"%s: hideMovementHints()", TAG);
|
||||
|
||||
if (!mMoveMenuIsVisible) {
|
||||
return;
|
||||
}
|
||||
mMoveMenuIsVisible = false;
|
||||
|
||||
animateAlphaTo(0, mArrowUp);
|
||||
animateAlphaTo(0, mArrowRight);
|
||||
animateAlphaTo(0, mArrowDown);
|
||||
@@ -756,19 +687,25 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
|
||||
public void showButtonsMenu(boolean show) {
|
||||
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
||||
"%s: showUserActions: %b", TAG, show);
|
||||
if (mButtonMenuIsVisible == show) {
|
||||
return;
|
||||
}
|
||||
mButtonMenuIsVisible = show;
|
||||
|
||||
if (show) {
|
||||
mActionButtonsContainer.setVisibility(VISIBLE);
|
||||
refocusPreviousButton();
|
||||
}
|
||||
animateAlphaTo(show ? 1 : 0, mActionButtonsContainer);
|
||||
animateAlphaTo(show ? 1 : 0, mDimLayer);
|
||||
mEduTextDrawer.closeIfNeeded();
|
||||
}
|
||||
|
||||
private void setFrameHighlighted(boolean highlighted) {
|
||||
mMenuFrameView.setActivated(highlighted);
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
interface Listener extends TvPipMenuEduTextDrawer.Listener {
|
||||
|
||||
void onBackPress();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user