Files
Lawnchair/wmshell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java
T
2024-11-02 10:55:28 +08:00

304 lines
12 KiB
Java

/*
* 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.ViewGroup.LayoutParams.MATCH_PARENT;
import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE;
import android.animation.Animator;
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.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();
}
int getEduTextDrawerHeight() {
return getVisibility() == GONE ? 0 : getHeight();
}
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() {
if (mEduTextView.getLayout() == null) {
return false;
}
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 heightAnimator = ValueAnimator.ofInt(getHeight(), 0);
heightAnimator.setDuration(eduTextSlideExitAnimationDuration);
heightAnimator.setInterpolator(TvPipInterpolators.BROWSE);
heightAnimator.addUpdateListener(animator -> {
final ViewGroup.LayoutParams params = getLayoutParams();
params.height = (int) animator.getAnimatedValue();
setLayoutParams(params);
});
heightAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(@NonNull Animator animator) {
}
@Override
public void onAnimationEnd(@NonNull Animator animator) {
onCloseEduTextAnimationEnd();
}
@Override
public void onAnimationCancel(@NonNull Animator animator) {
onCloseEduTextAnimationEnd();
}
@Override
public void onAnimationRepeat(@NonNull Animator animator) {
}
});
heightAnimator.start();
mListener.onCloseEduTextAnimationStart();
}
public void onCloseEduTextAnimationEnd() {
mListener.onCloseEduTextAnimationEnd();
}
/**
* 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 {
void onCloseEduTextAnimationStart();
void onCloseEduTextAnimationEnd();
}
}