1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.wm.shell.pip.tv; 18 19 import static android.view.Gravity.BOTTOM; 20 import static android.view.Gravity.CENTER; 21 import static android.view.View.GONE; 22 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; 23 24 import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE; 25 26 import android.animation.Animator; 27 import android.animation.ValueAnimator; 28 import android.content.Context; 29 import android.graphics.drawable.Drawable; 30 import android.os.Handler; 31 import android.text.Annotation; 32 import android.text.Spannable; 33 import android.text.SpannableString; 34 import android.text.SpannedString; 35 import android.text.TextUtils; 36 import android.view.ViewGroup; 37 import android.view.ViewTreeObserver; 38 import android.widget.FrameLayout; 39 import android.widget.FrameLayout.LayoutParams; 40 import android.widget.TextView; 41 42 import androidx.annotation.NonNull; 43 44 import com.android.internal.protolog.common.ProtoLog; 45 import com.android.wm.shell.R; 46 47 import java.util.Arrays; 48 49 /** 50 * The edu text drawer shows the user a hint for how to access the Picture-in-Picture menu. 51 * It displays a text in a drawer below the Picture-in-Picture window. The drawer has the same 52 * width as the Picture-in-Picture window. Depending on the Picture-in-Picture mode, there might 53 * not be enough space to fit the whole educational text in the available space. In such cases we 54 * apply a marquee animation to the TextView inside the drawer. 55 * 56 * The drawer is shown temporarily giving the user enough time to read it, after which it slides 57 * shut. We show the text for a duration calculated based on whether the text is marqueed or not. 58 */ 59 class TvPipMenuEduTextDrawer extends FrameLayout { 60 private static final String TAG = "TvPipMenuEduTextDrawer"; 61 62 private static final float MARQUEE_DP_PER_SECOND = 30; // Copy of TextView.MARQUEE_DP_PER_SECOND 63 private static final int MARQUEE_RESTART_DELAY = 1200; // Copy of TextView.MARQUEE_DELAY 64 private final float mMarqueeAnimSpeed; // pixels per ms 65 66 private final Runnable mCloseDrawerRunnable = this::closeDrawer; 67 private final Runnable mStartScrollEduTextRunnable = this::startScrollEduText; 68 69 private final Handler mMainHandler; 70 private final Listener mListener; 71 private final TextView mEduTextView; 72 TvPipMenuEduTextDrawer(@onNull Context context, Handler mainHandler, Listener listener)73 TvPipMenuEduTextDrawer(@NonNull Context context, Handler mainHandler, Listener listener) { 74 super(context, null, 0, 0); 75 76 mListener = listener; 77 mMainHandler = mainHandler; 78 79 // Taken from TextView.Marquee calculation 80 mMarqueeAnimSpeed = 81 (MARQUEE_DP_PER_SECOND * context.getResources().getDisplayMetrics().density) / 1000f; 82 83 mEduTextView = new TextView(mContext); 84 setupDrawer(); 85 } 86 setupDrawer()87 private void setupDrawer() { 88 final int eduTextHeight = mContext.getResources().getDimensionPixelSize( 89 R.dimen.pip_menu_edu_text_view_height); 90 final int marqueeRepeatLimit = mContext.getResources() 91 .getInteger(R.integer.pip_edu_text_scroll_times); 92 93 mEduTextView.setLayoutParams( 94 new LayoutParams(MATCH_PARENT, eduTextHeight, BOTTOM | CENTER)); 95 mEduTextView.setGravity(CENTER); 96 mEduTextView.setClickable(false); 97 mEduTextView.setText(createEduTextString()); 98 mEduTextView.setSingleLine(); 99 mEduTextView.setTextAppearance(R.style.TvPipEduText); 100 mEduTextView.setEllipsize(TextUtils.TruncateAt.MARQUEE); 101 mEduTextView.setMarqueeRepeatLimit(marqueeRepeatLimit); 102 mEduTextView.setHorizontallyScrolling(true); 103 mEduTextView.setHorizontalFadingEdgeEnabled(true); 104 mEduTextView.setSelected(false); 105 addView(mEduTextView); 106 107 setLayoutParams(new LayoutParams(MATCH_PARENT, eduTextHeight, CENTER)); 108 setClipChildren(true); 109 } 110 111 /** 112 * Initializes the edu text. Should only be called once when the PiP is entered 113 */ init()114 void init() { 115 ProtoLog.i(WM_SHELL_PICTURE_IN_PICTURE, "%s: init()", TAG); 116 scheduleLifecycleEvents(); 117 } 118 getEduTextDrawerHeight()119 int getEduTextDrawerHeight() { 120 return getVisibility() == GONE ? 0 : getHeight(); 121 } 122 scheduleLifecycleEvents()123 private void scheduleLifecycleEvents() { 124 final int startScrollDelay = mContext.getResources().getInteger( 125 R.integer.pip_edu_text_start_scroll_delay); 126 if (isEduTextMarqueed()) { 127 mMainHandler.postDelayed(mStartScrollEduTextRunnable, startScrollDelay); 128 } 129 mMainHandler.postDelayed(mCloseDrawerRunnable, startScrollDelay + getEduTextShowDuration()); 130 mEduTextView.getViewTreeObserver().addOnWindowAttachListener( 131 new ViewTreeObserver.OnWindowAttachListener() { 132 @Override 133 public void onWindowAttached() { 134 } 135 136 @Override 137 public void onWindowDetached() { 138 mEduTextView.getViewTreeObserver().removeOnWindowAttachListener(this); 139 mMainHandler.removeCallbacks(mStartScrollEduTextRunnable); 140 mMainHandler.removeCallbacks(mCloseDrawerRunnable); 141 } 142 }); 143 } 144 getEduTextShowDuration()145 private int getEduTextShowDuration() { 146 int eduTextShowDuration; 147 if (isEduTextMarqueed()) { 148 // Calculate the time it takes to fully scroll the text once: time = distance / speed 149 final float singleMarqueeDuration = 150 getMarqueeAnimEduTextLineWidth() / mMarqueeAnimSpeed; 151 // The TextView adds a delay between each marquee repetition. Take that into account 152 final float durationFromStartToStart = singleMarqueeDuration + MARQUEE_RESTART_DELAY; 153 // Finally, multiply by the number of times we repeat the marquee animation 154 eduTextShowDuration = 155 (int) durationFromStartToStart * mEduTextView.getMarqueeRepeatLimit(); 156 } else { 157 eduTextShowDuration = mContext.getResources() 158 .getInteger(R.integer.pip_edu_text_non_scroll_show_duration); 159 } 160 161 ProtoLog.d(WM_SHELL_PICTURE_IN_PICTURE, "%s: getEduTextShowDuration(), showDuration=%d", 162 TAG, eduTextShowDuration); 163 return eduTextShowDuration; 164 } 165 166 /** 167 * Returns true if the edu text width is bigger than the width of the text view, which indicates 168 * that the edu text will be marqueed 169 */ isEduTextMarqueed()170 private boolean isEduTextMarqueed() { 171 final int availableWidth = (int) mEduTextView.getWidth() 172 - mEduTextView.getCompoundPaddingLeft() 173 - mEduTextView.getCompoundPaddingRight(); 174 return availableWidth < getEduTextWidth(); 175 } 176 177 /** 178 * Returns the width of a single marquee repetition of the edu text in pixels. 179 * This is the width from the start of the edu text to the start of the next edu 180 * text when it is marqueed. 181 * 182 * This is calculated based on the TextView.Marquee#start calculations 183 */ getMarqueeAnimEduTextLineWidth()184 private float getMarqueeAnimEduTextLineWidth() { 185 // When the TextView has a marquee animation, it puts a gap between the text end and the 186 // start of the next edu text repetition. The space is equal to a third of the TextView 187 // width 188 final float gap = mEduTextView.getWidth() / 3.0f; 189 return getEduTextWidth() + gap; 190 } 191 startScrollEduText()192 private void startScrollEduText() { 193 ProtoLog.d(WM_SHELL_PICTURE_IN_PICTURE, "%s: startScrollEduText(), repeat=%d", 194 TAG, mEduTextView.getMarqueeRepeatLimit()); 195 mEduTextView.setSelected(true); 196 } 197 198 /** 199 * Returns the width of the edu text irrespective of the TextView width 200 */ getEduTextWidth()201 private int getEduTextWidth() { 202 return (int) mEduTextView.getLayout().getLineWidth(0); 203 } 204 205 /** 206 * Closes the edu text drawer if it hasn't been closed yet 207 */ closeIfNeeded()208 void closeIfNeeded() { 209 if (mMainHandler.hasCallbacks(mCloseDrawerRunnable)) { 210 ProtoLog.d(WM_SHELL_PICTURE_IN_PICTURE, 211 "%s: close(), closing the edu text drawer because of user action", TAG); 212 mMainHandler.removeCallbacks(mCloseDrawerRunnable); 213 mCloseDrawerRunnable.run(); 214 } else { 215 // Do nothing, the drawer has already been closed 216 } 217 } 218 closeDrawer()219 private void closeDrawer() { 220 ProtoLog.i(WM_SHELL_PICTURE_IN_PICTURE, "%s: closeDrawer()", TAG); 221 final int eduTextFadeExitAnimationDuration = mContext.getResources().getInteger( 222 R.integer.pip_edu_text_view_exit_animation_duration); 223 final int eduTextSlideExitAnimationDuration = mContext.getResources().getInteger( 224 R.integer.pip_edu_text_window_exit_animation_duration); 225 226 // Start fading out the edu text 227 mEduTextView.animate() 228 .alpha(0f) 229 .setInterpolator(TvPipInterpolators.EXIT) 230 .setDuration(eduTextFadeExitAnimationDuration) 231 .start(); 232 233 // Start animation to close the drawer by animating its height to 0 234 final ValueAnimator heightAnimator = ValueAnimator.ofInt(getHeight(), 0); 235 heightAnimator.setDuration(eduTextSlideExitAnimationDuration); 236 heightAnimator.setInterpolator(TvPipInterpolators.BROWSE); 237 heightAnimator.addUpdateListener(animator -> { 238 final ViewGroup.LayoutParams params = getLayoutParams(); 239 params.height = (int) animator.getAnimatedValue(); 240 setLayoutParams(params); 241 }); 242 heightAnimator.addListener(new Animator.AnimatorListener() { 243 @Override 244 public void onAnimationStart(@NonNull Animator animator) { 245 } 246 247 @Override 248 public void onAnimationEnd(@NonNull Animator animator) { 249 onCloseEduTextAnimationEnd(); 250 } 251 252 @Override 253 public void onAnimationCancel(@NonNull Animator animator) { 254 onCloseEduTextAnimationEnd(); 255 } 256 257 @Override 258 public void onAnimationRepeat(@NonNull Animator animator) { 259 } 260 }); 261 heightAnimator.start(); 262 263 mListener.onCloseEduTextAnimationStart(); 264 } 265 onCloseEduTextAnimationEnd()266 public void onCloseEduTextAnimationEnd() { 267 setVisibility(GONE); 268 mListener.onCloseEduTextAnimationEnd(); 269 } 270 271 /** 272 * Creates the educational text that will be displayed to the user. Here we replace the 273 * HOME annotation in the String with an icon 274 */ createEduTextString()275 private CharSequence createEduTextString() { 276 final SpannedString eduText = (SpannedString) getResources().getText(R.string.pip_edu_text); 277 final SpannableString spannableString = new SpannableString(eduText); 278 Arrays.stream(eduText.getSpans(0, eduText.length(), Annotation.class)).findFirst() 279 .ifPresent(annotation -> { 280 final Drawable icon = 281 getResources().getDrawable(R.drawable.home_icon, mContext.getTheme()); 282 if (icon != null) { 283 icon.mutate(); 284 icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight()); 285 spannableString.setSpan(new CenteredImageSpan(icon), 286 eduText.getSpanStart(annotation), 287 eduText.getSpanEnd(annotation), 288 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 289 } 290 }); 291 292 return spannableString; 293 } 294 295 /** 296 * A listener for edu text drawer event states. 297 */ 298 interface Listener { onCloseEduTextAnimationStart()299 void onCloseEduTextAnimationStart(); onCloseEduTextAnimationEnd()300 void onCloseEduTextAnimationEnd(); 301 } 302 303 } 304