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