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 17 package androidx.appcompat.widget; 18 19 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; 20 import static androidx.core.widget.AutoSizeableTextView.PLATFORM_SUPPORTS_AUTOSIZE; 21 22 import android.annotation.SuppressLint; 23 import android.content.Context; 24 import android.content.res.ColorStateList; 25 import android.content.res.Resources; 26 import android.graphics.Typeface; 27 import android.graphics.drawable.Drawable; 28 import android.os.Build; 29 import android.text.method.PasswordTransformationMethod; 30 import android.util.AttributeSet; 31 import android.util.TypedValue; 32 import android.widget.TextView; 33 34 import androidx.annotation.NonNull; 35 import androidx.annotation.RestrictTo; 36 import androidx.appcompat.R; 37 import androidx.core.content.res.ResourcesCompat; 38 import androidx.core.widget.TextViewCompat; 39 40 import java.lang.ref.WeakReference; 41 42 class AppCompatTextHelper { 43 44 // Enum for the "typeface" XML parameter. 45 private static final int SANS = 1; 46 private static final int SERIF = 2; 47 private static final int MONOSPACE = 3; 48 49 private final TextView mView; 50 51 private TintInfo mDrawableLeftTint; 52 private TintInfo mDrawableTopTint; 53 private TintInfo mDrawableRightTint; 54 private TintInfo mDrawableBottomTint; 55 private TintInfo mDrawableStartTint; 56 private TintInfo mDrawableEndTint; 57 58 private final @NonNull AppCompatTextViewAutoSizeHelper mAutoSizeTextHelper; 59 60 private int mStyle = Typeface.NORMAL; 61 private Typeface mFontTypeface; 62 private boolean mAsyncFontPending; 63 64 AppCompatTextHelper(TextView view) { 65 mView = view; 66 mAutoSizeTextHelper = new AppCompatTextViewAutoSizeHelper(mView); 67 } 68 69 @SuppressLint("NewApi") 70 void loadFromAttributes(AttributeSet attrs, int defStyleAttr) { 71 final Context context = mView.getContext(); 72 final AppCompatDrawableManager drawableManager = AppCompatDrawableManager.get(); 73 74 // First read the TextAppearance style id 75 TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, attrs, 76 R.styleable.AppCompatTextHelper, defStyleAttr, 0); 77 final int ap = a.getResourceId(R.styleable.AppCompatTextHelper_android_textAppearance, -1); 78 // Now read the compound drawable and grab any tints 79 if (a.hasValue(R.styleable.AppCompatTextHelper_android_drawableLeft)) { 80 mDrawableLeftTint = createTintInfo(context, drawableManager, 81 a.getResourceId(R.styleable.AppCompatTextHelper_android_drawableLeft, 0)); 82 } 83 if (a.hasValue(R.styleable.AppCompatTextHelper_android_drawableTop)) { 84 mDrawableTopTint = createTintInfo(context, drawableManager, 85 a.getResourceId(R.styleable.AppCompatTextHelper_android_drawableTop, 0)); 86 } 87 if (a.hasValue(R.styleable.AppCompatTextHelper_android_drawableRight)) { 88 mDrawableRightTint = createTintInfo(context, drawableManager, 89 a.getResourceId(R.styleable.AppCompatTextHelper_android_drawableRight, 0)); 90 } 91 if (a.hasValue(R.styleable.AppCompatTextHelper_android_drawableBottom)) { 92 mDrawableBottomTint = createTintInfo(context, drawableManager, 93 a.getResourceId(R.styleable.AppCompatTextHelper_android_drawableBottom, 0)); 94 } 95 96 if (Build.VERSION.SDK_INT >= 17) { 97 if (a.hasValue(R.styleable.AppCompatTextHelper_android_drawableStart)) { 98 mDrawableStartTint = createTintInfo(context, drawableManager, 99 a.getResourceId(R.styleable.AppCompatTextHelper_android_drawableStart, 0)); 100 } 101 if (a.hasValue(R.styleable.AppCompatTextHelper_android_drawableEnd)) { 102 mDrawableEndTint = createTintInfo(context, drawableManager, 103 a.getResourceId(R.styleable.AppCompatTextHelper_android_drawableEnd, 0)); 104 } 105 } 106 107 a.recycle(); 108 109 // PasswordTransformationMethod wipes out all other TransformationMethod instances 110 // in TextView's constructor, so we should only set a new transformation method 111 // if we don't have a PasswordTransformationMethod currently... 112 final boolean hasPwdTm = 113 mView.getTransformationMethod() instanceof PasswordTransformationMethod; 114 boolean allCaps = false; 115 boolean allCapsSet = false; 116 ColorStateList textColor = null; 117 ColorStateList textColorHint = null; 118 ColorStateList textColorLink = null; 119 120 // First check TextAppearance's textAllCaps value 121 if (ap != -1) { 122 a = TintTypedArray.obtainStyledAttributes(context, ap, R.styleable.TextAppearance); 123 if (!hasPwdTm && a.hasValue(R.styleable.TextAppearance_textAllCaps)) { 124 allCapsSet = true; 125 allCaps = a.getBoolean(R.styleable.TextAppearance_textAllCaps, false); 126 } 127 128 updateTypefaceAndStyle(context, a); 129 if (Build.VERSION.SDK_INT < 23) { 130 // If we're running on < API 23, the text color may contain theme references 131 // so let's re-set using our own inflater 132 if (a.hasValue(R.styleable.TextAppearance_android_textColor)) { 133 textColor = a.getColorStateList(R.styleable.TextAppearance_android_textColor); 134 } 135 if (a.hasValue(R.styleable.TextAppearance_android_textColorHint)) { 136 textColorHint = a.getColorStateList( 137 R.styleable.TextAppearance_android_textColorHint); 138 } 139 if (a.hasValue(R.styleable.TextAppearance_android_textColorLink)) { 140 textColorLink = a.getColorStateList( 141 R.styleable.TextAppearance_android_textColorLink); 142 } 143 } 144 a.recycle(); 145 } 146 147 // Now read the style's values 148 a = TintTypedArray.obtainStyledAttributes(context, attrs, R.styleable.TextAppearance, 149 defStyleAttr, 0); 150 if (!hasPwdTm && a.hasValue(R.styleable.TextAppearance_textAllCaps)) { 151 allCapsSet = true; 152 allCaps = a.getBoolean(R.styleable.TextAppearance_textAllCaps, false); 153 } 154 if (Build.VERSION.SDK_INT < 23) { 155 // If we're running on < API 23, the text color may contain theme references 156 // so let's re-set using our own inflater 157 if (a.hasValue(R.styleable.TextAppearance_android_textColor)) { 158 textColor = a.getColorStateList(R.styleable.TextAppearance_android_textColor); 159 } 160 if (a.hasValue(R.styleable.TextAppearance_android_textColorHint)) { 161 textColorHint = a.getColorStateList( 162 R.styleable.TextAppearance_android_textColorHint); 163 } 164 if (a.hasValue(R.styleable.TextAppearance_android_textColorLink)) { 165 textColorLink = a.getColorStateList( 166 R.styleable.TextAppearance_android_textColorLink); 167 } 168 } 169 170 updateTypefaceAndStyle(context, a); 171 a.recycle(); 172 173 if (textColor != null) { 174 mView.setTextColor(textColor); 175 } 176 if (textColorHint != null) { 177 mView.setHintTextColor(textColorHint); 178 } 179 if (textColorLink != null) { 180 mView.setLinkTextColor(textColorLink); 181 } 182 if (!hasPwdTm && allCapsSet) { 183 setAllCaps(allCaps); 184 } 185 if (mFontTypeface != null) { 186 mView.setTypeface(mFontTypeface, mStyle); 187 } 188 189 mAutoSizeTextHelper.loadFromAttributes(attrs, defStyleAttr); 190 191 if (PLATFORM_SUPPORTS_AUTOSIZE) { 192 // Delegate auto-size functionality to the framework implementation. 193 if (mAutoSizeTextHelper.getAutoSizeTextType() 194 != TextViewCompat.AUTO_SIZE_TEXT_TYPE_NONE) { 195 final int[] autoSizeTextSizesInPx = 196 mAutoSizeTextHelper.getAutoSizeTextAvailableSizes(); 197 if (autoSizeTextSizesInPx.length > 0) { 198 if (mView.getAutoSizeStepGranularity() != AppCompatTextViewAutoSizeHelper 199 .UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE) { 200 // Configured with granularity, preserve details. 201 mView.setAutoSizeTextTypeUniformWithConfiguration( 202 mAutoSizeTextHelper.getAutoSizeMinTextSize(), 203 mAutoSizeTextHelper.getAutoSizeMaxTextSize(), 204 mAutoSizeTextHelper.getAutoSizeStepGranularity(), 205 TypedValue.COMPLEX_UNIT_PX); 206 } else { 207 mView.setAutoSizeTextTypeUniformWithPresetSizes( 208 autoSizeTextSizesInPx, TypedValue.COMPLEX_UNIT_PX); 209 } 210 } 211 } 212 } 213 214 // Read line and baseline heights attributes. 215 a = TintTypedArray.obtainStyledAttributes(context, attrs, R.styleable.AppCompatTextView); 216 final int firstBaselineToTopHeight = a.getDimensionPixelSize( 217 R.styleable.AppCompatTextView_firstBaselineToTopHeight, -1); 218 final int lastBaselineToBottomHeight = a.getDimensionPixelSize( 219 R.styleable.AppCompatTextView_lastBaselineToBottomHeight, -1); 220 final int lineHeight = a.getDimensionPixelSize( 221 R.styleable.AppCompatTextView_lineHeight, -1); 222 a.recycle(); 223 if (firstBaselineToTopHeight != -1) { 224 TextViewCompat.setFirstBaselineToTopHeight(mView, firstBaselineToTopHeight); 225 } 226 if (lastBaselineToBottomHeight != -1) { 227 TextViewCompat.setLastBaselineToBottomHeight(mView, lastBaselineToBottomHeight); 228 } 229 if (lineHeight != -1) { 230 TextViewCompat.setLineHeight(mView, lineHeight); 231 } 232 } 233 234 private void updateTypefaceAndStyle(Context context, TintTypedArray a) { 235 mStyle = a.getInt(R.styleable.TextAppearance_android_textStyle, mStyle); 236 237 if (a.hasValue(R.styleable.TextAppearance_android_fontFamily) 238 || a.hasValue(R.styleable.TextAppearance_fontFamily)) { 239 mFontTypeface = null; 240 int fontFamilyId = a.hasValue(R.styleable.TextAppearance_fontFamily) 241 ? R.styleable.TextAppearance_fontFamily 242 : R.styleable.TextAppearance_android_fontFamily; 243 if (!context.isRestricted()) { 244 final WeakReference<TextView> textViewWeak = new WeakReference<>(mView); 245 ResourcesCompat.FontCallback replyCallback = new ResourcesCompat.FontCallback() { 246 @Override 247 public void onFontRetrieved(@NonNull Typeface typeface) { 248 onAsyncTypefaceReceived(textViewWeak, typeface); 249 } 250 251 @Override 252 public void onFontRetrievalFailed(int reason) { 253 // Do nothing. 254 } 255 }; 256 try { 257 // Note the callback will be triggered on the UI thread. 258 mFontTypeface = a.getFont(fontFamilyId, mStyle, replyCallback); 259 // If this call gave us an immediate result, ignore any pending callbacks. 260 mAsyncFontPending = mFontTypeface == null; 261 } catch (UnsupportedOperationException | Resources.NotFoundException e) { 262 // Expected if it is not a font resource. 263 } 264 } 265 if (mFontTypeface == null) { 266 // Try with String. This is done by TextView JB+, but fails in ICS 267 String fontFamilyName = a.getString(fontFamilyId); 268 if (fontFamilyName != null) { 269 mFontTypeface = Typeface.create(fontFamilyName, mStyle); 270 } 271 } 272 return; 273 } 274 275 if (a.hasValue(R.styleable.TextAppearance_android_typeface)) { 276 // Ignore previous pending fonts 277 mAsyncFontPending = false; 278 int typefaceIndex = a.getInt(R.styleable.TextAppearance_android_typeface, SANS); 279 switch (typefaceIndex) { 280 case SANS: 281 mFontTypeface = Typeface.SANS_SERIF; 282 break; 283 284 case SERIF: 285 mFontTypeface = Typeface.SERIF; 286 break; 287 288 case MONOSPACE: 289 mFontTypeface = Typeface.MONOSPACE; 290 break; 291 } 292 } 293 } 294 295 private void onAsyncTypefaceReceived(WeakReference<TextView> textViewWeak, Typeface typeface) { 296 if (mAsyncFontPending) { 297 mFontTypeface = typeface; 298 final TextView textView = textViewWeak.get(); 299 if (textView != null) { 300 textView.setTypeface(typeface, mStyle); 301 } 302 } 303 } 304 305 void onSetTextAppearance(Context context, int resId) { 306 final TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, 307 resId, R.styleable.TextAppearance); 308 if (a.hasValue(R.styleable.TextAppearance_textAllCaps)) { 309 // This breaks away slightly from the logic in TextView.setTextAppearance that serves 310 // as an "overlay" on the current state of the TextView. Since android:textAllCaps 311 // may have been set to true in this text appearance, we need to make sure that 312 // app:textAllCaps has the chance to override it 313 setAllCaps(a.getBoolean(R.styleable.TextAppearance_textAllCaps, false)); 314 } 315 if (Build.VERSION.SDK_INT < 23 316 && a.hasValue(R.styleable.TextAppearance_android_textColor)) { 317 // If we're running on < API 23, the text color may contain theme references 318 // so let's re-set using our own inflater 319 final ColorStateList textColor 320 = a.getColorStateList(R.styleable.TextAppearance_android_textColor); 321 if (textColor != null) { 322 mView.setTextColor(textColor); 323 } 324 } 325 326 updateTypefaceAndStyle(context, a); 327 a.recycle(); 328 if (mFontTypeface != null) { 329 mView.setTypeface(mFontTypeface, mStyle); 330 } 331 } 332 333 void setAllCaps(boolean allCaps) { 334 mView.setAllCaps(allCaps); 335 } 336 337 void applyCompoundDrawablesTints() { 338 if (mDrawableLeftTint != null || mDrawableTopTint != null || 339 mDrawableRightTint != null || mDrawableBottomTint != null) { 340 final Drawable[] compoundDrawables = mView.getCompoundDrawables(); 341 applyCompoundDrawableTint(compoundDrawables[0], mDrawableLeftTint); 342 applyCompoundDrawableTint(compoundDrawables[1], mDrawableTopTint); 343 applyCompoundDrawableTint(compoundDrawables[2], mDrawableRightTint); 344 applyCompoundDrawableTint(compoundDrawables[3], mDrawableBottomTint); 345 } 346 if (Build.VERSION.SDK_INT >= 17) { 347 if (mDrawableStartTint != null || mDrawableEndTint != null) { 348 final Drawable[] compoundDrawables = mView.getCompoundDrawablesRelative(); 349 applyCompoundDrawableTint(compoundDrawables[0], mDrawableStartTint); 350 applyCompoundDrawableTint(compoundDrawables[2], mDrawableEndTint); 351 } 352 } 353 } 354 355 private void applyCompoundDrawableTint(Drawable drawable, TintInfo info) { 356 if (drawable != null && info != null) { 357 AppCompatDrawableManager.tintDrawable(drawable, info, mView.getDrawableState()); 358 } 359 } 360 361 private static TintInfo createTintInfo(Context context, 362 AppCompatDrawableManager drawableManager, int drawableId) { 363 final ColorStateList tintList = drawableManager.getTintList(context, drawableId); 364 if (tintList != null) { 365 final TintInfo tintInfo = new TintInfo(); 366 tintInfo.mHasTintList = true; 367 tintInfo.mTintList = tintList; 368 return tintInfo; 369 } 370 return null; 371 } 372 373 /** @hide */ 374 @RestrictTo(LIBRARY_GROUP) 375 void onLayout(boolean changed, int left, int top, int right, int bottom) { 376 if (!PLATFORM_SUPPORTS_AUTOSIZE) { 377 autoSizeText(); 378 } 379 } 380 381 /** @hide */ 382 @RestrictTo(LIBRARY_GROUP) 383 void setTextSize(int unit, float size) { 384 if (!PLATFORM_SUPPORTS_AUTOSIZE) { 385 if (!isAutoSizeEnabled()) { 386 setTextSizeInternal(unit, size); 387 } 388 } 389 } 390 391 /** @hide */ 392 @RestrictTo(LIBRARY_GROUP) 393 void autoSizeText() { 394 mAutoSizeTextHelper.autoSizeText(); 395 } 396 397 /** @hide */ 398 @RestrictTo(LIBRARY_GROUP) 399 boolean isAutoSizeEnabled() { 400 return mAutoSizeTextHelper.isAutoSizeEnabled(); 401 } 402 403 private void setTextSizeInternal(int unit, float size) { 404 mAutoSizeTextHelper.setTextSizeInternal(unit, size); 405 } 406 407 void setAutoSizeTextTypeWithDefaults(@TextViewCompat.AutoSizeTextType int autoSizeTextType) { 408 mAutoSizeTextHelper.setAutoSizeTextTypeWithDefaults(autoSizeTextType); 409 } 410 411 void setAutoSizeTextTypeUniformWithConfiguration( 412 int autoSizeMinTextSize, 413 int autoSizeMaxTextSize, 414 int autoSizeStepGranularity, 415 int unit) throws IllegalArgumentException { 416 mAutoSizeTextHelper.setAutoSizeTextTypeUniformWithConfiguration( 417 autoSizeMinTextSize, autoSizeMaxTextSize, autoSizeStepGranularity, unit); 418 } 419 420 void setAutoSizeTextTypeUniformWithPresetSizes(@NonNull int[] presetSizes, int unit) 421 throws IllegalArgumentException { 422 mAutoSizeTextHelper.setAutoSizeTextTypeUniformWithPresetSizes(presetSizes, unit); 423 } 424 425 @TextViewCompat.AutoSizeTextType 426 int getAutoSizeTextType() { 427 return mAutoSizeTextHelper.getAutoSizeTextType(); 428 } 429 430 int getAutoSizeStepGranularity() { 431 return mAutoSizeTextHelper.getAutoSizeStepGranularity(); 432 } 433 434 int getAutoSizeMinTextSize() { 435 return mAutoSizeTextHelper.getAutoSizeMinTextSize(); 436 } 437 438 int getAutoSizeMaxTextSize() { 439 return mAutoSizeTextHelper.getAutoSizeMaxTextSize(); 440 } 441 442 int[] getAutoSizeTextAvailableSizes() { 443 return mAutoSizeTextHelper.getAutoSizeTextAvailableSizes(); 444 } 445 } 446