• Home
  • 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.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