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