Merge "Fix TV pip menu edu text hidden too soon"

This commit is contained in:
Galia Peycheva
2022-10-05 08:06:19 +00:00
committed by Android (Google) Code Review
7 changed files with 382 additions and 189 deletions

View File

@@ -119,7 +119,7 @@
<!-- Temporarily extending the background to show an edu text hint for opening the menu -->
<FrameLayout
android:id="@+id/tv_pip_menu_edu_text_container"
android:id="@+id/tv_pip_menu_edu_text_drawer_placeholder"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/tv_pip"
@@ -127,23 +127,8 @@
android:layout_alignStart="@+id/tv_pip"
android:layout_alignEnd="@+id/tv_pip"
android:background="@color/tv_pip_menu_background"
android:clipChildren="true">
<TextView
android:id="@+id/tv_pip_menu_edu_text"
android:layout_width="wrap_content"
android:layout_height="@dimen/pip_menu_edu_text_view_height"
android:layout_gravity="bottom|center"
android:gravity="center"
android:clickable="false"
android:paddingBottom="@dimen/pip_menu_border_width"
android:text="@string/pip_edu_text"
android:singleLine="true"
android:ellipsize="marquee"
android:marqueeRepeatLimit="1"
android:scrollHorizontally="true"
android:textAppearance="@style/TvPipEduText"/>
</FrameLayout>
android:paddingBottom="@dimen/pip_menu_border_width"
android:paddingTop="@dimen/pip_menu_border_width"/>
<!-- Frame around the PiP content + edu text hint - used to highlight open menu -->
<View

View File

@@ -41,8 +41,10 @@
<dimen name="pip_menu_edu_text_view_height">24dp</dimen>
<dimen name="pip_menu_edu_text_home_icon">9sp</dimen>
<dimen name="pip_menu_edu_text_home_icon_outline">14sp</dimen>
<integer name="pip_edu_text_show_duration_ms">10500</integer>
<integer name="pip_edu_text_window_exit_animation_duration_ms">1000</integer>
<integer name="pip_edu_text_view_exit_animation_duration_ms">300</integer>
<integer name="pip_edu_text_scroll_times">2</integer>
<integer name="pip_edu_text_non_scroll_show_duration">10500</integer>
<integer name="pip_edu_text_start_scroll_delay">2000</integer>
<integer name="pip_edu_text_window_exit_animation_duration">1000</integer>
<integer name="pip_edu_text_view_exit_animation_duration">300</integer>
</resources>

View File

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

View File

