1 /* 2 * Copyright (C) 2017 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 androidx.appcompat.widget; 18 19 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; 20 21 import android.content.Context; 22 import android.content.res.Resources; 23 import android.content.res.TypedArray; 24 import android.graphics.RectF; 25 import android.os.Build; 26 import android.text.Layout; 27 import android.text.StaticLayout; 28 import android.text.TextDirectionHeuristic; 29 import android.text.TextDirectionHeuristics; 30 import android.text.TextPaint; 31 import android.text.method.TransformationMethod; 32 import android.util.AttributeSet; 33 import android.util.DisplayMetrics; 34 import android.util.Log; 35 import android.util.TypedValue; 36 import android.widget.TextView; 37 38 import androidx.annotation.NonNull; 39 import androidx.annotation.Nullable; 40 import androidx.annotation.RequiresApi; 41 import androidx.annotation.RestrictTo; 42 import androidx.appcompat.R; 43 import androidx.core.widget.TextViewCompat; 44 45 import java.lang.reflect.Method; 46 import java.util.ArrayList; 47 import java.util.Arrays; 48 import java.util.Collections; 49 import java.util.List; 50 import java.util.concurrent.ConcurrentHashMap; 51 52 /** 53 * Utility class which encapsulates the logic for the TextView auto-size text feature added to 54 * the Android Framework in {@link android.os.Build.VERSION_CODES#O}. 55 * 56 * <p>A TextView can be instructed to let the size of the text expand or contract automatically to 57 * fill its layout based on the TextView's characteristics and boundaries. 58 */ 59 class AppCompatTextViewAutoSizeHelper { 60 private static final String TAG = "ACTVAutoSizeHelper"; 61 private static final RectF TEMP_RECTF = new RectF(); 62 // Default minimum size for auto-sizing text in scaled pixels. 63 private static final int DEFAULT_AUTO_SIZE_MIN_TEXT_SIZE_IN_SP = 12; 64 // Default maximum size for auto-sizing text in scaled pixels. 65 private static final int DEFAULT_AUTO_SIZE_MAX_TEXT_SIZE_IN_SP = 112; 66 // Default value for the step size in pixels. 67 private static final int DEFAULT_AUTO_SIZE_GRANULARITY_IN_PX = 1; 68 // Cache of TextView methods used via reflection; the key is the method name and the value is 69 // the method itself or null if it can not be found. 70 private static ConcurrentHashMap<String, Method> sTextViewMethodByNameCache = 71 new ConcurrentHashMap<>(); 72 // Use this to specify that any of the auto-size configuration int values have not been set. 73 static final float UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE = -1f; 74 // Ported from TextView#VERY_WIDE. Represents a maximum width in pixels the TextView takes when 75 // horizontal scrolling is activated. 76 private static final int VERY_WIDE = 1024 * 1024; 77 // Auto-size text type. 78 private int mAutoSizeTextType = TextViewCompat.AUTO_SIZE_TEXT_TYPE_NONE; 79 // Specify if auto-size text is needed. 80 private boolean mNeedsAutoSizeText = false; 81 // Step size for auto-sizing in pixels. 82 private float mAutoSizeStepGranularityInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE; 83 // Minimum text size for auto-sizing in pixels. 84 private float mAutoSizeMinTextSizeInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE; 85 // Maximum text size for auto-sizing in pixels. 86 private float mAutoSizeMaxTextSizeInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE; 87 // Contains a (specified or computed) distinct sorted set of text sizes in pixels to pick from 88 // when auto-sizing text. 89 private int[] mAutoSizeTextSizesInPx = new int[0]; 90 // Specifies whether auto-size should use the provided auto size steps set or if it should 91 // build the steps set using mAutoSizeMinTextSizeInPx, mAutoSizeMaxTextSizeInPx and 92 // mAutoSizeStepGranularityInPx. 93 private boolean mHasPresetAutoSizeValues = false; 94 private TextPaint mTempTextPaint; 95 96 private final TextView mTextView; 97 private final Context mContext; 98 99 AppCompatTextViewAutoSizeHelper(TextView textView) { 100 mTextView = textView; 101 mContext = mTextView.getContext(); 102 } 103 104 void loadFromAttributes(AttributeSet attrs, int defStyleAttr) { 105 float autoSizeMinTextSizeInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE; 106 float autoSizeMaxTextSizeInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE; 107 float autoSizeStepGranularityInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE; 108 109 TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.AppCompatTextView, 110 defStyleAttr, 0); 111 if (a.hasValue(R.styleable.AppCompatTextView_autoSizeTextType)) { 112 mAutoSizeTextType = a.getInt(R.styleable.AppCompatTextView_autoSizeTextType, 113 TextViewCompat.AUTO_SIZE_TEXT_TYPE_NONE); 114 } 115 if (a.hasValue(R.styleable.AppCompatTextView_autoSizeStepGranularity)) { 116 autoSizeStepGranularityInPx = a.getDimension( 117 R.styleable.AppCompatTextView_autoSizeStepGranularity, 118 UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE); 119 } 120 if (a.hasValue(R.styleable.AppCompatTextView_autoSizeMinTextSize)) { 121 autoSizeMinTextSizeInPx = a.getDimension( 122 R.styleable.AppCompatTextView_autoSizeMinTextSize, 123 UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE); 124 } 125 if (a.hasValue(R.styleable.AppCompatTextView_autoSizeMaxTextSize)) { 126 autoSizeMaxTextSizeInPx = a.getDimension( 127 R.styleable.AppCompatTextView_autoSizeMaxTextSize, 128 UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE); 129 } 130 if (a.hasValue(R.styleable.AppCompatTextView_autoSizePresetSizes)) { 131 final int autoSizeStepSizeArrayResId = a.getResourceId( 132 R.styleable.AppCompatTextView_autoSizePresetSizes, 0); 133 if (autoSizeStepSizeArrayResId > 0) { 134 final TypedArray autoSizePreDefTextSizes = a.getResources() 135 .obtainTypedArray(autoSizeStepSizeArrayResId); 136 setupAutoSizeUniformPresetSizes(autoSizePreDefTextSizes); 137 autoSizePreDefTextSizes.recycle(); 138 } 139 } 140 a.recycle(); 141 142 if (supportsAutoSizeText()) { 143 if (mAutoSizeTextType == TextViewCompat.AUTO_SIZE_TEXT_TYPE_UNIFORM) { 144 // If uniform auto-size has been specified but preset values have not been set then 145 // replace the auto-size configuration values that have not been specified with the 146 // defaults. 147 if (!mHasPresetAutoSizeValues) { 148 final DisplayMetrics displayMetrics = 149 mContext.getResources().getDisplayMetrics(); 150 151 if (autoSizeMinTextSizeInPx == UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE) { 152 autoSizeMinTextSizeInPx = TypedValue.applyDimension( 153 TypedValue.COMPLEX_UNIT_SP, 154 DEFAULT_AUTO_SIZE_MIN_TEXT_SIZE_IN_SP, 155 displayMetrics); 156 } 157 158 if (autoSizeMaxTextSizeInPx == UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE) { 159 autoSizeMaxTextSizeInPx = TypedValue.applyDimension( 160 TypedValue.COMPLEX_UNIT_SP, 161 DEFAULT_AUTO_SIZE_MAX_TEXT_SIZE_IN_SP, 162 displayMetrics); 163 } 164 165 if (autoSizeStepGranularityInPx 166 == UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE) { 167 autoSizeStepGranularityInPx = DEFAULT_AUTO_SIZE_GRANULARITY_IN_PX; 168 } 169 170 validateAndSetAutoSizeTextTypeUniformConfiguration(autoSizeMinTextSizeInPx, 171 autoSizeMaxTextSizeInPx, 172 autoSizeStepGranularityInPx); 173 } 174 175 setupAutoSizeText(); 176 } 177 } else { 178 mAutoSizeTextType = TextViewCompat.AUTO_SIZE_TEXT_TYPE_NONE; 179 } 180 } 181 182 /** 183 * Specify whether this widget should automatically scale the text to try to perfectly fit 184 * within the layout bounds by using the default auto-size configuration. 185 * 186 * @param autoSizeTextType the type of auto-size. Must be one of 187 * {@link TextViewCompat#AUTO_SIZE_TEXT_TYPE_NONE} or 188 * {@link TextViewCompat#AUTO_SIZE_TEXT_TYPE_UNIFORM} 189 * 190 * @attr ref R.styleable#AppCompatTextView_autoSizeTextType 191 * 192 * @see #getAutoSizeTextType() 193 * 194 * @hide 195 */ 196 @RestrictTo(LIBRARY_GROUP) 197 void setAutoSizeTextTypeWithDefaults(@TextViewCompat.AutoSizeTextType int autoSizeTextType) { 198 if (supportsAutoSizeText()) { 199 switch (autoSizeTextType) { 200 case TextViewCompat.AUTO_SIZE_TEXT_TYPE_NONE: 201 clearAutoSizeConfiguration(); 202 break; 203 case TextViewCompat.AUTO_SIZE_TEXT_TYPE_UNIFORM: 204 final DisplayMetrics displayMetrics = 205 mContext.getResources().getDisplayMetrics(); 206 final float autoSizeMinTextSizeInPx = TypedValue.applyDimension( 207 TypedValue.COMPLEX_UNIT_SP, 208 DEFAULT_AUTO_SIZE_MIN_TEXT_SIZE_IN_SP, 209 displayMetrics); 210 final float autoSizeMaxTextSizeInPx = TypedValue.applyDimension( 211 TypedValue.COMPLEX_UNIT_SP, 212 DEFAULT_AUTO_SIZE_MAX_TEXT_SIZE_IN_SP, 213 displayMetrics); 214 215 validateAndSetAutoSizeTextTypeUniformConfiguration( 216 autoSizeMinTextSizeInPx, 217 autoSizeMaxTextSizeInPx, 218 DEFAULT_AUTO_SIZE_GRANULARITY_IN_PX); 219 if (setupAutoSizeText()) { 220 autoSizeText(); 221 } 222 break; 223 default: 224 throw new IllegalArgumentException( 225 "Unknown auto-size text type: " + autoSizeTextType); 226 } 227 } 228 } 229 230 /** 231 * Specify whether this widget should automatically scale the text to try to perfectly fit 232 * within the layout bounds. If all the configuration params are valid the type of auto-size is 233 * set to {@link TextViewCompat#AUTO_SIZE_TEXT_TYPE_UNIFORM}. 234 * 235 * @param autoSizeMinTextSize the minimum text size available for auto-size 236 * @param autoSizeMaxTextSize the maximum text size available for auto-size 237 * @param autoSizeStepGranularity the auto-size step granularity. It is used in conjunction with 238 * the minimum and maximum text size in order to build the set of 239 * text sizes the system uses to choose from when auto-sizing 240 * @param unit the desired dimension unit for all sizes above. See {@link TypedValue} for the 241 * possible dimension units 242 * 243 * @throws IllegalArgumentException if any of the configuration params are invalid. 244 * 245 * @attr ref R.styleable#AppCompatTextView_autoSizeTextType 246 * @attr ref R.styleable#AppCompatTextView_autoSizeMinTextSize 247 * @attr ref R.styleable#AppCompatTextView_autoSizeMaxTextSize 248 * @attr ref R.styleable#AppCompatTextView_autoSizeStepGranularity 249 * 250 * @see #setAutoSizeTextTypeWithDefaults(int) 251 * @see #setAutoSizeTextTypeUniformWithPresetSizes(int[], int) 252 * @see #getAutoSizeMinTextSize() 253 * @see #getAutoSizeMaxTextSize() 254 * @see #getAutoSizeStepGranularity() 255 * @see #getAutoSizeTextAvailableSizes() 256 * 257 * @hide 258 */ 259 @RestrictTo(LIBRARY_GROUP) 260 void setAutoSizeTextTypeUniformWithConfiguration( 261 int autoSizeMinTextSize, 262 int autoSizeMaxTextSize, 263 int autoSizeStepGranularity, 264 int unit) throws IllegalArgumentException { 265 if (supportsAutoSizeText()) { 266 final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics(); 267 final float autoSizeMinTextSizeInPx = TypedValue.applyDimension( 268 unit, autoSizeMinTextSize, displayMetrics); 269 final float autoSizeMaxTextSizeInPx = TypedValue.applyDimension( 270 unit, autoSizeMaxTextSize, displayMetrics); 271 final float autoSizeStepGranularityInPx = TypedValue.applyDimension( 272 unit, autoSizeStepGranularity, displayMetrics); 273 274 validateAndSetAutoSizeTextTypeUniformConfiguration(autoSizeMinTextSizeInPx, 275 autoSizeMaxTextSizeInPx, 276 autoSizeStepGranularityInPx); 277 if (setupAutoSizeText()) { 278 autoSizeText(); 279 } 280 } 281 } 282 283 /** 284 * Specify whether this widget should automatically scale the text to try to perfectly fit 285 * within the layout bounds. If at least one value from the <code>presetSizes</code> is valid 286 * then the type of auto-size is set to {@link TextViewCompat#AUTO_SIZE_TEXT_TYPE_UNIFORM}. 287 * 288 * @param presetSizes an {@code int} array of sizes in pixels 289 * @param unit the desired dimension unit for the preset sizes above. See {@link TypedValue} for 290 * the possible dimension units 291 * 292 * @throws IllegalArgumentException if all of the <code>presetSizes</code> are invalid. 293 *_ 294 * @attr ref R.styleable#AppCompatTextView_autoSizeTextType 295 * @attr ref R.styleable#AppCompatTextView_autoSizePresetSizes 296 * 297 * @see #setAutoSizeTextTypeWithDefaults(int) 298 * @see #setAutoSizeTextTypeUniformWithConfiguration(int, int, int, int) 299 * @see #getAutoSizeMinTextSize() 300 * @see #getAutoSizeMaxTextSize() 301 * @see #getAutoSizeTextAvailableSizes() 302 * 303 * @hide 304 */ 305 @RestrictTo(LIBRARY_GROUP) 306 void setAutoSizeTextTypeUniformWithPresetSizes(@NonNull int[] presetSizes, int unit) 307 throws IllegalArgumentException { 308 if (supportsAutoSizeText()) { 309 final int presetSizesLength = presetSizes.length; 310 if (presetSizesLength > 0) { 311 int[] presetSizesInPx = new int[presetSizesLength]; 312 313 if (unit == TypedValue.COMPLEX_UNIT_PX) { 314 presetSizesInPx = Arrays.copyOf(presetSizes, presetSizesLength); 315 } else { 316 final DisplayMetrics displayMetrics = 317 mContext.getResources().getDisplayMetrics(); 318 // Convert all to sizes to pixels. 319 for (int i = 0; i < presetSizesLength; i++) { 320 presetSizesInPx[i] = Math.round(TypedValue.applyDimension(unit, 321 presetSizes[i], displayMetrics)); 322 } 323 } 324 325 mAutoSizeTextSizesInPx = cleanupAutoSizePresetSizes(presetSizesInPx); 326 if (!setupAutoSizeUniformPresetSizesConfiguration()) { 327 throw new IllegalArgumentException("None of the preset sizes is valid: " 328 + Arrays.toString(presetSizes)); 329 } 330 } else { 331 mHasPresetAutoSizeValues = false; 332 } 333 334 if (setupAutoSizeText()) { 335 autoSizeText(); 336 } 337 } 338 } 339 340 /** 341 * Returns the type of auto-size set for this widget. 342 * 343 * @return an {@code int} corresponding to one of the auto-size types: 344 * {@link TextViewCompat#AUTO_SIZE_TEXT_TYPE_NONE} or 345 * {@link TextViewCompat#AUTO_SIZE_TEXT_TYPE_UNIFORM} 346 * 347 * @attr ref R.styleable#AppCompatTextView_autoSizeTextType 348 * 349 * @see #setAutoSizeTextTypeWithDefaults(int) 350 * @see #setAutoSizeTextTypeUniformWithConfiguration(int, int, int, int) 351 * @see #setAutoSizeTextTypeUniformWithPresetSizes(int[], int) 352 * 353 * @hide 354 */ 355 @RestrictTo(LIBRARY_GROUP) 356 @TextViewCompat.AutoSizeTextType 357 int getAutoSizeTextType() { 358 return mAutoSizeTextType; 359 } 360 361 /** 362 * @return the current auto-size step granularity in pixels. 363 * 364 * @attr ref R.styleable#AppCompatTextView_autoSizeStepGranularity 365 * 366 * @see #setAutoSizeTextTypeUniformWithConfiguration(int, int, int, int) 367 * 368 * @hide 369 */ 370 @RestrictTo(LIBRARY_GROUP) 371 int getAutoSizeStepGranularity() { 372 return Math.round(mAutoSizeStepGranularityInPx); 373 } 374 375 /** 376 * @return the current auto-size minimum text size in pixels (the default is 12sp). Note that 377 * if auto-size has not been configured this function returns {@code -1}. 378 * 379 * @attr ref R.styleable#AppCompatTextView_autoSizeMinTextSize 380 * 381 * @see #setAutoSizeTextTypeUniformWithConfiguration(int, int, int, int) 382 * @see #setAutoSizeTextTypeUniformWithPresetSizes(int[], int) 383 * 384 * @hide 385 */ 386 @RestrictTo(LIBRARY_GROUP) 387 int getAutoSizeMinTextSize() { 388 return Math.round(mAutoSizeMinTextSizeInPx); 389 } 390 391 /** 392 * @return the current auto-size maximum text size in pixels (the default is 112sp). Note that 393 * if auto-size has not been configured this function returns {@code -1}. 394 * 395 * @attr ref R.styleable#AppCompatTextView_autoSizeMaxTextSize 396 * 397 * @see #setAutoSizeTextTypeUniformWithConfiguration(int, int, int, int) 398 * @see #setAutoSizeTextTypeUniformWithPresetSizes(int[], int) 399 * 400 * @hide 401 */ 402 @RestrictTo(LIBRARY_GROUP) 403 int getAutoSizeMaxTextSize() { 404 return Math.round(mAutoSizeMaxTextSizeInPx); 405 } 406 407 /** 408 * @return the current auto-size {@code int} sizes array (in pixels). 409 * 410 * @see #setAutoSizeTextTypeUniformWithConfiguration(int, int, int, int) 411 * @see #setAutoSizeTextTypeUniformWithPresetSizes(int[], int) 412 * 413 * @hide 414 */ 415 @RestrictTo(LIBRARY_GROUP) 416 int[] getAutoSizeTextAvailableSizes() { 417 return mAutoSizeTextSizesInPx; 418 } 419 420 private void setupAutoSizeUniformPresetSizes(TypedArray textSizes) { 421 final int textSizesLength = textSizes.length(); 422 final int[] parsedSizes = new int[textSizesLength]; 423 424 if (textSizesLength > 0) { 425 for (int i = 0; i < textSizesLength; i++) { 426 parsedSizes[i] = textSizes.getDimensionPixelSize(i, -1); 427 } 428 mAutoSizeTextSizesInPx = cleanupAutoSizePresetSizes(parsedSizes); 429 setupAutoSizeUniformPresetSizesConfiguration(); 430 } 431 } 432 433 private boolean setupAutoSizeUniformPresetSizesConfiguration() { 434 final int sizesLength = mAutoSizeTextSizesInPx.length; 435 mHasPresetAutoSizeValues = sizesLength > 0; 436 if (mHasPresetAutoSizeValues) { 437 mAutoSizeTextType = TextViewCompat.AUTO_SIZE_TEXT_TYPE_UNIFORM; 438 mAutoSizeMinTextSizeInPx = mAutoSizeTextSizesInPx[0]; 439 mAutoSizeMaxTextSizeInPx = mAutoSizeTextSizesInPx[sizesLength - 1]; 440 mAutoSizeStepGranularityInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE; 441 } 442 return mHasPresetAutoSizeValues; 443 } 444 445 // Returns distinct sorted positive values. 446 private int[] cleanupAutoSizePresetSizes(int[] presetValues) { 447 final int presetValuesLength = presetValues.length; 448 if (presetValuesLength == 0) { 449 return presetValues; 450 } 451 Arrays.sort(presetValues); 452 453 final List<Integer> uniqueValidSizes = new ArrayList<>(); 454 for (int i = 0; i < presetValuesLength; i++) { 455 final int currentPresetValue = presetValues[i]; 456 457 if (currentPresetValue > 0 458 && Collections.binarySearch(uniqueValidSizes, currentPresetValue) < 0) { 459 uniqueValidSizes.add(currentPresetValue); 460 } 461 } 462 463 if (presetValuesLength == uniqueValidSizes.size()) { 464 return presetValues; 465 } else { 466 final int uniqueValidSizesLength = uniqueValidSizes.size(); 467 final int[] cleanedUpSizes = new int[uniqueValidSizesLength]; 468 for (int i = 0; i < uniqueValidSizesLength; i++) { 469 cleanedUpSizes[i] = uniqueValidSizes.get(i); 470 } 471 return cleanedUpSizes; 472 } 473 } 474 475 /** 476 * If all params are valid then save the auto-size configuration. 477 * 478 * @throws IllegalArgumentException if any of the params are invalid 479 */ 480 private void validateAndSetAutoSizeTextTypeUniformConfiguration( 481 float autoSizeMinTextSizeInPx, 482 float autoSizeMaxTextSizeInPx, 483 float autoSizeStepGranularityInPx) throws IllegalArgumentException { 484 // First validate. 485 if (autoSizeMinTextSizeInPx <= 0) { 486 throw new IllegalArgumentException("Minimum auto-size text size (" 487 + autoSizeMinTextSizeInPx + "px) is less or equal to (0px)"); 488 } 489 490 if (autoSizeMaxTextSizeInPx <= autoSizeMinTextSizeInPx) { 491 throw new IllegalArgumentException("Maximum auto-size text size (" 492 + autoSizeMaxTextSizeInPx + "px) is less or equal to minimum auto-size " 493 + "text size (" + autoSizeMinTextSizeInPx + "px)"); 494 } 495 496 if (autoSizeStepGranularityInPx <= 0) { 497 throw new IllegalArgumentException("The auto-size step granularity (" 498 + autoSizeStepGranularityInPx + "px) is less or equal to (0px)"); 499 } 500 501 // All good, persist the configuration. 502 mAutoSizeTextType = TextViewCompat.AUTO_SIZE_TEXT_TYPE_UNIFORM; 503 mAutoSizeMinTextSizeInPx = autoSizeMinTextSizeInPx; 504 mAutoSizeMaxTextSizeInPx = autoSizeMaxTextSizeInPx; 505 mAutoSizeStepGranularityInPx = autoSizeStepGranularityInPx; 506 mHasPresetAutoSizeValues = false; 507 } 508 509 private boolean setupAutoSizeText() { 510 if (supportsAutoSizeText() 511 && mAutoSizeTextType == TextViewCompat.AUTO_SIZE_TEXT_TYPE_UNIFORM) { 512 // Calculate the sizes set based on minimum size, maximum size and step size if we do 513 // not have a predefined set of sizes or if the current sizes array is empty. 514 if (!mHasPresetAutoSizeValues || mAutoSizeTextSizesInPx.length == 0) { 515 // Calculate sizes to choose from based on the current auto-size configuration. 516 int autoSizeValuesLength = 1; 517 float currentSize = Math.round(mAutoSizeMinTextSizeInPx); 518 while (Math.round(currentSize + mAutoSizeStepGranularityInPx) 519 <= Math.round(mAutoSizeMaxTextSizeInPx)) { 520 autoSizeValuesLength++; 521 currentSize += mAutoSizeStepGranularityInPx; 522 } 523 int[] autoSizeTextSizesInPx = new int[autoSizeValuesLength]; 524 float sizeToAdd = mAutoSizeMinTextSizeInPx; 525 for (int i = 0; i < autoSizeValuesLength; i++) { 526 autoSizeTextSizesInPx[i] = Math.round(sizeToAdd); 527 sizeToAdd += mAutoSizeStepGranularityInPx; 528 } 529 mAutoSizeTextSizesInPx = cleanupAutoSizePresetSizes(autoSizeTextSizesInPx); 530 } 531 532 mNeedsAutoSizeText = true; 533 } else { 534 mNeedsAutoSizeText = false; 535 } 536 537 return mNeedsAutoSizeText; 538 } 539 540 /** 541 * Automatically computes and sets the text size. 542 * 543 * @hide 544 */ 545 @RestrictTo(LIBRARY_GROUP) 546 void autoSizeText() { 547 if (!isAutoSizeEnabled()) { 548 return; 549 } 550 551 if (mNeedsAutoSizeText) { 552 if (mTextView.getMeasuredHeight() <= 0 || mTextView.getMeasuredWidth() <= 0) { 553 return; 554 } 555 556 final boolean horizontallyScrolling = invokeAndReturnWithDefault( 557 mTextView, "getHorizontallyScrolling", false); 558 final int availableWidth = horizontallyScrolling 559 ? VERY_WIDE 560 : mTextView.getMeasuredWidth() - mTextView.getTotalPaddingLeft() 561 - mTextView.getTotalPaddingRight(); 562 final int availableHeight = mTextView.getHeight() - mTextView.getCompoundPaddingBottom() 563 - mTextView.getCompoundPaddingTop(); 564 565 if (availableWidth <= 0 || availableHeight <= 0) { 566 return; 567 } 568 569 synchronized (TEMP_RECTF) { 570 TEMP_RECTF.setEmpty(); 571 TEMP_RECTF.right = availableWidth; 572 TEMP_RECTF.bottom = availableHeight; 573 final float optimalTextSize = findLargestTextSizeWhichFits(TEMP_RECTF); 574 if (optimalTextSize != mTextView.getTextSize()) { 575 setTextSizeInternal(TypedValue.COMPLEX_UNIT_PX, optimalTextSize); 576 } 577 } 578 } 579 // Always try to auto-size if enabled. Functions that do not want to trigger auto-sizing 580 // after the next layout pass should set this to false. 581 mNeedsAutoSizeText = true; 582 } 583 584 private void clearAutoSizeConfiguration() { 585 mAutoSizeTextType = TextViewCompat.AUTO_SIZE_TEXT_TYPE_NONE; 586 mAutoSizeMinTextSizeInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE; 587 mAutoSizeMaxTextSizeInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE; 588 mAutoSizeStepGranularityInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE; 589 mAutoSizeTextSizesInPx = new int[0]; 590 mNeedsAutoSizeText = false; 591 } 592 593 /** @hide */ 594 @RestrictTo(LIBRARY_GROUP) 595 void setTextSizeInternal(int unit, float size) { 596 Resources res = mContext == null 597 ? Resources.getSystem() 598 : mContext.getResources(); 599 600 setRawTextSize(TypedValue.applyDimension(unit, size, res.getDisplayMetrics())); 601 } 602 603 private void setRawTextSize(float size) { 604 if (size != mTextView.getPaint().getTextSize()) { 605 mTextView.getPaint().setTextSize(size); 606 607 boolean isInLayout = false; 608 if (Build.VERSION.SDK_INT >= 18) { 609 isInLayout = mTextView.isInLayout(); 610 } 611 612 if (mTextView.getLayout() != null) { 613 // Do not auto-size right after setting the text size. 614 mNeedsAutoSizeText = false; 615 616 final String methodName = "nullLayouts"; 617 try { 618 Method method = getTextViewMethod(methodName); 619 if (method != null) { 620 method.invoke(mTextView); 621 } 622 } catch (Exception ex) { 623 Log.w(TAG, "Failed to invoke TextView#" + methodName + "() method", ex); 624 } 625 626 if (!isInLayout) { 627 mTextView.requestLayout(); 628 } else { 629 mTextView.forceLayout(); 630 } 631 632 mTextView.invalidate(); 633 } 634 } 635 } 636 637 /** 638 * Performs a binary search to find the largest text size that will still fit within the size 639 * available to this view. 640 */ 641 private int findLargestTextSizeWhichFits(RectF availableSpace) { 642 final int sizesCount = mAutoSizeTextSizesInPx.length; 643 if (sizesCount == 0) { 644 throw new IllegalStateException("No available text sizes to choose from."); 645 } 646 647 int bestSizeIndex = 0; 648 int lowIndex = bestSizeIndex + 1; 649 int highIndex = sizesCount - 1; 650 int sizeToTryIndex; 651 while (lowIndex <= highIndex) { 652 sizeToTryIndex = (lowIndex + highIndex) / 2; 653 if (suggestedSizeFitsInSpace(mAutoSizeTextSizesInPx[sizeToTryIndex], availableSpace)) { 654 bestSizeIndex = lowIndex; 655 lowIndex = sizeToTryIndex + 1; 656 } else { 657 highIndex = sizeToTryIndex - 1; 658 bestSizeIndex = highIndex; 659 } 660 } 661 662 return mAutoSizeTextSizesInPx[bestSizeIndex]; 663 } 664 665 private boolean suggestedSizeFitsInSpace(int suggestedSizeInPx, RectF availableSpace) { 666 CharSequence text = mTextView.getText(); 667 TransformationMethod transformationMethod = mTextView.getTransformationMethod(); 668 if (transformationMethod != null) { 669 CharSequence transformedText = transformationMethod.getTransformation(text, mTextView); 670 if (transformedText != null) { 671 text = transformedText; 672 } 673 } 674 675 final int maxLines = Build.VERSION.SDK_INT >= 16 ? mTextView.getMaxLines() : -1; 676 if (mTempTextPaint == null) { 677 mTempTextPaint = new TextPaint(); 678 } else { 679 mTempTextPaint.reset(); 680 } 681 mTempTextPaint.set(mTextView.getPaint()); 682 mTempTextPaint.setTextSize(suggestedSizeInPx); 683 684 // Needs reflection call due to being private. 685 Layout.Alignment alignment = invokeAndReturnWithDefault( 686 mTextView, "getLayoutAlignment", Layout.Alignment.ALIGN_NORMAL); 687 final StaticLayout layout = Build.VERSION.SDK_INT >= 23 688 ? createStaticLayoutForMeasuring( 689 text, alignment, Math.round(availableSpace.right), maxLines) 690 : createStaticLayoutForMeasuringPre23( 691 text, alignment, Math.round(availableSpace.right)); 692 // Lines overflow. 693 if (maxLines != -1 && (layout.getLineCount() > maxLines 694 || (layout.getLineEnd(layout.getLineCount() - 1)) != text.length())) { 695 return false; 696 } 697 698 // Height overflow. 699 if (layout.getHeight() > availableSpace.bottom) { 700 return false; 701 } 702 703 return true; 704 } 705 706 @RequiresApi(23) 707 private StaticLayout createStaticLayoutForMeasuring(CharSequence text, 708 Layout.Alignment alignment, int availableWidth, int maxLines) { 709 // Can use the StaticLayout.Builder (along with TextView params added in or after 710 // API 23) to construct the layout. 711 final TextDirectionHeuristic textDirectionHeuristic = invokeAndReturnWithDefault( 712 mTextView, "getTextDirectionHeuristic", 713 TextDirectionHeuristics.FIRSTSTRONG_LTR); 714 715 final StaticLayout.Builder layoutBuilder = StaticLayout.Builder.obtain( 716 text, 0, text.length(), mTempTextPaint, availableWidth); 717 718 return layoutBuilder.setAlignment(alignment) 719 .setLineSpacing( 720 mTextView.getLineSpacingExtra(), 721 mTextView.getLineSpacingMultiplier()) 722 .setIncludePad(mTextView.getIncludeFontPadding()) 723 .setBreakStrategy(mTextView.getBreakStrategy()) 724 .setHyphenationFrequency(mTextView.getHyphenationFrequency()) 725 .setMaxLines(maxLines == -1 ? Integer.MAX_VALUE : maxLines) 726 .setTextDirection(textDirectionHeuristic) 727 .build(); 728 } 729 730 private StaticLayout createStaticLayoutForMeasuringPre23(CharSequence text, 731 Layout.Alignment alignment, int availableWidth) { 732 // Setup defaults. 733 float lineSpacingMultiplier = 1.0f; 734 float lineSpacingAdd = 0.0f; 735 boolean includePad = true; 736 737 if (Build.VERSION.SDK_INT >= 16) { 738 // Call public methods. 739 lineSpacingMultiplier = mTextView.getLineSpacingMultiplier(); 740 lineSpacingAdd = mTextView.getLineSpacingExtra(); 741 includePad = mTextView.getIncludeFontPadding(); 742 } else { 743 // Call private methods and make sure to provide fallback defaults in case something 744 // goes wrong. The default values have been inlined with the StaticLayout defaults. 745 lineSpacingMultiplier = invokeAndReturnWithDefault(mTextView, 746 "getLineSpacingMultiplier", lineSpacingMultiplier); 747 lineSpacingAdd = invokeAndReturnWithDefault(mTextView, 748 "getLineSpacingExtra", lineSpacingAdd); 749 includePad = invokeAndReturnWithDefault(mTextView, 750 "getIncludeFontPadding", includePad); 751 } 752 753 // The layout could not be constructed using the builder so fall back to the 754 // most broad constructor. 755 return new StaticLayout(text, mTempTextPaint, availableWidth, 756 alignment, 757 lineSpacingMultiplier, 758 lineSpacingAdd, 759 includePad); 760 } 761 762 private <T> T invokeAndReturnWithDefault(@NonNull Object object, 763 @NonNull final String methodName, @NonNull final T defaultValue) { 764 T result = null; 765 boolean exceptionThrown = false; 766 767 try { 768 // Cache lookup. 769 Method method = getTextViewMethod(methodName); 770 result = (T) method.invoke(object); 771 } catch (Exception ex) { 772 exceptionThrown = true; 773 Log.w(TAG, "Failed to invoke TextView#" + methodName + "() method", ex); 774 } finally { 775 if (result == null && exceptionThrown) { 776 result = defaultValue; 777 } 778 } 779 780 return result; 781 } 782 783 @Nullable 784 private Method getTextViewMethod(@NonNull final String methodName) { 785 try { 786 Method method = sTextViewMethodByNameCache.get(methodName); 787 if (method == null) { 788 method = TextView.class.getDeclaredMethod(methodName); 789 if (method != null) { 790 method.setAccessible(true); 791 // Cache update. 792 sTextViewMethodByNameCache.put(methodName, method); 793 } 794 } 795 796 return method; 797 } catch (Exception ex) { 798 Log.w(TAG, "Failed to retrieve TextView#" + methodName + "() method", ex); 799 return null; 800 } 801 } 802 803 /** 804 * @return {@code true} if this widget supports auto-sizing text and has been configured to 805 * auto-size. 806 * 807 * @hide 808 */ 809 @RestrictTo(LIBRARY_GROUP) 810 boolean isAutoSizeEnabled() { 811 return supportsAutoSizeText() 812 && mAutoSizeTextType != TextViewCompat.AUTO_SIZE_TEXT_TYPE_NONE; 813 } 814 815 /** 816 * @return {@code true} if this TextView supports auto-sizing text to fit within its container. 817 */ 818 private boolean supportsAutoSizeText() { 819 // Auto-size only supports TextView and all siblings but EditText. 820 return !(mTextView instanceof AppCompatEditText); 821 } 822 } 823