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