@@ -127,7 +127,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
private int mPipForceCloseDelay;
private int mResizeAnimationDuration;
private int mEduTextWindowExitAnimationDurationMs;
private int mEduTextWindowExitAnimationDuration;
public static Pip create(
Context context,
@@ -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) {

View File

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

View File

@@ -0,0 +1,280 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.wm.shell.pip.tv;
import static android.view.Gravity.BOTTOM;
import static android.view.Gravity.CENTER;
import static android.view.View.GONE;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.text.Annotation;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannedString;
import android.text.TextUtils;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.FrameLayout;
import android.widget.FrameLayout.LayoutParams;
import android.widget.TextView;
import androidx.annotation.NonNull;
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.R;
import java.util.Arrays;
/**
* The edu text drawer shows the user a hint for how to access the Picture-in-Picture menu.
* It displays a text in a drawer below the Picture-in-Picture window. The drawer has the same
* width as the Picture-in-Picture window. Depending on the Picture-in-Picture mode, there might
* not be enough space to fit the whole educational text in the available space. In such cases we
* apply a marquee animation to the TextView inside the drawer.
*
* The drawer is shown temporarily giving the user enough time to read it, after which it slides
* shut. We show the text for a duration calculated based on whether the text is marqueed or not.
*/
class TvPipMenuEduTextDrawer extends FrameLayout {
private static final String TAG = "TvPipMenuEduTextDrawer";
private static final float MARQUEE_DP_PER_SECOND = 30; // Copy of TextView.MARQUEE_DP_PER_SECOND
private static final int MARQUEE_RESTART_DELAY = 1200; // Copy of TextView.MARQUEE_DELAY
private final float mMarqueeAnimSpeed; // pixels per ms
private final Runnable mCloseDrawerRunnable = this::closeDrawer;
private final Runnable mStartScrollEduTextRunnable = this::startScrollEduText;
private final Handler mMainHandler;
private final Listener mListener;
private final TextView mEduTextView;
TvPipMenuEduTextDrawer(@NonNull Context context, Handler mainHandler, Listener listener) {
super(context, null, 0, 0);
mListener = listener;
mMainHandler = mainHandler;
// Taken from TextView.Marquee calculation
mMarqueeAnimSpeed =
(MARQUEE_DP_PER_SECOND * context.getResources().getDisplayMetrics().density) / 1000f;
mEduTextView = new TextView(mContext);
setupDrawer();
}
private void setupDrawer() {
final int eduTextHeight = mContext.getResources().getDimensionPixelSize(
R.dimen.pip_menu_edu_text_view_height);
final int marqueeRepeatLimit = mContext.getResources()
.getInteger(R.integer.pip_edu_text_scroll_times);
mEduTextView.setLayoutParams(
new LayoutParams(MATCH_PARENT, eduTextHeight, BOTTOM | CENTER));
mEduTextView.setGravity(CENTER);
mEduTextView.setClickable(false);
mEduTextView.setText(createEduTextString());
mEduTextView.setSingleLine();
mEduTextView.setTextAppearance(R.style.TvPipEduText);
mEduTextView.setEllipsize(TextUtils.TruncateAt.MARQUEE);
mEduTextView.setMarqueeRepeatLimit(marqueeRepeatLimit);
mEduTextView.setHorizontallyScrolling(true);
mEduTextView.setHorizontalFadingEdgeEnabled(true);
mEduTextView.setSelected(false);
addView(mEduTextView);
setLayoutParams(new LayoutParams(MATCH_PARENT, eduTextHeight, CENTER));
setClipChildren(true);
}
/**
* Initializes the edu text. Should only be called once when the PiP is entered
*/
void init() {
ProtoLog.i(WM_SHELL_PICTURE_IN_PICTURE, "%s: init()", TAG);
scheduleLifecycleEvents();
}
private void scheduleLifecycleEvents() {
final int startScrollDelay = mContext.getResources().getInteger(
R.integer.pip_edu_text_start_scroll_delay);
if (isEduTextMarqueed()) {
mMainHandler.postDelayed(mStartScrollEduTextRunnable, startScrollDelay);
}
mMainHandler.postDelayed(mCloseDrawerRunnable, startScrollDelay + getEduTextShowDuration());
mEduTextView.getViewTreeObserver().addOnWindowAttachListener(
new ViewTreeObserver.OnWindowAttachListener() {
@Override
public void onWindowAttached() {
}
@Override
public void onWindowDetached() {
mEduTextView.getViewTreeObserver().removeOnWindowAttachListener(this);
mMainHandler.removeCallbacks(mStartScrollEduTextRunnable);
mMainHandler.removeCallbacks(mCloseDrawerRunnable);
}
});
}
private int getEduTextShowDuration() {
int eduTextShowDuration;
if (isEduTextMarqueed()) {
// Calculate the time it takes to fully scroll the text once: time = distance / speed
final float singleMarqueeDuration =
getMarqueeAnimEduTextLineWidth() / mMarqueeAnimSpeed;
// The TextView adds a delay between each marquee repetition. Take that into account
final float durationFromStartToStart = singleMarqueeDuration + MARQUEE_RESTART_DELAY;
// Finally, multiply by the number of times we repeat the marquee animation
eduTextShowDuration =
(int) durationFromStartToStart * mEduTextView.getMarqueeRepeatLimit();
} else {
eduTextShowDuration = mContext.getResources()
.getInteger(R.integer.pip_edu_text_non_scroll_show_duration);
}
ProtoLog.d(WM_SHELL_PICTURE_IN_PICTURE, "%s: getEduTextShowDuration(), showDuration=%d",
TAG, eduTextShowDuration);
return eduTextShowDuration;
}
/**
* Returns true if the edu text width is bigger than the width of the text view, which indicates
* that the edu text will be marqueed
*/
private boolean isEduTextMarqueed() {
final int availableWidth = (int) mEduTextView.getWidth()
- mEduTextView.getCompoundPaddingLeft()
- mEduTextView.getCompoundPaddingRight();
return availableWidth < getEduTextWidth();
}
/**
* Returns the width of a single marquee repetition of the edu text in pixels.
* This is the width from the start of the edu text to the start of the next edu
* text when it is marqueed.
*
* This is calculated based on the TextView.Marquee#start calculations
*/
private float getMarqueeAnimEduTextLineWidth() {
// When the TextView has a marquee animation, it puts a gap between the text end and the
// start of the next edu text repetition. The space is equal to a third of the TextView
// width
final float gap = mEduTextView.getWidth() / 3.0f;
return getEduTextWidth() + gap;
}
private void startScrollEduText() {
ProtoLog.d(WM_SHELL_PICTURE_IN_PICTURE, "%s: startScrollEduText(), repeat=%d",
TAG, mEduTextView.getMarqueeRepeatLimit());
mEduTextView.setSelected(true);
}
/**
* Returns the width of the edu text irrespective of the TextView width
*/
private int getEduTextWidth() {
return (int) mEduTextView.getLayout().getLineWidth(0);
}
/**
* Closes the edu text drawer if it hasn't been closed yet
*/
void closeIfNeeded() {
if (mMainHandler.hasCallbacks(mCloseDrawerRunnable)) {
ProtoLog.d(WM_SHELL_PICTURE_IN_PICTURE,
"%s: close(), closing the edu text drawer because of user action", TAG);
mMainHandler.removeCallbacks(mCloseDrawerRunnable);
mCloseDrawerRunnable.run();
} else {
// Do nothing, the drawer has already been closed
}
}
private void closeDrawer() {
ProtoLog.i(WM_SHELL_PICTURE_IN_PICTURE, "%s: closeDrawer()", TAG);
final int eduTextFadeExitAnimationDuration = mContext.getResources().getInteger(
R.integer.pip_edu_text_view_exit_animation_duration);
final int eduTextSlideExitAnimationDuration = mContext.getResources().getInteger(
R.integer.pip_edu_text_window_exit_animation_duration);
// Start fading out the edu text
mEduTextView.animate()
.alpha(0f)
.setInterpolator(TvPipInterpolators.EXIT)
.setDuration(eduTextFadeExitAnimationDuration)
.start();
// Start animation to close the drawer by animating its height to 0
final ValueAnimator heightAnimation = ValueAnimator.ofInt(getHeight(), 0);
heightAnimation.setDuration(eduTextSlideExitAnimationDuration);
heightAnimation.setInterpolator(TvPipInterpolators.BROWSE);
heightAnimation.addUpdateListener(animator -> {
final ViewGroup.LayoutParams params = getLayoutParams();
params.height = (int) animator.getAnimatedValue();
setLayoutParams(params);
if (params.height == 0) {
setVisibility(GONE);
}
});
heightAnimation.start();
mListener.onCloseEduText();
}
/**
* Creates the educational text that will be displayed to the user. Here we replace the
* HOME annotation in the String with an icon
*/
private CharSequence createEduTextString() {
final SpannedString eduText = (SpannedString) getResources().getText(R.string.pip_edu_text);
final SpannableString spannableString = new SpannableString(eduText);
Arrays.stream(eduText.getSpans(0, eduText.length(), Annotation.class)).findFirst()
.ifPresent(annotation -> {
final Drawable icon =
getResources().getDrawable(R.drawable.home_icon, mContext.getTheme());
if (icon != null) {
icon.mutate();
icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight());
spannableString.setSpan(new CenteredImageSpan(icon),
eduText.getSpanStart(annotation),
eduText.getSpanEnd(annotation),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
});
return spannableString;
}
/**
* A listener for edu text drawer event states.
*/
interface Listener {
/**
* The edu text closing impacts the size of the Picture-in-Picture window and influences
* how it is positioned on the screen.
*/
void onCloseEduText();
}
}

View File

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