• Home
  • History
  • Annotate
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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