29
29
import androidx .appcompat .widget .AppCompatCheckBox ;
30
30
import android .text .TextUtils ;
31
31
import android .util .AttributeSet ;
32
+ import android .view .accessibility .AccessibilityNodeInfo ;
33
+ import androidx .annotation .NonNull ;
32
34
import androidx .annotation .Nullable ;
35
+ import androidx .annotation .StringRes ;
33
36
import androidx .core .graphics .drawable .DrawableCompat ;
34
37
import androidx .core .widget .CompoundButtonCompat ;
35
38
import com .google .android .material .color .MaterialColors ;
36
39
import com .google .android .material .internal .ThemeEnforcement ;
37
40
import com .google .android .material .internal .ViewUtils ;
38
41
import com .google .android .material .resources .MaterialResources ;
42
+ import java .util .LinkedHashSet ;
39
43
40
44
/**
41
45
* A class that creates a Material Themed CheckBox.
@@ -49,16 +53,36 @@ public class MaterialCheckBox extends AppCompatCheckBox {
49
53
50
54
private static final int DEF_STYLE_RES =
51
55
R .style .Widget_MaterialComponents_CompoundButton_CheckBox ;
52
- private static final int [][] ENABLED_CHECKED_STATES =
56
+ private static final int [] ERROR_STATE_SET = {R .attr .state_error };
57
+ private static final int [][] CHECKBOX_STATES =
53
58
new int [][] {
54
- new int [] {android .R .attr .state_enabled , android .R .attr .state_checked }, // [0]
55
- new int [] {android .R .attr .state_enabled , -android .R .attr .state_checked }, // [1]
56
- new int [] {-android .R .attr .state_enabled , android .R .attr .state_checked }, // [2]
57
- new int [] {-android .R .attr .state_enabled , -android .R .attr .state_checked } // [3]
59
+ new int [] {android .R .attr .state_enabled , R .attr .state_error }, // [0]
60
+ new int [] {android .R .attr .state_enabled , android .R .attr .state_checked }, // [1]
61
+ new int [] {android .R .attr .state_enabled , -android .R .attr .state_checked }, // [2]
62
+ new int [] {-android .R .attr .state_enabled , android .R .attr .state_checked }, // [3]
63
+ new int [] {-android .R .attr .state_enabled , -android .R .attr .state_checked } // [4]
58
64
};
65
+ @ NonNull private final LinkedHashSet <OnErrorChangedListener > onErrorChangedListeners =
66
+ new LinkedHashSet <>();
59
67
@ Nullable private ColorStateList materialThemeColorsTintList ;
60
68
private boolean useMaterialThemeColors ;
61
69
private boolean centerIfNoTextEnabled ;
70
+ private boolean errorShown ;
71
+ private CharSequence errorAccessibilityLabel ;
72
+
73
+ /**
74
+ * Callback interface invoked when the checkbox error state changes.
75
+ */
76
+ public interface OnErrorChangedListener {
77
+
78
+ /**
79
+ * Called when the error state of a checkbox changes.
80
+ *
81
+ * @param checkBox the {@link MaterialCheckBox}
82
+ * @param errorShown whether the checkbox is on error
83
+ */
84
+ void onErrorChanged (@ NonNull MaterialCheckBox checkBox , boolean errorShown );
85
+ }
62
86
63
87
public MaterialCheckBox (Context context ) {
64
88
this (context , null );
@@ -90,6 +114,9 @@ public MaterialCheckBox(Context context, @Nullable AttributeSet attrs, int defSt
90
114
attributes .getBoolean (R .styleable .MaterialCheckBox_useMaterialThemeColors , false );
91
115
centerIfNoTextEnabled =
92
116
attributes .getBoolean (R .styleable .MaterialCheckBox_centerIfNoTextEnabled , true );
117
+ errorShown = attributes .getBoolean (R .styleable .MaterialCheckBox_errorShown , false );
118
+ errorAccessibilityLabel =
119
+ attributes .getText (R .styleable .MaterialCheckBox_errorAccessibilityLabel );
93
120
94
121
attributes .recycle ();
95
122
}
@@ -130,6 +157,121 @@ protected void onAttachedToWindow() {
130
157
}
131
158
}
132
159
160
+ @ Override
161
+ protected int [] onCreateDrawableState (int extraSpace ) {
162
+ final int [] drawableStates = super .onCreateDrawableState (extraSpace + 1 );
163
+
164
+ if (isErrorShown ()) {
165
+ mergeDrawableStates (drawableStates , ERROR_STATE_SET );
166
+ }
167
+
168
+ return drawableStates ;
169
+ }
170
+
171
+ @ Override
172
+ public void onInitializeAccessibilityNodeInfo (@ Nullable AccessibilityNodeInfo info ) {
173
+ super .onInitializeAccessibilityNodeInfo (info );
174
+ if (info == null ) {
175
+ return ;
176
+ }
177
+
178
+ if (isErrorShown ()) {
179
+ info .setText (info .getText () + ", " + errorAccessibilityLabel );
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Sets whether the checkbox should be on error state. If true, the error color will be applied to
185
+ * the checkbox.
186
+ *
187
+ * @param errorShown whether the checkbox should be on error state.
188
+ * @see #isErrorShown()
189
+ * @attr ref com.google.android.material.R.styleable#MaterialCheckBox_errorShown
190
+ */
191
+ public void setErrorShown (boolean errorShown ) {
192
+ if (this .errorShown == errorShown ) {
193
+ return ;
194
+ }
195
+ this .errorShown = errorShown ;
196
+ refreshDrawableState ();
197
+ for (OnErrorChangedListener listener : onErrorChangedListeners ) {
198
+ listener .onErrorChanged (this , this .errorShown );
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Returns whether the checkbox is on error state.
204
+ *
205
+ * @see #setErrorShown(boolean)
206
+ * @attr ref com.google.android.material.R.styleable#MaterialCheckBox_errorShown
207
+ */
208
+ public boolean isErrorShown () {
209
+ return errorShown ;
210
+ }
211
+
212
+ /**
213
+ * Sets the accessibility label to be used for the error state announcement by screen readers.
214
+ *
215
+ * @param resId resource ID of the error announcement text
216
+ * @see #setErrorShown(boolean)
217
+ * @see #getErrorAccessibilityLabel()
218
+ * @attr ref com.google.android.material.R.styleable#MaterialCheckBox_errorAccessibilityLabel
219
+ */
220
+ public void setErrorAccessibilityLabelResource (@ StringRes int resId ) {
221
+ setErrorAccessibilityLabel (resId != 0 ? getResources ().getText (resId ) : null );
222
+ }
223
+
224
+ /**
225
+ * Sets the accessibility label to be used for the error state announcement by screen readers.
226
+ *
227
+ * @param errorAccessibilityLabel the error announcement
228
+ * @see #setErrorShown(boolean)
229
+ * @see #getErrorAccessibilityLabel()
230
+ * @attr ref com.google.android.material.R.styleable#MaterialCheckBox_errorAccessibilityLabel
231
+ */
232
+ public void setErrorAccessibilityLabel (@ Nullable CharSequence errorAccessibilityLabel ) {
233
+ this .errorAccessibilityLabel = errorAccessibilityLabel ;
234
+ }
235
+
236
+ /**
237
+ * Returns the accessibility label used for the error state announcement.
238
+ *
239
+ * @see #setErrorAccessibilityLabel(CharSequence)
240
+ * @attr ref com.google.android.material.R.styleable#MaterialCheckBox_errorAccessibilityLabel
241
+ */
242
+ @ Nullable
243
+ public CharSequence getErrorAccessibilityLabel () {
244
+ return errorAccessibilityLabel ;
245
+ }
246
+
247
+ /**
248
+ * Adds a {@link OnErrorChangedListener} that will be invoked when the checkbox error state
249
+ * changes.
250
+ *
251
+ * <p>Components that add a listener should take care to remove it when finished via {@link
252
+ * #removeOnErrorChangedListener(OnErrorChangedListener)}.
253
+ *
254
+ * @param listener listener to add
255
+ */
256
+ public void addOnErrorChangedListener (@ NonNull OnErrorChangedListener listener ) {
257
+ onErrorChangedListeners .add (listener );
258
+ }
259
+
260
+ /**
261
+ * Remove a listener that was previously added via {@link
262
+ * #addOnErrorChangedListener(OnErrorChangedListener)}
263
+ *
264
+ * @param listener listener to remove
265
+ */
266
+ public void removeOnErrorChangedListener (@ NonNull OnErrorChangedListener listener ) {
267
+ onErrorChangedListeners .remove (listener );
268
+ }
269
+
270
+ /** Remove all previously added {@link OnErrorChangedListener}s. */
271
+ public void clearOnErrorChangedListeners () {
272
+ onErrorChangedListeners .clear ();
273
+ }
274
+
133
275
/**
134
276
* Forces the {@link MaterialCheckBox} to use colors from a Material Theme. Overrides any
135
277
* specified ButtonTintList. If set to false, sets the tints to null. Use {@link
@@ -167,21 +309,24 @@ public boolean isCenterIfNoTextEnabled() {
167
309
168
310
private ColorStateList getMaterialThemeColorsTintList () {
169
311
if (materialThemeColorsTintList == null ) {
170
- int [] checkBoxColorsList = new int [ENABLED_CHECKED_STATES .length ];
312
+ int [] checkBoxColorsList = new int [CHECKBOX_STATES .length ];
171
313
int colorControlActivated = MaterialColors .getColor (this , R .attr .colorControlActivated );
314
+ int colorError = MaterialColors .getColor (this , R .attr .colorError );
172
315
int colorSurface = MaterialColors .getColor (this , R .attr .colorSurface );
173
316
int colorOnSurface = MaterialColors .getColor (this , R .attr .colorOnSurface );
174
317
175
318
checkBoxColorsList [0 ] =
176
- MaterialColors .layer (colorSurface , colorControlActivated , MaterialColors .ALPHA_FULL );
319
+ MaterialColors .layer (colorSurface , colorError , MaterialColors .ALPHA_FULL );
177
320
checkBoxColorsList [1 ] =
178
- MaterialColors .layer (colorSurface , colorOnSurface , MaterialColors .ALPHA_MEDIUM );
321
+ MaterialColors .layer (colorSurface , colorControlActivated , MaterialColors .ALPHA_FULL );
179
322
checkBoxColorsList [2 ] =
180
- MaterialColors .layer (colorSurface , colorOnSurface , MaterialColors .ALPHA_DISABLED );
323
+ MaterialColors .layer (colorSurface , colorOnSurface , MaterialColors .ALPHA_MEDIUM );
181
324
checkBoxColorsList [3 ] =
182
325
MaterialColors .layer (colorSurface , colorOnSurface , MaterialColors .ALPHA_DISABLED );
326
+ checkBoxColorsList [4 ] =
327
+ MaterialColors .layer (colorSurface , colorOnSurface , MaterialColors .ALPHA_DISABLED );
183
328
184
- materialThemeColorsTintList = new ColorStateList (ENABLED_CHECKED_STATES , checkBoxColorsList );
329
+ materialThemeColorsTintList = new ColorStateList (CHECKBOX_STATES , checkBoxColorsList );
185
330
}
186
331
return materialThemeColorsTintList ;
187
332
}
0 commit comments