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