1 /* 2 * Copyright (C) 2014 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_PREFIX; 20 import static androidx.appcompat.widget.ViewUtils.SDK_LEVEL_SUPPORTS_AUTOSIZE; 21 22 import android.annotation.SuppressLint; 23 import android.content.Context; 24 import android.content.res.ColorStateList; 25 import android.graphics.PorterDuff; 26 import android.graphics.drawable.Drawable; 27 import android.text.InputFilter; 28 import android.util.AttributeSet; 29 import android.view.ActionMode; 30 import android.view.accessibility.AccessibilityEvent; 31 import android.view.accessibility.AccessibilityNodeInfo; 32 import android.widget.Button; 33 import android.widget.TextView; 34 35 import androidx.annotation.DrawableRes; 36 import androidx.annotation.RestrictTo; 37 import androidx.appcompat.R; 38 import androidx.core.view.TintableBackgroundView; 39 import androidx.core.widget.AutoSizeableTextView; 40 import androidx.core.widget.TextViewCompat; 41 import androidx.core.widget.TintableCompoundDrawablesView; 42 import androidx.resourceinspection.annotation.AppCompatShadowedAttributes; 43 44 import org.jspecify.annotations.NonNull; 45 import org.jspecify.annotations.Nullable; 46 47 /** 48 * A {@link Button} which supports compatible features on older versions of the platform, 49 * including: 50 * <ul> 51 * <li>Allows dynamic tint of its background via the background tint methods in 52 * {@link androidx.core.view.ViewCompat}.</li> 53 * <li>Allows setting of the background tint using {@link R.attr#backgroundTint} and 54 * {@link R.attr#backgroundTintMode}.</li> 55 * <li>Allows setting of the font family using {@link android.R.attr#fontFamily}</li> 56 * </ul> 57 * 58 * <p>This will automatically be used when you use {@link Button} in your layouts 59 * and the top-level activity / dialog is provided by 60 * <a href="{@docRoot}topic/libraries/support-library/packages.html#v7-appcompat">appcompat</a>. 61 * You should only need to manually use this class when writing custom views.</p> 62 */ 63 @AppCompatShadowedAttributes 64 public class AppCompatButton extends Button implements TintableBackgroundView, 65 AutoSizeableTextView, TintableCompoundDrawablesView, EmojiCompatConfigurationView { 66 67 private final AppCompatBackgroundHelper mBackgroundTintHelper; 68 private final AppCompatTextHelper mTextHelper; 69 private @NonNull AppCompatEmojiTextHelper mAppCompatEmojiTextHelper; 70 AppCompatButton(@onNull Context context)71 public AppCompatButton(@NonNull Context context) { 72 this(context, null); 73 } 74 AppCompatButton(@onNull Context context, @Nullable AttributeSet attrs)75 public AppCompatButton(@NonNull Context context, @Nullable AttributeSet attrs) { 76 this(context, attrs, R.attr.buttonStyle); 77 } 78 AppCompatButton( @onNull Context context, @Nullable AttributeSet attrs, int defStyleAttr)79 public AppCompatButton( 80 @NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 81 super(TintContextWrapper.wrap(context), attrs, defStyleAttr); 82 83 ThemeUtils.checkAppCompatTheme(this, getContext()); 84 85 mBackgroundTintHelper = new AppCompatBackgroundHelper(this); 86 mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr); 87 88 mTextHelper = new AppCompatTextHelper(this); 89 mTextHelper.loadFromAttributes(attrs, defStyleAttr); 90 mTextHelper.applyCompoundDrawablesTints(); 91 92 AppCompatEmojiTextHelper emojiTextViewHelper = getEmojiTextViewHelper(); 93 emojiTextViewHelper.loadFromAttributes(attrs, defStyleAttr); 94 } 95 96 @Override setBackgroundResource(@rawableRes int resId)97 public void setBackgroundResource(@DrawableRes int resId) { 98 super.setBackgroundResource(resId); 99 if (mBackgroundTintHelper != null) { 100 mBackgroundTintHelper.onSetBackgroundResource(resId); 101 } 102 } 103 104 @Override setBackgroundDrawable(@ullable Drawable background)105 public void setBackgroundDrawable(@Nullable Drawable background) { 106 super.setBackgroundDrawable(background); 107 if (mBackgroundTintHelper != null) { 108 mBackgroundTintHelper.onSetBackgroundDrawable(background); 109 } 110 } 111 112 /** 113 * This should be accessed via 114 * {@link androidx.core.view.ViewCompat#setBackgroundTintList(android.view.View, ColorStateList)} 115 * 116 */ 117 @RestrictTo(LIBRARY_GROUP_PREFIX) 118 @Override setSupportBackgroundTintList(@ullable ColorStateList tint)119 public void setSupportBackgroundTintList(@Nullable ColorStateList tint) { 120 if (mBackgroundTintHelper != null) { 121 mBackgroundTintHelper.setSupportBackgroundTintList(tint); 122 } 123 } 124 125 /** 126 * This should be accessed via 127 * {@link androidx.core.view.ViewCompat#getBackgroundTintList(android.view.View)} 128 * 129 */ 130 @RestrictTo(LIBRARY_GROUP_PREFIX) 131 @Override getSupportBackgroundTintList()132 public @Nullable ColorStateList getSupportBackgroundTintList() { 133 return mBackgroundTintHelper != null 134 ? mBackgroundTintHelper.getSupportBackgroundTintList() : null; 135 } 136 137 /** 138 * This should be accessed via 139 * {@link androidx.core.view.ViewCompat#setBackgroundTintMode(android.view.View, PorterDuff.Mode)} 140 * 141 */ 142 @RestrictTo(LIBRARY_GROUP_PREFIX) 143 @Override setSupportBackgroundTintMode(PorterDuff.@ullable Mode tintMode)144 public void setSupportBackgroundTintMode(PorterDuff.@Nullable Mode tintMode) { 145 if (mBackgroundTintHelper != null) { 146 mBackgroundTintHelper.setSupportBackgroundTintMode(tintMode); 147 } 148 } 149 150 /** 151 * This should be accessed via 152 * {@link androidx.core.view.ViewCompat#getBackgroundTintMode(android.view.View)} 153 * 154 */ 155 @RestrictTo(LIBRARY_GROUP_PREFIX) 156 @Override getSupportBackgroundTintMode()157 public PorterDuff.@Nullable Mode getSupportBackgroundTintMode() { 158 return mBackgroundTintHelper != null 159 ? mBackgroundTintHelper.getSupportBackgroundTintMode() : null; 160 } 161 162 @Override drawableStateChanged()163 protected void drawableStateChanged() { 164 super.drawableStateChanged(); 165 if (mBackgroundTintHelper != null) { 166 mBackgroundTintHelper.applySupportBackgroundTint(); 167 } 168 if (mTextHelper != null) { 169 mTextHelper.applyCompoundDrawablesTints(); 170 } 171 } 172 173 @Override setTextAppearance(Context context, int resId)174 public void setTextAppearance(Context context, int resId) { 175 super.setTextAppearance(context, resId); 176 if (mTextHelper != null) { 177 mTextHelper.onSetTextAppearance(context, resId); 178 } 179 } 180 181 @Override onInitializeAccessibilityEvent(AccessibilityEvent event)182 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 183 super.onInitializeAccessibilityEvent(event); 184 event.setClassName(Button.class.getName()); 185 } 186 187 @Override onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)188 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 189 super.onInitializeAccessibilityNodeInfo(info); 190 info.setClassName(Button.class.getName()); 191 } 192 193 @Override onLayout(boolean changed, int left, int top, int right, int bottom)194 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 195 super.onLayout(changed, left, top, right, bottom); 196 if (mTextHelper != null) { 197 mTextHelper.onLayout(changed, left, top, right, bottom); 198 } 199 } 200 201 @Override setTextSize(int unit, float size)202 public void setTextSize(int unit, float size) { 203 if (SDK_LEVEL_SUPPORTS_AUTOSIZE) { 204 super.setTextSize(unit, size); 205 } else { 206 if (mTextHelper != null) { 207 mTextHelper.setTextSize(unit, size); 208 } 209 } 210 } 211 212 @Override onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter)213 protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) { 214 super.onTextChanged(text, start, lengthBefore, lengthAfter); 215 boolean useTextHelper = mTextHelper != null && !SDK_LEVEL_SUPPORTS_AUTOSIZE 216 && mTextHelper.isAutoSizeEnabled(); 217 if (useTextHelper) { 218 mTextHelper.autoSizeText(); 219 } 220 } 221 222 /** 223 */ 224 @RestrictTo(LIBRARY_GROUP_PREFIX) 225 @Override setAutoSizeTextTypeWithDefaults( @extViewCompat.AutoSizeTextType int autoSizeTextType)226 public void setAutoSizeTextTypeWithDefaults( 227 @TextViewCompat.AutoSizeTextType int autoSizeTextType) { 228 if (SDK_LEVEL_SUPPORTS_AUTOSIZE) { 229 super.setAutoSizeTextTypeWithDefaults(autoSizeTextType); 230 } else { 231 if (mTextHelper != null) { 232 mTextHelper.setAutoSizeTextTypeWithDefaults(autoSizeTextType); 233 } 234 } 235 } 236 237 /** 238 */ 239 @RestrictTo(LIBRARY_GROUP_PREFIX) 240 @Override setAutoSizeTextTypeUniformWithConfiguration( int autoSizeMinTextSize, int autoSizeMaxTextSize, int autoSizeStepGranularity, int unit)241 public void setAutoSizeTextTypeUniformWithConfiguration( 242 int autoSizeMinTextSize, 243 int autoSizeMaxTextSize, 244 int autoSizeStepGranularity, 245 int unit) throws IllegalArgumentException { 246 if (SDK_LEVEL_SUPPORTS_AUTOSIZE) { 247 super.setAutoSizeTextTypeUniformWithConfiguration( 248 autoSizeMinTextSize, autoSizeMaxTextSize, autoSizeStepGranularity, unit); 249 } else { 250 if (mTextHelper != null) { 251 mTextHelper.setAutoSizeTextTypeUniformWithConfiguration( 252 autoSizeMinTextSize, autoSizeMaxTextSize, autoSizeStepGranularity, unit); 253 } 254 } 255 } 256 257 /** 258 */ 259 @RestrictTo(LIBRARY_GROUP_PREFIX) 260 @Override setAutoSizeTextTypeUniformWithPresetSizes(int @NonNull [] presetSizes, int unit)261 public void setAutoSizeTextTypeUniformWithPresetSizes(int @NonNull [] presetSizes, int unit) 262 throws IllegalArgumentException { 263 if (SDK_LEVEL_SUPPORTS_AUTOSIZE) { 264 super.setAutoSizeTextTypeUniformWithPresetSizes(presetSizes, unit); 265 } else { 266 if (mTextHelper != null) { 267 mTextHelper.setAutoSizeTextTypeUniformWithPresetSizes(presetSizes, unit); 268 } 269 } 270 } 271 272 /** 273 */ 274 @RestrictTo(LIBRARY_GROUP_PREFIX) 275 @Override 276 @TextViewCompat.AutoSizeTextType 277 // Suppress lint error for TextView.AUTO_SIZE_TEXT_TYPE_UNIFORM [WrongConstant] 278 @SuppressLint("WrongConstant") getAutoSizeTextType()279 public int getAutoSizeTextType() { 280 if (SDK_LEVEL_SUPPORTS_AUTOSIZE) { 281 return super.getAutoSizeTextType() == TextView.AUTO_SIZE_TEXT_TYPE_UNIFORM 282 ? TextViewCompat.AUTO_SIZE_TEXT_TYPE_UNIFORM 283 : TextViewCompat.AUTO_SIZE_TEXT_TYPE_NONE; 284 } else { 285 if (mTextHelper != null) { 286 return mTextHelper.getAutoSizeTextType(); 287 } 288 } 289 return TextViewCompat.AUTO_SIZE_TEXT_TYPE_NONE; 290 } 291 292 /** 293 */ 294 @RestrictTo(LIBRARY_GROUP_PREFIX) 295 @Override getAutoSizeStepGranularity()296 public int getAutoSizeStepGranularity() { 297 if (SDK_LEVEL_SUPPORTS_AUTOSIZE) { 298 return super.getAutoSizeStepGranularity(); 299 } else { 300 if (mTextHelper != null) { 301 return mTextHelper.getAutoSizeStepGranularity(); 302 } 303 } 304 return -1; 305 } 306 307 /** 308 */ 309 @RestrictTo(LIBRARY_GROUP_PREFIX) 310 @Override getAutoSizeMinTextSize()311 public int getAutoSizeMinTextSize() { 312 if (SDK_LEVEL_SUPPORTS_AUTOSIZE) { 313 return super.getAutoSizeMinTextSize(); 314 } else { 315 if (mTextHelper != null) { 316 return mTextHelper.getAutoSizeMinTextSize(); 317 } 318 } 319 return -1; 320 } 321 322 /** 323 */ 324 @RestrictTo(LIBRARY_GROUP_PREFIX) 325 @Override getAutoSizeMaxTextSize()326 public int getAutoSizeMaxTextSize() { 327 if (SDK_LEVEL_SUPPORTS_AUTOSIZE) { 328 return super.getAutoSizeMaxTextSize(); 329 } else { 330 if (mTextHelper != null) { 331 return mTextHelper.getAutoSizeMaxTextSize(); 332 } 333 } 334 return -1; 335 } 336 337 /** 338 */ 339 @RestrictTo(LIBRARY_GROUP_PREFIX) 340 @Override getAutoSizeTextAvailableSizes()341 public int[] getAutoSizeTextAvailableSizes() { 342 if (SDK_LEVEL_SUPPORTS_AUTOSIZE) { 343 return super.getAutoSizeTextAvailableSizes(); 344 } else { 345 if (mTextHelper != null) { 346 return mTextHelper.getAutoSizeTextAvailableSizes(); 347 } 348 } 349 return new int[0]; 350 } 351 352 /** 353 * Sets the properties of this field to transform input to ALL CAPS 354 * display. This may use a "small caps" formatting if available. 355 * This setting will be ignored if this field is editable or selectable. 356 * 357 * This call replaces the current transformation method. Disabling this 358 * will not necessarily restore the previous behavior from before this 359 * was enabled. 360 */ setSupportAllCaps(boolean allCaps)361 public void setSupportAllCaps(boolean allCaps) { 362 if (mTextHelper != null) { 363 mTextHelper.setAllCaps(allCaps); 364 } 365 } 366 367 /** 368 * See 369 * {@link TextViewCompat#setCustomSelectionActionModeCallback(TextView, ActionMode.Callback)} 370 */ 371 @Override setCustomSelectionActionModeCallback( ActionMode.@ullable Callback actionModeCallback)372 public void setCustomSelectionActionModeCallback( 373 ActionMode.@Nullable Callback actionModeCallback) { 374 super.setCustomSelectionActionModeCallback( 375 TextViewCompat.wrapCustomSelectionActionModeCallback(this, actionModeCallback)); 376 } 377 378 @Override getCustomSelectionActionModeCallback()379 public ActionMode.@Nullable Callback getCustomSelectionActionModeCallback() { 380 return TextViewCompat.unwrapCustomSelectionActionModeCallback( 381 super.getCustomSelectionActionModeCallback()); 382 } 383 384 /** 385 * @param tint the tint to apply, may be {@code null} to clear tint 386 */ 387 @Override 388 @RestrictTo(LIBRARY_GROUP_PREFIX) setSupportCompoundDrawablesTintList(@ullable ColorStateList tint)389 public void setSupportCompoundDrawablesTintList(@Nullable ColorStateList tint) { 390 mTextHelper.setCompoundDrawableTintList(tint); 391 mTextHelper.applyCompoundDrawablesTints(); 392 } 393 394 /** 395 * @return the tint applied to the compound drawables 396 */ 397 @Override 398 @RestrictTo(LIBRARY_GROUP_PREFIX) getSupportCompoundDrawablesTintList()399 public @Nullable ColorStateList getSupportCompoundDrawablesTintList() { 400 return mTextHelper.getCompoundDrawableTintList(); 401 } 402 403 /** 404 * @param tintMode the blending mode used to apply the tint, may be {@code null} to clear tint 405 */ 406 @Override 407 @RestrictTo(LIBRARY_GROUP_PREFIX) setSupportCompoundDrawablesTintMode(PorterDuff.@ullable Mode tintMode)408 public void setSupportCompoundDrawablesTintMode(PorterDuff.@Nullable Mode tintMode) { 409 mTextHelper.setCompoundDrawableTintMode(tintMode); 410 mTextHelper.applyCompoundDrawablesTints(); 411 } 412 413 /** 414 * @return the blending mode used to apply the tint to the compound drawables 415 */ 416 @Override 417 @RestrictTo(LIBRARY_GROUP_PREFIX) getSupportCompoundDrawablesTintMode()418 public PorterDuff.@Nullable Mode getSupportCompoundDrawablesTintMode() { 419 return mTextHelper.getCompoundDrawableTintMode(); 420 } 421 422 423 @Override setFilters(@uppressWarnings"ArrayReturn") InputFilter @onNull [] filters)424 public void setFilters(@SuppressWarnings("ArrayReturn") InputFilter @NonNull [] filters) { 425 super.setFilters(getEmojiTextViewHelper().getFilters(filters)); 426 } 427 428 429 /** 430 * This may be called from super constructors. 431 */ getEmojiTextViewHelper()432 private @NonNull AppCompatEmojiTextHelper getEmojiTextViewHelper() { 433 //noinspection ConstantConditions 434 if (mAppCompatEmojiTextHelper == null) { 435 mAppCompatEmojiTextHelper = new AppCompatEmojiTextHelper(this); 436 } 437 return mAppCompatEmojiTextHelper; 438 } 439 440 @Override setAllCaps(boolean allCaps)441 public void setAllCaps(boolean allCaps) { 442 super.setAllCaps(allCaps); 443 getEmojiTextViewHelper().setAllCaps(allCaps); 444 } 445 446 447 @Override setEmojiCompatEnabled(boolean enabled)448 public void setEmojiCompatEnabled(boolean enabled) { 449 getEmojiTextViewHelper().setEnabled(enabled); 450 } 451 452 @Override isEmojiCompatEnabled()453 public boolean isEmojiCompatEnabled() { 454 return getEmojiTextViewHelper().isEnabled(); 455 } 456 } 457