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