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