1/* 2 * Copyright (C) 2015 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 17package android.support.v4.widget; 18 19import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; 20 21import android.graphics.drawable.Drawable; 22import android.os.Build; 23import android.support.annotation.DrawableRes; 24import android.support.annotation.IntDef; 25import android.support.annotation.NonNull; 26import android.support.annotation.Nullable; 27import android.support.annotation.RequiresApi; 28import android.support.annotation.RestrictTo; 29import android.support.annotation.StyleRes; 30import android.util.Log; 31import android.util.TypedValue; 32import android.view.View; 33import android.widget.TextView; 34 35import java.lang.annotation.Retention; 36import java.lang.annotation.RetentionPolicy; 37import java.lang.reflect.Field; 38 39/** 40 * Helper for accessing features in {@link TextView} in a backwards compatible fashion. 41 */ 42public final class TextViewCompat { 43 44 /** 45 * The TextView does not auto-size text (default). 46 */ 47 public static final int AUTO_SIZE_TEXT_TYPE_NONE = 0; 48 49 /** 50 * The TextView scales text size both horizontally and vertically to fit within the 51 * container. 52 */ 53 public static final int AUTO_SIZE_TEXT_TYPE_UNIFORM = 1; 54 55 /** @hide */ 56 @RestrictTo(LIBRARY_GROUP) 57 @IntDef({AUTO_SIZE_TEXT_TYPE_NONE, AUTO_SIZE_TEXT_TYPE_UNIFORM}) 58 @Retention(RetentionPolicy.SOURCE) 59 public @interface AutoSizeTextType {} 60 61 // Hide constructor 62 private TextViewCompat() {} 63 64 static class TextViewCompatBaseImpl { 65 private static final String LOG_TAG = "TextViewCompatBase"; 66 private static final int LINES = 1; 67 68 private static Field sMaximumField; 69 private static boolean sMaximumFieldFetched; 70 private static Field sMaxModeField; 71 private static boolean sMaxModeFieldFetched; 72 73 private static Field sMinimumField; 74 private static boolean sMinimumFieldFetched; 75 private static Field sMinModeField; 76 private static boolean sMinModeFieldFetched; 77 78 public void setCompoundDrawablesRelative(@NonNull TextView textView, 79 @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end, 80 @Nullable Drawable bottom) { 81 textView.setCompoundDrawables(start, top, end, bottom); 82 } 83 84 public void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView, 85 @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end, 86 @Nullable Drawable bottom) { 87 textView.setCompoundDrawablesWithIntrinsicBounds(start, top, end, bottom); 88 } 89 90 public void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView, 91 @DrawableRes int start, @DrawableRes int top, @DrawableRes int end, 92 @DrawableRes int bottom) { 93 textView.setCompoundDrawablesWithIntrinsicBounds(start, top, end, bottom); 94 } 95 96 private static Field retrieveField(String fieldName) { 97 Field field = null; 98 try { 99 field = TextView.class.getDeclaredField(fieldName); 100 field.setAccessible(true); 101 } catch (NoSuchFieldException e) { 102 Log.e(LOG_TAG, "Could not retrieve " + fieldName + " field."); 103 } 104 return field; 105 } 106 107 private static int retrieveIntFromField(Field field, TextView textView) { 108 try { 109 return field.getInt(textView); 110 } catch (IllegalAccessException e) { 111 Log.d(LOG_TAG, "Could not retrieve value of " + field.getName() + " field."); 112 } 113 return -1; 114 } 115 116 public int getMaxLines(TextView textView) { 117 if (!sMaxModeFieldFetched) { 118 sMaxModeField = retrieveField("mMaxMode"); 119 sMaxModeFieldFetched = true; 120 } 121 if (sMaxModeField != null && retrieveIntFromField(sMaxModeField, textView) == LINES) { 122 // If the max mode is using lines, we can grab the maximum value 123 if (!sMaximumFieldFetched) { 124 sMaximumField = retrieveField("mMaximum"); 125 sMaximumFieldFetched = true; 126 } 127 if (sMaximumField != null) { 128 return retrieveIntFromField(sMaximumField, textView); 129 } 130 } 131 return -1; 132 } 133 134 public int getMinLines(TextView textView) { 135 if (!sMinModeFieldFetched) { 136 sMinModeField = retrieveField("mMinMode"); 137 sMinModeFieldFetched = true; 138 } 139 if (sMinModeField != null && retrieveIntFromField(sMinModeField, textView) == LINES) { 140 // If the min mode is using lines, we can grab the maximum value 141 if (!sMinimumFieldFetched) { 142 sMinimumField = retrieveField("mMinimum"); 143 sMinimumFieldFetched = true; 144 } 145 if (sMinimumField != null) { 146 return retrieveIntFromField(sMinimumField, textView); 147 } 148 } 149 return -1; 150 } 151 152 @SuppressWarnings("deprecation") 153 public void setTextAppearance(TextView textView, @StyleRes int resId) { 154 textView.setTextAppearance(textView.getContext(), resId); 155 } 156 157 public Drawable[] getCompoundDrawablesRelative(@NonNull TextView textView) { 158 return textView.getCompoundDrawables(); 159 } 160 161 public void setAutoSizeTextTypeWithDefaults(TextView textView, int autoSizeTextType) { 162 if (textView instanceof AutoSizeableTextView) { 163 ((AutoSizeableTextView) textView).setAutoSizeTextTypeWithDefaults(autoSizeTextType); 164 } 165 } 166 167 public void setAutoSizeTextTypeUniformWithConfiguration( 168 TextView textView, 169 int autoSizeMinTextSize, 170 int autoSizeMaxTextSize, 171 int autoSizeStepGranularity, 172 int unit) throws IllegalArgumentException { 173 if (textView instanceof AutoSizeableTextView) { 174 ((AutoSizeableTextView) textView).setAutoSizeTextTypeUniformWithConfiguration( 175 autoSizeMinTextSize, autoSizeMaxTextSize, autoSizeStepGranularity, unit); 176 } 177 } 178 179 public void setAutoSizeTextTypeUniformWithPresetSizes(TextView textView, 180 @NonNull int[] presetSizes, int unit) throws IllegalArgumentException { 181 if (textView instanceof AutoSizeableTextView) { 182 ((AutoSizeableTextView) textView).setAutoSizeTextTypeUniformWithPresetSizes( 183 presetSizes, unit); 184 } 185 } 186 187 public int getAutoSizeTextType(TextView textView) { 188 if (textView instanceof AutoSizeableTextView) { 189 return ((AutoSizeableTextView) textView).getAutoSizeTextType(); 190 } 191 return AUTO_SIZE_TEXT_TYPE_NONE; 192 } 193 194 public int getAutoSizeStepGranularity(TextView textView) { 195 if (textView instanceof AutoSizeableTextView) { 196 return ((AutoSizeableTextView) textView).getAutoSizeStepGranularity(); 197 } 198 return -1; 199 } 200 201 public int getAutoSizeMinTextSize(TextView textView) { 202 if (textView instanceof AutoSizeableTextView) { 203 return ((AutoSizeableTextView) textView).getAutoSizeMinTextSize(); 204 } 205 return -1; 206 } 207 208 public int getAutoSizeMaxTextSize(TextView textView) { 209 if (textView instanceof AutoSizeableTextView) { 210 return ((AutoSizeableTextView) textView).getAutoSizeMaxTextSize(); 211 } 212 return -1; 213 } 214 215 public int[] getAutoSizeTextAvailableSizes(TextView textView) { 216 if (textView instanceof AutoSizeableTextView) { 217 return ((AutoSizeableTextView) textView).getAutoSizeTextAvailableSizes(); 218 } 219 return new int[0]; 220 } 221 } 222 223 @RequiresApi(16) 224 static class TextViewCompatApi16Impl extends TextViewCompatBaseImpl { 225 @Override 226 public int getMaxLines(TextView textView) { 227 return textView.getMaxLines(); 228 } 229 230 @Override 231 public int getMinLines(TextView textView) { 232 return textView.getMinLines(); 233 } 234 } 235 236 @RequiresApi(17) 237 static class TextViewCompatApi17Impl extends TextViewCompatApi16Impl { 238 @Override 239 public void setCompoundDrawablesRelative(@NonNull TextView textView, 240 @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end, 241 @Nullable Drawable bottom) { 242 boolean rtl = textView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; 243 textView.setCompoundDrawables(rtl ? end : start, top, rtl ? start : end, bottom); 244 } 245 246 @Override 247 public void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView, 248 @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end, 249 @Nullable Drawable bottom) { 250 boolean rtl = textView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; 251 textView.setCompoundDrawablesWithIntrinsicBounds(rtl ? end : start, top, 252 rtl ? start : end, bottom); 253 } 254 255 @Override 256 public void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView, 257 @DrawableRes int start, @DrawableRes int top, @DrawableRes int end, 258 @DrawableRes int bottom) { 259 boolean rtl = textView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; 260 textView.setCompoundDrawablesWithIntrinsicBounds(rtl ? end : start, top, 261 rtl ? start : end, bottom); 262 } 263 264 @Override 265 public Drawable[] getCompoundDrawablesRelative(@NonNull TextView textView) { 266 final boolean rtl = textView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; 267 final Drawable[] compounds = textView.getCompoundDrawables(); 268 if (rtl) { 269 // If we're on RTL, we need to invert the horizontal result like above 270 final Drawable start = compounds[2]; 271 final Drawable end = compounds[0]; 272 compounds[0] = start; 273 compounds[2] = end; 274 } 275 return compounds; 276 } 277 } 278 279 @RequiresApi(18) 280 static class TextViewCompatApi18Impl extends TextViewCompatApi17Impl { 281 @Override 282 public void setCompoundDrawablesRelative(@NonNull TextView textView, 283 @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end, 284 @Nullable Drawable bottom) { 285 textView.setCompoundDrawablesRelative(start, top, end, bottom); 286 } 287 288 @Override 289 public void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView, 290 @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end, 291 @Nullable Drawable bottom) { 292 textView.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom); 293 } 294 295 @Override 296 public void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView, 297 @DrawableRes int start, @DrawableRes int top, @DrawableRes int end, 298 @DrawableRes int bottom) { 299 textView.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom); 300 } 301 302 @Override 303 public Drawable[] getCompoundDrawablesRelative(@NonNull TextView textView) { 304 return textView.getCompoundDrawablesRelative(); 305 } 306 } 307 308 @RequiresApi(23) 309 static class TextViewCompatApi23Impl extends TextViewCompatApi18Impl { 310 @Override 311 public void setTextAppearance(@NonNull TextView textView, @StyleRes int resId) { 312 textView.setTextAppearance(resId); 313 } 314 } 315 316 @RequiresApi(26) 317 static class TextViewCompatApi26Impl extends TextViewCompatApi23Impl { 318 @Override 319 public void setAutoSizeTextTypeWithDefaults(TextView textView, int autoSizeTextType) { 320 textView.setAutoSizeTextTypeWithDefaults(autoSizeTextType); 321 } 322 323 @Override 324 public void setAutoSizeTextTypeUniformWithConfiguration( 325 TextView textView, 326 int autoSizeMinTextSize, 327 int autoSizeMaxTextSize, 328 int autoSizeStepGranularity, 329 int unit) throws IllegalArgumentException { 330 textView.setAutoSizeTextTypeUniformWithConfiguration( 331 autoSizeMinTextSize, autoSizeMaxTextSize, autoSizeStepGranularity, unit); 332 } 333 334 @Override 335 public void setAutoSizeTextTypeUniformWithPresetSizes(TextView textView, 336 @NonNull int[] presetSizes, int unit) throws IllegalArgumentException { 337 textView.setAutoSizeTextTypeUniformWithPresetSizes(presetSizes, unit); 338 } 339 340 @Override 341 public int getAutoSizeTextType(TextView textView) { 342 return textView.getAutoSizeTextType(); 343 } 344 345 @Override 346 public int getAutoSizeStepGranularity(TextView textView) { 347 return textView.getAutoSizeStepGranularity(); 348 } 349 350 @Override 351 public int getAutoSizeMinTextSize(TextView textView) { 352 return textView.getAutoSizeMinTextSize(); 353 } 354 355 @Override 356 public int getAutoSizeMaxTextSize(TextView textView) { 357 return textView.getAutoSizeMaxTextSize(); 358 } 359 360 @Override 361 public int[] getAutoSizeTextAvailableSizes(TextView textView) { 362 return textView.getAutoSizeTextAvailableSizes(); 363 } 364 } 365 366 static final TextViewCompatBaseImpl IMPL; 367 368 static { 369 if (Build.VERSION.SDK_INT >= 26) { 370 IMPL = new TextViewCompatApi26Impl(); 371 } else if (Build.VERSION.SDK_INT >= 23) { 372 IMPL = new TextViewCompatApi23Impl(); 373 } else if (Build.VERSION.SDK_INT >= 18) { 374 IMPL = new TextViewCompatApi18Impl(); 375 } else if (Build.VERSION.SDK_INT >= 17) { 376 IMPL = new TextViewCompatApi17Impl(); 377 } else if (Build.VERSION.SDK_INT >= 16) { 378 IMPL = new TextViewCompatApi16Impl(); 379 } else { 380 IMPL = new TextViewCompatBaseImpl(); 381 } 382 } 383 384 /** 385 * Sets the Drawables (if any) to appear to the start of, above, to the end 386 * of, and below the text. Use {@code null} if you do not want a Drawable 387 * there. The Drawables must already have had {@link Drawable#setBounds} 388 * called. 389 * <p/> 390 * Calling this method will overwrite any Drawables previously set using 391 * {@link TextView#setCompoundDrawables} or related methods. 392 * 393 * @param textView The TextView against which to invoke the method. 394 * @attr name android:drawableStart 395 * @attr name android:drawableTop 396 * @attr name android:drawableEnd 397 * @attr name android:drawableBottom 398 */ 399 public static void setCompoundDrawablesRelative(@NonNull TextView textView, 400 @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end, 401 @Nullable Drawable bottom) { 402 IMPL.setCompoundDrawablesRelative(textView, start, top, end, bottom); 403 } 404 405 /** 406 * Sets the Drawables (if any) to appear to the start of, above, to the end 407 * of, and below the text. Use {@code null} if you do not want a Drawable 408 * there. The Drawables' bounds will be set to their intrinsic bounds. 409 * <p/> 410 * Calling this method will overwrite any Drawables previously set using 411 * {@link TextView#setCompoundDrawables} or related methods. 412 * 413 * @param textView The TextView against which to invoke the method. 414 * @attr name android:drawableStart 415 * @attr name android:drawableTop 416 * @attr name android:drawableEnd 417 * @attr name android:drawableBottom 418 */ 419 public static void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView, 420 @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end, 421 @Nullable Drawable bottom) { 422 IMPL.setCompoundDrawablesRelativeWithIntrinsicBounds(textView, start, top, end, bottom); 423 } 424 425 /** 426 * Sets the Drawables (if any) to appear to the start of, above, to the end 427 * of, and below the text. Use 0 if you do not want a Drawable there. The 428 * Drawables' bounds will be set to their intrinsic bounds. 429 * <p/> 430 * Calling this method will overwrite any Drawables previously set using 431 * {@link TextView#setCompoundDrawables} or related methods. 432 * 433 * @param textView The TextView against which to invoke the method. 434 * @param start Resource identifier of the start Drawable. 435 * @param top Resource identifier of the top Drawable. 436 * @param end Resource identifier of the end Drawable. 437 * @param bottom Resource identifier of the bottom Drawable. 438 * @attr name android:drawableStart 439 * @attr name android:drawableTop 440 * @attr name android:drawableEnd 441 * @attr name android:drawableBottom 442 */ 443 public static void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView, 444 @DrawableRes int start, @DrawableRes int top, @DrawableRes int end, 445 @DrawableRes int bottom) { 446 IMPL.setCompoundDrawablesRelativeWithIntrinsicBounds(textView, start, top, end, bottom); 447 } 448 449 /** 450 * Returns the maximum number of lines displayed in the given TextView, or -1 if the maximum 451 * height was set in pixels instead. 452 */ 453 public static int getMaxLines(@NonNull TextView textView) { 454 return IMPL.getMaxLines(textView); 455 } 456 457 /** 458 * Returns the minimum number of lines displayed in the given TextView, or -1 if the minimum 459 * height was set in pixels instead. 460 */ 461 public static int getMinLines(@NonNull TextView textView) { 462 return IMPL.getMinLines(textView); 463 } 464 465 /** 466 * Sets the text appearance from the specified style resource. 467 * <p> 468 * Use a framework-defined {@code TextAppearance} style like 469 * {@link android.R.style#TextAppearance_Material_Body1 @android:style/TextAppearance.Material.Body1}. 470 * 471 * @param textView The TextView against which to invoke the method. 472 * @param resId The resource identifier of the style to apply. 473 */ 474 public static void setTextAppearance(@NonNull TextView textView, @StyleRes int resId) { 475 IMPL.setTextAppearance(textView, resId); 476 } 477 478 /** 479 * Returns drawables for the start, top, end, and bottom borders from the given text view. 480 */ 481 public static Drawable[] getCompoundDrawablesRelative(@NonNull TextView textView) { 482 return IMPL.getCompoundDrawablesRelative(textView); 483 } 484 485 /** 486 * Specify whether this widget should automatically scale the text to try to perfectly fit 487 * within the layout bounds by using the default auto-size configuration. 488 * 489 * @param autoSizeTextType the type of auto-size. Must be one of 490 * {@link TextViewCompat#AUTO_SIZE_TEXT_TYPE_NONE} or 491 * {@link TextViewCompat#AUTO_SIZE_TEXT_TYPE_UNIFORM} 492 * 493 * @attr name android:autoSizeTextType 494 */ 495 public static void setAutoSizeTextTypeWithDefaults(TextView textView, int autoSizeTextType) { 496 IMPL.setAutoSizeTextTypeWithDefaults(textView, autoSizeTextType); 497 } 498 499 /** 500 * Specify whether this widget should automatically scale the text to try to perfectly fit 501 * within the layout bounds. If all the configuration params are valid the type of auto-size is 502 * set to {@link TextViewCompat#AUTO_SIZE_TEXT_TYPE_UNIFORM}. 503 * 504 * @param autoSizeMinTextSize the minimum text size available for auto-size 505 * @param autoSizeMaxTextSize the maximum text size available for auto-size 506 * @param autoSizeStepGranularity the auto-size step granularity. It is used in conjunction with 507 * the minimum and maximum text size in order to build the set of 508 * text sizes the system uses to choose from when auto-sizing 509 * @param unit the desired dimension unit for all sizes above. See {@link TypedValue} for the 510 * possible dimension units 511 * 512 * @throws IllegalArgumentException if any of the configuration params are invalid. 513 * 514 * @attr name android:autoSizeTextType 515 * @attr name android:autoSizeTextType 516 * @attr name android:autoSizeMinTextSize 517 * @attr name android:autoSizeMaxTextSize 518 * @attr name android:autoSizeStepGranularity 519 */ 520 public static void setAutoSizeTextTypeUniformWithConfiguration( 521 TextView textView, 522 int autoSizeMinTextSize, 523 int autoSizeMaxTextSize, 524 int autoSizeStepGranularity, 525 int unit) throws IllegalArgumentException { 526 IMPL.setAutoSizeTextTypeUniformWithConfiguration(textView, autoSizeMinTextSize, 527 autoSizeMaxTextSize, autoSizeStepGranularity, unit); 528 } 529 530 /** 531 * Specify whether this widget should automatically scale the text to try to perfectly fit 532 * within the layout bounds. If at least one value from the <code>presetSizes</code> is valid 533 * then the type of auto-size is set to {@link TextViewCompat#AUTO_SIZE_TEXT_TYPE_UNIFORM}. 534 * 535 * @param presetSizes an {@code int} array of sizes in pixels 536 * @param unit the desired dimension unit for the preset sizes above. See {@link TypedValue} for 537 * the possible dimension units 538 * 539 * @throws IllegalArgumentException if all of the <code>presetSizes</code> are invalid. 540 *_ 541 * @attr name android:autoSizeTextType 542 * @attr name android:autoSizePresetSizes 543 */ 544 public static void setAutoSizeTextTypeUniformWithPresetSizes(TextView textView, 545 @NonNull int[] presetSizes, int unit) throws IllegalArgumentException { 546 IMPL.setAutoSizeTextTypeUniformWithPresetSizes(textView, presetSizes, unit); 547 } 548 549 /** 550 * Returns the type of auto-size set for this widget. 551 * 552 * @return an {@code int} corresponding to one of the auto-size types: 553 * {@link TextViewCompat#AUTO_SIZE_TEXT_TYPE_NONE} or 554 * {@link TextViewCompat#AUTO_SIZE_TEXT_TYPE_UNIFORM} 555 * 556 * @attr name android:autoSizeTextType 557 */ 558 public static int getAutoSizeTextType(TextView textView) { 559 return IMPL.getAutoSizeTextType(textView); 560 } 561 562 /** 563 * @return the current auto-size step granularity in pixels. 564 * 565 * @attr name android:autoSizeStepGranularity 566 */ 567 public static int getAutoSizeStepGranularity(TextView textView) { 568 return IMPL.getAutoSizeStepGranularity(textView); 569 } 570 571 /** 572 * @return the current auto-size minimum text size in pixels (the default is 12sp). Note that 573 * if auto-size has not been configured this function returns {@code -1}. 574 * 575 * @attr name android:autoSizeMinTextSize 576 */ 577 public static int getAutoSizeMinTextSize(TextView textView) { 578 return IMPL.getAutoSizeMinTextSize(textView); 579 } 580 581 /** 582 * @return the current auto-size maximum text size in pixels (the default is 112sp). Note that 583 * if auto-size has not been configured this function returns {@code -1}. 584 * 585 * @attr name android:autoSizeMaxTextSize 586 */ 587 public static int getAutoSizeMaxTextSize(TextView textView) { 588 return IMPL.getAutoSizeMaxTextSize(textView); 589 } 590 591 /** 592 * @return the current auto-size {@code int} sizes array (in pixels). 593 * 594 * @attr name android:autoSizePresetSizes 595 */ 596 public static int[] getAutoSizeTextAvailableSizes(TextView textView) { 597 return IMPL.getAutoSizeTextAvailableSizes(textView); 598 } 599} 600