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.ViewGroup.LayoutParams.MATCH_PARENT; 22 23 import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE; 24 25 import android.animation.Animator; 26 import android.animation.ValueAnimator; 27 import android.content.Context; 28 import android.graphics.drawable.Drawable; 29 import android.os.Handler; 30 import android.text.Annotation; 31 import android.text.Spannable; 32 import android.text.SpannableString; 33 import android.text.SpannedString; 34 import android.text.TextUtils; 35 import android.view.ViewGroup; 36 import android.view.ViewTreeObserver; 37 import android.widget.FrameLayout; 38 import android.widget.TextView; 39 40 import androidx.annotation.NonNull; 41 42 import com.android.internal.protolog.common.ProtoLog; 43 import com.android.wm.shell.R; 44 45 import java.util.Arrays; 46 47 /** 48 * The edu text drawer shows the user a hint for how to access the Picture-in-Picture menu. 49 * It displays a text in a drawer below the Picture-in-Picture window. The drawer has the same 50 * width as the Picture-in-Picture window. Depending on the Picture-in-Picture mode, there might 51 * not be enough space to fit the whole educational text in the available space. In such cases we 52 * apply a marquee animation to the TextView inside the drawer. 53 * 54 * The drawer is shown temporarily giving the user enough time to read it, after which it slides 55 * shut. We show the text for a duration calculated based on whether the text is marqueed or not. 56 */ 57 class TvPipMenuEduTextDrawer extends FrameLayout { 58 private static final String TAG = "TvPipMenuEduTextDrawer"; 59 60 private static final float MARQUEE_DP_PER_SECOND = 30; // Copy of TextView.MARQUEE_DP_PER_SECOND 61 private static final int MARQUEE_RESTART_DELAY = 1200; // Copy of TextView.MARQUEE_DELAY 62 private final float mMarqueeAnimSpeed; // pixels per ms 63 64 private final Runnable mCloseDrawerRunnable = this::closeDrawer; 65 private final Runnable mStartScrollEduTextRunnable = this::startScrollEduText; 66 67 private final Handler mMainHandler; 68 private final Listener mListener; 69 private final TextView mEduTextView; 70 TvPipMenuEduTextDrawer(@onNull Context context, Handler mainHandler, Listener listener)71 TvPipMenuEduTextDrawer(@NonNull Context context, Handler mainHandler, Listener listener) { 72 super(context, null, 0, 0); 73 74 mListener = listener; 75 mMainHandler = mainHandler; 76 77 // Taken from TextView.Marquee calculation 78 mMarqueeAnimSpeed = 79 (MARQUEE_DP_PER_SECOND * context.getResources().getDisplayMetrics().density) / 1000f; 80 81 mEduTextView = new TextView(mContext); 82 setupDrawer(); 83 } 84 setupDrawer()85 private void setupDrawer() { 86 final int eduTextHeight = mContext.getResources().getDimensionPixelSize( 87 R.dimen.pip_menu_edu_text_view_height); 88 final int marqueeRepeatLimit = mContext.getResources() 89 .getInteger(R.integer.pip_edu_text_scroll_times); 90 91 mEduTextView.setLayoutParams( 92 new LayoutParams(MATCH_PARENT, eduTextHeight, BOTTOM | CENTER)); 93 mEduTextView.setGravity(CENTER); 94 mEduTextView.setClickable(false); 95 mEduTextView.setText(createEduTextString()); 96 mEduTextView.setSingleLine(); 97 mEduTextView.setTextAppearance(R.style.TvPipEduText); 98 mEduTextView.setEllipsize(TextUtils.TruncateAt.MARQUEE); 99 mEduTextView.setMarqueeRepeatLimit(marqueeRepeatLimit); 100 mEduTextView.setHorizontallyScrolling(true); 101 mEduTextView.setHorizontalFadingEdgeEnabled(true); 102 mEduTextView.setSelected(false); 103 addView(mEduTextView); 104 105 setLayoutParams(new LayoutParams(MATCH_PARENT, eduTextHeight, CENTER)); 106 setClipChildren(true); 107 } 108 109 /** 110 * Initializes the edu text. Should only be called once when the PiP is entered 111 */ init()112 void init() { 113 ProtoLog.i(WM_SHELL_PICTURE_IN_PICTURE, "%s: init()", TAG); 114 scheduleLifecycleEvents(); 115 } 116 getEduTextDrawerHeight()117 int getEduTextDrawerHeight() { 118 return getVisibility() == GONE ? 0 : getHeight(); 119 } 120 scheduleLifecycleEvents()121 private void scheduleLifecycleEvents() { 122 final int startScrollDelay = mContext.getResources().getInteger( 123 R.integer.pip_edu_text_start_scroll_delay); 124 if (isEduTextMarqueed()) { 125 mMainHandler.postDelayed(mStartScrollEduTextRunnable, startScrollDelay); 126 } 127 mMainHandler.postDelayed(mCloseDrawerRunnable, startScrollDelay + getEduTextShowDuration()); 128 mEduTextView.getViewTreeObserver().addOnWindowAttachListener( 129 new ViewTreeObserver.OnWindowAttachListener() { 130 @Override 131 public void onWindowAttached() { 132 } 133 134 @Override 135 public void onWindowDetached() { 136 mEduTextView.getViewTreeObserver().removeOnWindowAttachListener(this); 137 mMainHandler.removeCallbacks(mStartScrollEduTextRunnable); 138 mMainHandler.removeCallbacks(mCloseDrawerRunnable); 139 } 140 }); 141 } 142 getEduTextShowDuration()143 private int getEduTextShowDuration() { 144 int eduTextShowDuration; 145 if (isEduTextMarqueed()) { 146 // Calculate the time it takes to fully scroll the text once: time = distance / speed 147 final float singleMarqueeDuration = 148 getMarqueeAnimEduTextLineWidth() / mMarqueeAnimSpeed; 149 // The TextView adds a delay between each marquee repetition. Take that into account 150 final float durationFromStartToStart = singleMarqueeDuration + MARQUEE_RESTART_DELAY; 151 // Finally, multiply by the number of times we repeat the marquee animation 152 eduTextShowDuration = 153 (int) durationFromStartToStart * mEduTextView.getMarqueeRepeatLimit(); 154 } else { 155 eduTextShowDuration = mContext.getResources() 156 .getInteger(R.integer.pip_edu_text_non_scroll_show_duration); 157 } 158 159 ProtoLog.d(WM_SHELL_PICTURE_IN_PICTURE, "%s: getEduTextShowDuration(), showDuration=%d", 160 TAG, eduTextShowDuration); 161 return eduTextShowDuration; 162 } 163 164 /** 165 * Returns true if the edu text width is bigger than the width of the text view, which indicates 166 * that the edu text will be marqueed 167 */ isEduTextMarqueed()168 private boolean isEduTextMarqueed() { 169 if (mEduTextView.getLayout() == null) { 170 return false; 171 } 172 final int availableWidth = (int) mEduTextView.getWidth() 173 - mEduTextView.getCompoundPaddingLeft() 174 - mEduTextView.getCompoundPaddingRight(); 175 return availableWidth < getEduTextWidth(); 176 } 177 178 /** 179 * Returns the width of a single marquee repetition of the edu text in pixels. 180 * This is the width from the start of the edu text to the start of the next edu 181 * text when it is marqueed. 182 * 183 * This is calculated based on the TextView.Marquee#start calculations 184 */ getMarqueeAnimEduTextLineWidth()185 private float getMarqueeAnimEduTextLineWidth() { 186 // When the TextView has a marquee animation, it puts a gap between the text end and the 187 // start of the next edu text repetition. The space is equal to a third of the TextView 188 // width 189 final float gap = mEduTextView.getWidth() / 3.0f; 190 return getEduTextWidth() + gap; 191 } 192 startScrollEduText()193 private void startScrollEduText() { 194 ProtoLog.d(WM_SHELL_PICTURE_IN_PICTURE, "%s: startScrollEduText(), repeat=%d", 195 TAG, mEduTextView.getMarqueeRepeatLimit()); 196 mEduTextView.setSelected(true); 197 } 198 199 /** 200 * Returns the width of the edu text irrespective of the TextView width 201 */ getEduTextWidth()202 private int getEduTextWidth() { 203 return (int) mEduTextView.getLayout().getLineWidth(0); 204 } 205 206 /** 207 * Closes the edu text drawer if it hasn't been closed yet 208 */ closeIfNeeded()209 void closeIfNeeded() { 210 if (mMainHandler.hasCallbacks(mCloseDrawerRunnable)) { 211 ProtoLog.d(WM_SHELL_PICTURE_IN_PICTURE, 212 "%s: close(), closing the edu text drawer because of user action", TAG); 213 mMainHandler.removeCallbacks(mCloseDrawerRunnable); 214 mCloseDrawerRunnable.run(); 215 } else { 216 // Do nothing, the drawer has already been closed 217 } 218 } 219 closeDrawer()220 private void closeDrawer() { 221 ProtoLog.i(WM_SHELL_PICTURE_IN_PICTURE, "%s: closeDrawer()", TAG); 222 final int eduTextFadeExitAnimationDuration = mContext.getResources().getInteger( 223 R.integer.pip_edu_text_view_exit_animation_duration); 224 final int eduTextSlideExitAnimationDuration = mContext.getResources().getInteger( 225 R.integer.pip_edu_text_window_exit_animation_duration); 226 227 // Start fading out the edu text 228 mEduTextView.animate() 229 .alpha(0f) 230 .setInterpolator(TvPipInterpolators.EXIT) 231 .setDuration(eduTextFadeExitAnimationDuration) 232 .start(); 233 234 // Start animation to close the drawer by animating its height to 0 235 final ValueAnimator heightAnimator = ValueAnimator.ofInt(getHeight(), 0); 236 heightAnimator.setDuration(eduTextSlideExitAnimationDuration); 237 heightAnimator.setInterpolator(TvPipInterpolators.BROWSE); 238 heightAnimator.addUpdateListener(animator -> { 239 final ViewGroup.LayoutParams params = getLayoutParams(); 240 params.height = (int) animator.getAnimatedValue(); 241 setLayoutParams(params); 242 }); 243 heightAnimator.addListener(new Animator.AnimatorListener() { 244 @Override 245 public void onAnimationStart(@NonNull Animator animator) { 246 } 247 248 @Override 249 public void onAnimationEnd(@NonNull Animator animator) { 250 onCloseEduTextAnimationEnd(); 251 } 252 253 @Override 254 public void onAnimationCancel(@NonNull Animator animator) { 255 onCloseEduTextAnimationEnd(); 256 } 257 258 @Override 259 public void onAnimationRepeat(@NonNull Animator animator) { 260 } 261 }); 262 heightAnimator.start(); 263 264 mListener.onCloseEduTextAnimationStart(); 265 } 266 onCloseEduTextAnimationEnd()267 public void onCloseEduTextAnimationEnd() { 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