• 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.systemui.clipboardoverlay;
18 
19 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
20 
21 import static com.android.systemui.Flags.showClipboardIndication;
22 
23 import android.animation.Animator;
24 import android.animation.AnimatorListenerAdapter;
25 import android.animation.AnimatorSet;
26 import android.animation.ObjectAnimator;
27 import android.animation.TimeInterpolator;
28 import android.animation.ValueAnimator;
29 import android.annotation.Nullable;
30 import android.app.PendingIntent;
31 import android.app.RemoteAction;
32 import android.content.Context;
33 import android.content.res.Resources;
34 import android.graphics.Bitmap;
35 import android.graphics.Insets;
36 import android.graphics.Paint;
37 import android.graphics.Rect;
38 import android.graphics.Region;
39 import android.graphics.drawable.Icon;
40 import android.util.AttributeSet;
41 import android.util.DisplayMetrics;
42 import android.util.Log;
43 import android.util.MathUtils;
44 import android.util.TypedValue;
45 import android.view.DisplayCutout;
46 import android.view.Gravity;
47 import android.view.LayoutInflater;
48 import android.view.View;
49 import android.view.WindowInsets;
50 import android.view.accessibility.AccessibilityManager;
51 import android.view.animation.LinearInterpolator;
52 import android.view.animation.PathInterpolator;
53 import android.widget.FrameLayout;
54 import android.widget.ImageView;
55 import android.widget.LinearLayout;
56 import android.widget.TextView;
57 
58 import androidx.constraintlayout.widget.ConstraintLayout;
59 import androidx.core.view.ViewCompat;
60 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
61 
62 import com.android.systemui.res.R;
63 import com.android.systemui.screenshot.DraggableConstraintLayout;
64 import com.android.systemui.screenshot.FloatingWindowUtil;
65 import com.android.systemui.screenshot.ui.binder.ActionButtonViewBinder;
66 import com.android.systemui.screenshot.ui.viewmodel.ActionButtonAppearance;
67 import com.android.systemui.screenshot.ui.viewmodel.ActionButtonViewModel;
68 
69 import kotlin.Unit;
70 import kotlin.jvm.functions.Function0;
71 
72 import java.util.ArrayList;
73 
74 /**
75  * Handles the visual elements and animations for the clipboard overlay.
76  */
77 public class ClipboardOverlayView extends DraggableConstraintLayout {
78 
79     interface ClipboardOverlayCallbacks extends SwipeDismissCallbacks {
onDismissButtonTapped()80         void onDismissButtonTapped();
81 
onRemoteCopyButtonTapped()82         void onRemoteCopyButtonTapped();
83 
onShareButtonTapped()84         void onShareButtonTapped();
85 
onPreviewTapped()86         void onPreviewTapped();
87 
onMinimizedViewTapped()88         void onMinimizedViewTapped();
89     }
90 
91     private static final String TAG = "ClipboardView";
92 
93     private static final int SWIPE_PADDING_DP = 12; // extra padding around views to allow swipe
94     private static final int FONT_SEARCH_STEP_PX = 4;
95 
96     private final DisplayMetrics mDisplayMetrics;
97     private final AccessibilityManager mAccessibilityManager;
98     private final ArrayList<View> mActionChips = new ArrayList<>();
99 
100     private View mClipboardPreview;
101     private ImageView mImagePreview;
102     private TextView mTextPreview;
103     private TextView mHiddenPreview;
104     private LinearLayout mMinimizedPreview;
105     private View mPreviewBorder;
106     private View mShareChip;
107     private View mRemoteCopyChip;
108     private View mActionContainerBackground;
109     private View mIndicationContainer;
110     private TextView mIndicationText;
111     private View mDismissButton;
112     private LinearLayout mActionContainer;
113     private ClipboardOverlayCallbacks mClipboardCallbacks;
114     private ActionButtonViewBinder mActionButtonViewBinder = new ActionButtonViewBinder();
115 
ClipboardOverlayView(Context context)116     public ClipboardOverlayView(Context context) {
117         this(context, null);
118     }
119 
ClipboardOverlayView(Context context, AttributeSet attrs)120     public ClipboardOverlayView(Context context, AttributeSet attrs) {
121         this(context, attrs, 0);
122     }
123 
ClipboardOverlayView(Context context, AttributeSet attrs, int defStyleAttr)124     public ClipboardOverlayView(Context context, AttributeSet attrs, int defStyleAttr) {
125         super(context, attrs, defStyleAttr);
126         mDisplayMetrics = new DisplayMetrics();
127         mContext.getDisplay().getRealMetrics(mDisplayMetrics);
128         mAccessibilityManager = AccessibilityManager.getInstance(mContext);
129     }
130 
131     @Override
onFinishInflate()132     protected void onFinishInflate() {
133         mActionContainerBackground = requireViewById(R.id.actions_container_background);
134         mActionContainer = requireViewById(R.id.actions);
135         mClipboardPreview = requireViewById(R.id.clipboard_preview);
136         mPreviewBorder = requireViewById(R.id.preview_border);
137         mImagePreview = requireViewById(R.id.image_preview);
138         mTextPreview = requireViewById(R.id.text_preview);
139         mHiddenPreview = requireViewById(R.id.hidden_preview);
140         mMinimizedPreview = requireViewById(R.id.minimized_preview);
141         mShareChip = requireViewById(R.id.share_chip);
142         mRemoteCopyChip = requireViewById(R.id.remote_copy_chip);
143         mDismissButton = requireViewById(R.id.dismiss_button);
144         mIndicationContainer = requireViewById(R.id.indication_container);
145         mIndicationText = mIndicationContainer.findViewById(R.id.indication_text);
146 
147         bindDefaultActionChips();
148 
149         mTextPreview.getViewTreeObserver().addOnPreDrawListener(() -> {
150             int availableHeight = mTextPreview.getHeight()
151                     - (mTextPreview.getPaddingTop() + mTextPreview.getPaddingBottom());
152             mTextPreview.setMaxLines(Math.max(availableHeight / mTextPreview.getLineHeight(), 1));
153             return true;
154         });
155         super.onFinishInflate();
156     }
157 
bindDefaultActionChips()158     private void bindDefaultActionChips() {
159         mActionButtonViewBinder.bind(mRemoteCopyChip,
160                 ActionButtonViewModel.Companion.withNextId(
161                         new ActionButtonAppearance(
162                                 Icon.createWithResource(mContext,
163                                         R.drawable.ic_baseline_devices_24).loadDrawable(
164                                         mContext),
165                                 null,
166                                 mContext.getString(R.string.clipboard_send_nearby_description),
167                                 true),
168                         new Function0<>() {
169                             @Override
170                             public Unit invoke() {
171                                 if (mClipboardCallbacks != null) {
172                                     mClipboardCallbacks.onRemoteCopyButtonTapped();
173                                 }
174                                 return null;
175                             }
176                         }));
177         mActionButtonViewBinder.bind(mShareChip,
178                 ActionButtonViewModel.Companion.withNextId(
179                         new ActionButtonAppearance(
180                                 Icon.createWithResource(mContext,
181                                         R.drawable.ic_screenshot_share).loadDrawable(mContext),
182                                 null,
183                                 mContext.getString(com.android.internal.R.string.share),
184                                 true),
185                         new Function0<>() {
186                             @Override
187                             public Unit invoke() {
188                                 if (mClipboardCallbacks != null) {
189                                     mClipboardCallbacks.onShareButtonTapped();
190                                 }
191                                 return null;
192                             }
193                         }));
194     }
195 
196     @Override
setCallbacks(SwipeDismissCallbacks callbacks)197     public void setCallbacks(SwipeDismissCallbacks callbacks) {
198         super.setCallbacks(callbacks);
199         ClipboardOverlayCallbacks clipboardCallbacks = (ClipboardOverlayCallbacks) callbacks;
200         mDismissButton.setOnClickListener(v -> clipboardCallbacks.onDismissButtonTapped());
201         mClipboardPreview.setOnClickListener(v -> clipboardCallbacks.onPreviewTapped());
202         mMinimizedPreview.setOnClickListener(v -> clipboardCallbacks.onMinimizedViewTapped());
203         mClipboardCallbacks = clipboardCallbacks;
204     }
205 
setEditAccessibilityAction(boolean editable)206     void setEditAccessibilityAction(boolean editable) {
207         if (editable) {
208             ViewCompat.replaceAccessibilityAction(mClipboardPreview,
209                     AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
210                     mContext.getString(R.string.clipboard_edit), null);
211         } else {
212             ViewCompat.replaceAccessibilityAction(mClipboardPreview,
213                     AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
214                     null, null);
215         }
216     }
217 
setIndicationText(CharSequence text)218     void setIndicationText(CharSequence text) {
219         mIndicationText.setText(text);
220 
221         // Set the visibility of clipboard indication based on the text is empty or not.
222         int visibility = text.isEmpty() ? View.GONE : View.VISIBLE;
223         mIndicationContainer.setVisibility(visibility);
224     }
225 
setMinimized(boolean minimized)226     void setMinimized(boolean minimized) {
227         if (minimized) {
228             mMinimizedPreview.setVisibility(View.VISIBLE);
229             mClipboardPreview.setVisibility(View.GONE);
230             mPreviewBorder.setVisibility(View.GONE);
231             mActionContainer.setVisibility(View.GONE);
232             mActionContainerBackground.setVisibility(View.GONE);
233         } else {
234             mMinimizedPreview.setVisibility(View.GONE);
235             mClipboardPreview.setVisibility(View.VISIBLE);
236             mPreviewBorder.setVisibility(View.VISIBLE);
237             mActionContainer.setVisibility(View.VISIBLE);
238         }
239 
240         if (showClipboardIndication()) {
241             // Adjust the margin of clipboard indication based on the minimized state.
242             int marginStart = minimized ? getResources().getDimensionPixelSize(
243                     R.dimen.overlay_action_container_margin_horizontal)
244                     : getResources().getDimensionPixelSize(
245                             R.dimen.overlay_action_container_minimum_edge_spacing);
246             ConstraintLayout.LayoutParams params =
247                     (ConstraintLayout.LayoutParams) mIndicationContainer.getLayoutParams();
248             params.setMarginStart(marginStart);
249             mIndicationContainer.setLayoutParams(params);
250         }
251     }
252 
setInsets(WindowInsets insets, int orientation)253     void setInsets(WindowInsets insets, int orientation) {
254         FrameLayout.LayoutParams p = (FrameLayout.LayoutParams) getLayoutParams();
255         if (p == null) {
256             return;
257         }
258         Rect margins = computeMargins(insets, orientation);
259 
260         p.setMargins(margins.left, margins.top, margins.right, margins.bottom);
261         setLayoutParams(p);
262         requestLayout();
263     }
264 
isInTouchRegion(int x, int y)265     boolean isInTouchRegion(int x, int y) {
266         Region touchRegion = new Region();
267         final Rect tmpRect = new Rect();
268 
269         mPreviewBorder.getBoundsOnScreen(tmpRect);
270         tmpRect.inset(
271                 (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP),
272                 (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP));
273         touchRegion.op(tmpRect, Region.Op.UNION);
274 
275         mActionContainerBackground.getBoundsOnScreen(tmpRect);
276         tmpRect.inset(
277                 (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP),
278                 (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP));
279         touchRegion.op(tmpRect, Region.Op.UNION);
280 
281         mMinimizedPreview.getBoundsOnScreen(tmpRect);
282         tmpRect.inset(
283                 (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP),
284                 (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP));
285         touchRegion.op(tmpRect, Region.Op.UNION);
286 
287         mDismissButton.getBoundsOnScreen(tmpRect);
288         touchRegion.op(tmpRect, Region.Op.UNION);
289 
290         return touchRegion.contains(x, y);
291     }
292 
setRemoteCopyVisibility(boolean visible)293     void setRemoteCopyVisibility(boolean visible) {
294         if (visible) {
295             mRemoteCopyChip.setVisibility(View.VISIBLE);
296             mActionContainerBackground.setVisibility(View.VISIBLE);
297         } else {
298             mRemoteCopyChip.setVisibility(View.GONE);
299         }
300     }
301 
showDefaultTextPreview()302     void showDefaultTextPreview() {
303         String copied = mContext.getString(R.string.clipboard_overlay_text_copied);
304         showTextPreview(copied, false);
305     }
306 
showTextPreview(CharSequence text, boolean hidden)307     void showTextPreview(CharSequence text, boolean hidden) {
308         TextView textView = hidden ? mHiddenPreview : mTextPreview;
309         showSinglePreview(textView);
310         textView.setText(text.subSequence(0, Math.min(500, text.length())));
311         updateTextSize(text, textView);
312         textView.addOnLayoutChangeListener(
313                 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
314                     if (right - left != oldRight - oldLeft) {
315                         updateTextSize(text, textView);
316                     }
317                 });
318     }
319 
getPreview()320     View getPreview() {
321         return mClipboardPreview;
322     }
323 
showImagePreview(@ullable Bitmap thumbnail)324     void showImagePreview(@Nullable Bitmap thumbnail) {
325         if (thumbnail == null) {
326             mHiddenPreview.setText(mContext.getString(R.string.clipboard_text_hidden));
327             showSinglePreview(mHiddenPreview);
328         } else {
329             mImagePreview.setImageBitmap(thumbnail);
330             showSinglePreview(mImagePreview);
331         }
332     }
333 
showShareChip()334     void showShareChip() {
335         mShareChip.setVisibility(View.VISIBLE);
336         mActionContainerBackground.setVisibility(View.VISIBLE);
337     }
338 
reset()339     void reset() {
340         setTranslationX(0);
341         setAlpha(0);
342         mActionContainerBackground.setVisibility(View.GONE);
343         mIndicationContainer.setVisibility(View.GONE);
344         mDismissButton.setVisibility(View.GONE);
345         mShareChip.setVisibility(View.GONE);
346         mRemoteCopyChip.setVisibility(View.GONE);
347         setEditAccessibilityAction(false);
348         resetActionChips();
349     }
350 
resetActionChips()351     void resetActionChips() {
352         for (View chip : mActionChips) {
353             mActionContainer.removeView(chip);
354         }
355         mActionChips.clear();
356     }
357 
getMinimizedFadeoutAnimation()358     Animator getMinimizedFadeoutAnimation() {
359         ObjectAnimator anim = ObjectAnimator.ofFloat(mMinimizedPreview, "alpha", 1, 0);
360         anim.setDuration(66);
361         anim.addListener(new AnimatorListenerAdapter() {
362             @Override
363             public void onAnimationEnd(Animator animation) {
364                 super.onAnimationEnd(animation);
365                 mMinimizedPreview.setVisibility(View.GONE);
366                 mMinimizedPreview.setAlpha(1);
367             }
368         });
369         return anim;
370     }
371 
getEnterAnimation()372     Animator getEnterAnimation() {
373         if (mAccessibilityManager.isEnabled()) {
374             mDismissButton.setVisibility(View.VISIBLE);
375         }
376         TimeInterpolator linearInterpolator = new LinearInterpolator();
377         TimeInterpolator scaleInterpolator = new PathInterpolator(0, 0, 0, 1f);
378         AnimatorSet enterAnim = new AnimatorSet();
379 
380         ValueAnimator rootAnim = ValueAnimator.ofFloat(0, 1);
381         rootAnim.setInterpolator(linearInterpolator);
382         rootAnim.setDuration(66);
383         rootAnim.addUpdateListener(animation -> {
384             setAlpha(animation.getAnimatedFraction());
385         });
386 
387         ValueAnimator scaleAnim = ValueAnimator.ofFloat(0, 1);
388         scaleAnim.setInterpolator(scaleInterpolator);
389         scaleAnim.setDuration(333);
390         scaleAnim.addUpdateListener(animation -> {
391             float previewScale = MathUtils.lerp(.9f, 1f, animation.getAnimatedFraction());
392             mMinimizedPreview.setScaleX(previewScale);
393             mMinimizedPreview.setScaleY(previewScale);
394             mClipboardPreview.setScaleX(previewScale);
395             mClipboardPreview.setScaleY(previewScale);
396             mPreviewBorder.setScaleX(previewScale);
397             mPreviewBorder.setScaleY(previewScale);
398 
399             float pivotX = mClipboardPreview.getWidth() / 2f + mClipboardPreview.getX();
400             mActionContainerBackground.setPivotX(pivotX - mActionContainerBackground.getX());
401             mActionContainer.setPivotX(pivotX - ((View) mActionContainer.getParent()).getX());
402             float actionsScaleX = MathUtils.lerp(.7f, 1f, animation.getAnimatedFraction());
403             float actionsScaleY = MathUtils.lerp(.9f, 1f, animation.getAnimatedFraction());
404             mActionContainer.setScaleX(actionsScaleX);
405             mActionContainer.setScaleY(actionsScaleY);
406             mActionContainerBackground.setScaleX(actionsScaleX);
407             mActionContainerBackground.setScaleY(actionsScaleY);
408         });
409 
410         ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1);
411         alphaAnim.setInterpolator(linearInterpolator);
412         alphaAnim.setDuration(283);
413         alphaAnim.addUpdateListener(animation -> {
414             float alpha = animation.getAnimatedFraction();
415             mMinimizedPreview.setAlpha(alpha);
416             mClipboardPreview.setAlpha(alpha);
417             mPreviewBorder.setAlpha(alpha);
418             mDismissButton.setAlpha(alpha);
419             mActionContainer.setAlpha(alpha);
420         });
421 
422         mMinimizedPreview.setAlpha(0);
423         mActionContainer.setAlpha(0);
424         mPreviewBorder.setAlpha(0);
425         mClipboardPreview.setAlpha(0);
426         enterAnim.play(rootAnim).with(scaleAnim);
427         enterAnim.play(alphaAnim).after(50).after(rootAnim);
428 
429         enterAnim.addListener(new AnimatorListenerAdapter() {
430             @Override
431             public void onAnimationEnd(Animator animation) {
432                 super.onAnimationEnd(animation);
433                 setAlpha(1);
434             }
435         });
436         return enterAnim;
437     }
438 
getFadeOutAnimation()439     Animator getFadeOutAnimation() {
440         ValueAnimator alphaAnim = ValueAnimator.ofFloat(1, 0);
441         alphaAnim.addUpdateListener(animation -> {
442             float alpha = (float) animation.getAnimatedValue();
443             mActionContainer.setAlpha(alpha);
444             mActionContainerBackground.setAlpha(alpha);
445             mPreviewBorder.setAlpha(alpha);
446             mDismissButton.setAlpha(alpha);
447         });
448         alphaAnim.setDuration(300);
449         return alphaAnim;
450     }
451 
getExitAnimation()452     Animator getExitAnimation() {
453         TimeInterpolator linearInterpolator = new LinearInterpolator();
454         TimeInterpolator scaleInterpolator = new PathInterpolator(.3f, 0, 1f, 1f);
455         AnimatorSet exitAnim = new AnimatorSet();
456 
457         ValueAnimator rootAnim = ValueAnimator.ofFloat(0, 1);
458         rootAnim.setInterpolator(linearInterpolator);
459         rootAnim.setDuration(100);
460         rootAnim.addUpdateListener(anim -> setAlpha(1 - anim.getAnimatedFraction()));
461 
462         ValueAnimator scaleAnim = ValueAnimator.ofFloat(0, 1);
463         scaleAnim.setInterpolator(scaleInterpolator);
464         scaleAnim.setDuration(250);
465         scaleAnim.addUpdateListener(animation -> {
466             float previewScale = MathUtils.lerp(1f, .9f, animation.getAnimatedFraction());
467             mMinimizedPreview.setScaleX(previewScale);
468             mMinimizedPreview.setScaleY(previewScale);
469             mClipboardPreview.setScaleX(previewScale);
470             mClipboardPreview.setScaleY(previewScale);
471             mPreviewBorder.setScaleX(previewScale);
472             mPreviewBorder.setScaleY(previewScale);
473 
474             float pivotX = mClipboardPreview.getWidth() / 2f + mClipboardPreview.getX();
475             mActionContainerBackground.setPivotX(pivotX - mActionContainerBackground.getX());
476             mActionContainer.setPivotX(pivotX - ((View) mActionContainer.getParent()).getX());
477             float actionScaleX = MathUtils.lerp(1f, .8f, animation.getAnimatedFraction());
478             float actionScaleY = MathUtils.lerp(1f, .9f, animation.getAnimatedFraction());
479             mActionContainer.setScaleX(actionScaleX);
480             mActionContainer.setScaleY(actionScaleY);
481             mActionContainerBackground.setScaleX(actionScaleX);
482             mActionContainerBackground.setScaleY(actionScaleY);
483         });
484 
485         ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1);
486         alphaAnim.setInterpolator(linearInterpolator);
487         alphaAnim.setDuration(166);
488         alphaAnim.addUpdateListener(animation -> {
489             float alpha = 1 - animation.getAnimatedFraction();
490             mMinimizedPreview.setAlpha(alpha);
491             mClipboardPreview.setAlpha(alpha);
492             mPreviewBorder.setAlpha(alpha);
493             mDismissButton.setAlpha(alpha);
494             mActionContainer.setAlpha(alpha);
495         });
496 
497         exitAnim.play(alphaAnim).with(scaleAnim);
498         exitAnim.play(rootAnim).after(150).after(alphaAnim);
499         return exitAnim;
500     }
501 
setActionChip(RemoteAction action, Runnable onFinish)502     void setActionChip(RemoteAction action, Runnable onFinish) {
503         mActionContainerBackground.setVisibility(View.VISIBLE);
504         View chip = constructShelfActionChip(action, onFinish);
505         mActionContainer.addView(chip);
506         mActionChips.add(chip);
507     }
508 
showSinglePreview(View v)509     private void showSinglePreview(View v) {
510         mTextPreview.setVisibility(View.GONE);
511         mImagePreview.setVisibility(View.GONE);
512         mHiddenPreview.setVisibility(View.GONE);
513         mMinimizedPreview.setVisibility(View.GONE);
514         v.setVisibility(View.VISIBLE);
515     }
516 
constructShelfActionChip(RemoteAction action, Runnable onFinish)517     private View constructShelfActionChip(RemoteAction action, Runnable onFinish) {
518         View chip = LayoutInflater.from(mContext).inflate(
519                 R.layout.shelf_action_chip, mActionContainer, false);
520         mActionButtonViewBinder.bind(chip, ActionButtonViewModel.Companion.withNextId(
521                 new ActionButtonAppearance(action.getIcon().loadDrawable(mContext),
522                         action.getTitle(), action.getTitle(), false), new Function0<>() {
523                     @Override
524                     public Unit invoke() {
525                         try {
526                             action.getActionIntent().send();
527                             onFinish.run();
528                         } catch (PendingIntent.CanceledException e) {
529                             Log.e(TAG, "Failed to send intent");
530                         }
531                         return null;
532                     }
533                 }));
534 
535         return chip;
536     }
537 
updateTextSize(CharSequence text, TextView textView)538     private static void updateTextSize(CharSequence text, TextView textView) {
539         Paint paint = new Paint(textView.getPaint());
540         Resources res = textView.getResources();
541         float minFontSize = res.getDimensionPixelSize(R.dimen.clipboard_overlay_min_font);
542         float maxFontSize = res.getDimensionPixelSize(R.dimen.clipboard_overlay_max_font);
543         if (isOneWord(text) && fitsInView(text, textView, paint, minFontSize)) {
544             // If the text is a single word and would fit within the TextView at the min font size,
545             // find the biggest font size that will fit.
546             float fontSizePx = minFontSize;
547             while (fontSizePx + FONT_SEARCH_STEP_PX < maxFontSize
548                     && fitsInView(text, textView, paint, fontSizePx + FONT_SEARCH_STEP_PX)) {
549                 fontSizePx += FONT_SEARCH_STEP_PX;
550             }
551             // Need to turn off autosizing, otherwise setTextSize is a no-op.
552             textView.setAutoSizeTextTypeWithDefaults(TextView.AUTO_SIZE_TEXT_TYPE_NONE);
553             // It's possible to hit the max font size and not fill the width, so centering
554             // horizontally looks better in this case.
555             textView.setGravity(Gravity.CENTER);
556             textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, (int) fontSizePx);
557         } else {
558             // Otherwise just stick with autosize.
559             textView.setAutoSizeTextTypeUniformWithConfiguration((int) minFontSize,
560                     (int) maxFontSize, FONT_SEARCH_STEP_PX, TypedValue.COMPLEX_UNIT_PX);
561             textView.setGravity(Gravity.CENTER_VERTICAL | Gravity.START);
562         }
563     }
564 
fitsInView(CharSequence text, TextView textView, Paint paint, float fontSizePx)565     private static boolean fitsInView(CharSequence text, TextView textView, Paint paint,
566             float fontSizePx) {
567         paint.setTextSize(fontSizePx);
568         float size = paint.measureText(text.toString());
569         float availableWidth = textView.getWidth() - textView.getPaddingLeft()
570                 - textView.getPaddingRight();
571         return size < availableWidth;
572     }
573 
isOneWord(CharSequence text)574     private static boolean isOneWord(CharSequence text) {
575         return text.toString().split("\\s+", 2).length == 1;
576     }
577 
computeMargins(WindowInsets insets, int orientation)578     private static Rect computeMargins(WindowInsets insets, int orientation) {
579         DisplayCutout cutout = insets.getDisplayCutout();
580         Insets navBarInsets = insets.getInsets(WindowInsets.Type.navigationBars());
581         Insets imeInsets = insets.getInsets(WindowInsets.Type.ime());
582         if (cutout == null) {
583             return new Rect(0, 0, 0, Math.max(imeInsets.bottom, navBarInsets.bottom));
584         } else {
585             Insets waterfall = cutout.getWaterfallInsets();
586             if (orientation == ORIENTATION_PORTRAIT) {
587                 return new Rect(
588                         waterfall.left,
589                         Math.max(cutout.getSafeInsetTop(), waterfall.top),
590                         waterfall.right,
591                         Math.max(imeInsets.bottom,
592                                 Math.max(cutout.getSafeInsetBottom(),
593                                         Math.max(navBarInsets.bottom, waterfall.bottom))));
594             } else {
595                 return new Rect(
596                         waterfall.left,
597                         waterfall.top,
598                         waterfall.right,
599                         Math.max(imeInsets.bottom,
600                                 Math.max(navBarInsets.bottom, waterfall.bottom)));
601             }
602         }
603     }
604 }
605