onErrorChangedListeners =
+ new LinkedHashSet<>();
@Nullable private ColorStateList materialThemeColorsTintList;
private boolean useMaterialThemeColors;
private boolean centerIfNoTextEnabled;
+ private boolean errorShown;
+ private CharSequence errorAccessibilityLabel;
+
+ /**
+ * Callback interface invoked when the checkbox error state changes.
+ */
+ public interface OnErrorChangedListener {
+
+ /**
+ * Called when the error state of a checkbox changes.
+ *
+ * @param checkBox the {@link MaterialCheckBox}
+ * @param errorShown whether the checkbox is on error
+ */
+ void onErrorChanged(@NonNull MaterialCheckBox checkBox, boolean errorShown);
+ }
public MaterialCheckBox(Context context) {
this(context, null);
@@ -90,6 +114,9 @@ public MaterialCheckBox(Context context, @Nullable AttributeSet attrs, int defSt
attributes.getBoolean(R.styleable.MaterialCheckBox_useMaterialThemeColors, false);
centerIfNoTextEnabled =
attributes.getBoolean(R.styleable.MaterialCheckBox_centerIfNoTextEnabled, true);
+ errorShown = attributes.getBoolean(R.styleable.MaterialCheckBox_errorShown, false);
+ errorAccessibilityLabel =
+ attributes.getText(R.styleable.MaterialCheckBox_errorAccessibilityLabel);
attributes.recycle();
}
@@ -130,6 +157,121 @@ protected void onAttachedToWindow() {
}
}
+ @Override
+ protected int[] onCreateDrawableState(int extraSpace) {
+ final int[] drawableStates = super.onCreateDrawableState(extraSpace + 1);
+
+ if (isErrorShown()) {
+ mergeDrawableStates(drawableStates, ERROR_STATE_SET);
+ }
+
+ return drawableStates;
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(@Nullable AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ if (info == null) {
+ return;
+ }
+
+ if (isErrorShown()) {
+ info.setText(info.getText() + ", " + errorAccessibilityLabel);
+ }
+ }
+
+ /**
+ * Sets whether the checkbox should be on error state. If true, the error color will be applied to
+ * the checkbox.
+ *
+ * @param errorShown whether the checkbox should be on error state.
+ * @see #isErrorShown()
+ * @attr ref com.google.android.material.R.styleable#MaterialCheckBox_errorShown
+ */
+ public void setErrorShown(boolean errorShown) {
+ if (this.errorShown == errorShown) {
+ return;
+ }
+ this.errorShown = errorShown;
+ refreshDrawableState();
+ for (OnErrorChangedListener listener : onErrorChangedListeners) {
+ listener.onErrorChanged(this, this.errorShown);
+ }
+ }
+
+ /**
+ * Returns whether the checkbox is on error state.
+ *
+ * @see #setErrorShown(boolean)
+ * @attr ref com.google.android.material.R.styleable#MaterialCheckBox_errorShown
+ */
+ public boolean isErrorShown() {
+ return errorShown;
+ }
+
+ /**
+ * Sets the accessibility label to be used for the error state announcement by screen readers.
+ *
+ * @param resId resource ID of the error announcement text
+ * @see #setErrorShown(boolean)
+ * @see #getErrorAccessibilityLabel()
+ * @attr ref com.google.android.material.R.styleable#MaterialCheckBox_errorAccessibilityLabel
+ */
+ public void setErrorAccessibilityLabelResource(@StringRes int resId) {
+ setErrorAccessibilityLabel(resId != 0 ? getResources().getText(resId) : null);
+ }
+
+ /**
+ * Sets the accessibility label to be used for the error state announcement by screen readers.
+ *
+ * @param errorAccessibilityLabel the error announcement
+ * @see #setErrorShown(boolean)
+ * @see #getErrorAccessibilityLabel()
+ * @attr ref com.google.android.material.R.styleable#MaterialCheckBox_errorAccessibilityLabel
+ */
+ public void setErrorAccessibilityLabel(@Nullable CharSequence errorAccessibilityLabel) {
+ this.errorAccessibilityLabel = errorAccessibilityLabel;
+ }
+
+ /**
+ * Returns the accessibility label used for the error state announcement.
+ *
+ * @see #setErrorAccessibilityLabel(CharSequence)
+ * @attr ref com.google.android.material.R.styleable#MaterialCheckBox_errorAccessibilityLabel
+ */
+ @Nullable
+ public CharSequence getErrorAccessibilityLabel() {
+ return errorAccessibilityLabel;
+ }
+
+ /**
+ * Adds a {@link OnErrorChangedListener} that will be invoked when the checkbox error state
+ * changes.
+ *
+ * Components that add a listener should take care to remove it when finished via {@link
+ * #removeOnErrorChangedListener(OnErrorChangedListener)}.
+ *
+ * @param listener listener to add
+ */
+ public void addOnErrorChangedListener(@NonNull OnErrorChangedListener listener) {
+ onErrorChangedListeners.add(listener);
+ }
+
+ /**
+ * Remove a listener that was previously added via {@link
+ * #addOnErrorChangedListener(OnErrorChangedListener)}
+ *
+ * @param listener listener to remove
+ */
+ public void removeOnErrorChangedListener(@NonNull OnErrorChangedListener listener) {
+ onErrorChangedListeners.remove(listener);
+ }
+
+ /** Remove all previously added {@link OnErrorChangedListener}s. */
+ public void clearOnErrorChangedListeners() {
+ onErrorChangedListeners.clear();
+ }
+
/**
* Forces the {@link MaterialCheckBox} to use colors from a Material Theme. Overrides any
* specified ButtonTintList. If set to false, sets the tints to null. Use {@link
@@ -167,21 +309,24 @@ public boolean isCenterIfNoTextEnabled() {
private ColorStateList getMaterialThemeColorsTintList() {
if (materialThemeColorsTintList == null) {
- int[] checkBoxColorsList = new int[ENABLED_CHECKED_STATES.length];
+ int[] checkBoxColorsList = new int[CHECKBOX_STATES.length];
int colorControlActivated = MaterialColors.getColor(this, R.attr.colorControlActivated);
+ int colorError = MaterialColors.getColor(this, R.attr.colorError);
int colorSurface = MaterialColors.getColor(this, R.attr.colorSurface);
int colorOnSurface = MaterialColors.getColor(this, R.attr.colorOnSurface);
checkBoxColorsList[0] =
- MaterialColors.layer(colorSurface, colorControlActivated, MaterialColors.ALPHA_FULL);
+ MaterialColors.layer(colorSurface, colorError, MaterialColors.ALPHA_FULL);
checkBoxColorsList[1] =
- MaterialColors.layer(colorSurface, colorOnSurface, MaterialColors.ALPHA_MEDIUM);
+ MaterialColors.layer(colorSurface, colorControlActivated, MaterialColors.ALPHA_FULL);
checkBoxColorsList[2] =
- MaterialColors.layer(colorSurface, colorOnSurface, MaterialColors.ALPHA_DISABLED);
+ MaterialColors.layer(colorSurface, colorOnSurface, MaterialColors.ALPHA_MEDIUM);
checkBoxColorsList[3] =
MaterialColors.layer(colorSurface, colorOnSurface, MaterialColors.ALPHA_DISABLED);
+ checkBoxColorsList[4] =
+ MaterialColors.layer(colorSurface, colorOnSurface, MaterialColors.ALPHA_DISABLED);
- materialThemeColorsTintList = new ColorStateList(ENABLED_CHECKED_STATES, checkBoxColorsList);
+ materialThemeColorsTintList = new ColorStateList(CHECKBOX_STATES, checkBoxColorsList);
}
return materialThemeColorsTintList;
}
diff --git a/lib/java/com/google/android/material/checkbox/res-public/values/public.xml b/lib/java/com/google/android/material/checkbox/res-public/values/public.xml
index cbaff48669d..4466c0a6af7 100644
--- a/lib/java/com/google/android/material/checkbox/res-public/values/public.xml
+++ b/lib/java/com/google/android/material/checkbox/res-public/values/public.xml
@@ -20,4 +20,7 @@
+
+
+
diff --git a/lib/java/com/google/android/material/checkbox/res/color/m3_checkbox_button_tint.xml b/lib/java/com/google/android/material/checkbox/res/color/m3_checkbox_button_tint.xml
new file mode 100644
index 00000000000..cb0b521f90e
--- /dev/null
+++ b/lib/java/com/google/android/material/checkbox/res/color/m3_checkbox_button_tint.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lib/java/com/google/android/material/checkbox/res/values/attrs.xml b/lib/java/com/google/android/material/checkbox/res/values/attrs.xml
index 3ea19be5f4d..90e8766b20d 100644
--- a/lib/java/com/google/android/material/checkbox/res/values/attrs.xml
+++ b/lib/java/com/google/android/material/checkbox/res/values/attrs.xml
@@ -26,5 +26,18 @@
+
+
+
+
+
+
+
+
+
diff --git a/lib/java/com/google/android/material/checkbox/res/values/strings.xml b/lib/java/com/google/android/material/checkbox/res/values/strings.xml
new file mode 100644
index 00000000000..95bd57d99f5
--- /dev/null
+++ b/lib/java/com/google/android/material/checkbox/res/values/strings.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+ Error: invalid
+
+
diff --git a/lib/java/com/google/android/material/checkbox/res/values/styles.xml b/lib/java/com/google/android/material/checkbox/res/values/styles.xml
index ceb20582aba..259469b895f 100644
--- a/lib/java/com/google/android/material/checkbox/res/values/styles.xml
+++ b/lib/java/com/google/android/material/checkbox/res/values/styles.xml
@@ -26,7 +26,8 @@
diff --git a/lib/javatests/com/google/android/material/checkbox/MaterialCheckBoxTest.java b/lib/javatests/com/google/android/material/checkbox/MaterialCheckBoxTest.java
index b6ce903b066..c717a990fd3 100644
--- a/lib/javatests/com/google/android/material/checkbox/MaterialCheckBoxTest.java
+++ b/lib/javatests/com/google/android/material/checkbox/MaterialCheckBoxTest.java
@@ -19,6 +19,8 @@
import com.google.android.material.test.R;
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
import android.content.res.ColorStateList;
import android.graphics.Color;
@@ -28,10 +30,12 @@
import android.view.View;
import android.widget.CheckBox;
import androidx.core.widget.CompoundButtonCompat;
+import com.google.android.material.checkbox.MaterialCheckBox.OnErrorChangedListener;
import com.google.android.material.color.MaterialColors;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.Mockito;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
@@ -41,15 +45,56 @@ public class MaterialCheckBoxTest {
private static final int[] STATE_CHECKED =
new int[] {android.R.attr.state_enabled, android.R.attr.state_checked};
+ private static final int[] STATE_ERROR =
+ new int[] {android.R.attr.state_enabled, R.attr.state_error};
private static final int[] STATE_UNCHECKED = new int[] {android.R.attr.state_enabled};
private AppCompatActivity activity;
private View checkboxes;
+ private MaterialCheckBox materialCheckBox;
@Before
public void createAndThemeApplicationContext() {
activity = Robolectric.buildActivity(TestActivity.class).setup().get();
checkboxes = activity.getLayoutInflater().inflate(R.layout.test_design_checkbox, null);
+ materialCheckBox = checkboxes.findViewById(R.id.test_checkbox);
+ }
+
+ @Test
+ public void testSetError_succeeds() {
+ materialCheckBox.setErrorShown(true);
+
+ assertThat(materialCheckBox.isErrorShown()).isTrue();
+ }
+
+ @Test
+ public void testSetError_callsListener() {
+ OnErrorChangedListener mockListener = Mockito.mock(OnErrorChangedListener.class);
+ materialCheckBox.setErrorShown(false);
+ materialCheckBox.addOnErrorChangedListener(mockListener);
+
+ materialCheckBox.setErrorShown(true);
+
+ verify(mockListener).onErrorChanged(materialCheckBox, /* errorShown= */ true);
+ }
+
+ @Test
+ public void testSetError_withSameValue_doesNotCallListener() {
+ OnErrorChangedListener mockListener = Mockito.mock(OnErrorChangedListener.class);
+ materialCheckBox.setErrorShown(false);
+ materialCheckBox.addOnErrorChangedListener(mockListener);
+
+ materialCheckBox.setErrorShown(false);
+
+ verify(mockListener, never()).onErrorChanged(materialCheckBox, /* errorShown= */ false);
+ }
+
+ @Test
+ public void testSetErrorA11yLabel_succeeds() {
+ materialCheckBox.setErrorAccessibilityLabel("error");
+
+ assertThat(materialCheckBox.getErrorAccessibilityLabel()).isNotNull();
+ assertThat(materialCheckBox.getErrorAccessibilityLabel().toString()).isEqualTo("error");
}
@Test
@@ -75,8 +120,11 @@ public void testThemeableAndroidButtonTint() {
*/
static void testThemeableButtonTint(CheckBox checkBox) {
ColorStateList buttonTintList = CompoundButtonCompat.getButtonTintList(checkBox);
+
assertThat(buttonTintList.getColorForState(STATE_CHECKED, Color.BLACK))
.isEqualTo(MaterialColors.getColor(checkBox, R.attr.colorControlActivated));
+ assertThat(buttonTintList.getColorForState(STATE_ERROR, Color.BLACK))
+ .isEqualTo(MaterialColors.getColor(checkBox, R.attr.colorError));
assertThat(buttonTintList.getColorForState(STATE_UNCHECKED, Color.BLACK))
.isEqualTo(MaterialColors.getColor(checkBox, R.attr.colorOnSurface));
}
diff --git a/lib/javatests/com/google/android/material/checkbox/res/color/checkbox_themeable_attribute_color.xml b/lib/javatests/com/google/android/material/checkbox/res/color/checkbox_themeable_attribute_color.xml
index 7b55f4ca089..73e77a950fc 100644
--- a/lib/javatests/com/google/android/material/checkbox/res/color/checkbox_themeable_attribute_color.xml
+++ b/lib/javatests/com/google/android/material/checkbox/res/color/checkbox_themeable_attribute_color.xml
@@ -14,7 +14,9 @@
limitations under the License.
-->
-
+
+
diff --git a/lib/javatests/com/google/android/material/checkbox/res/layout/test_design_checkbox.xml b/lib/javatests/com/google/android/material/checkbox/res/layout/test_design_checkbox.xml
index cb4d7dbbe7c..063351d62f5 100644
--- a/lib/javatests/com/google/android/material/checkbox/res/layout/test_design_checkbox.xml
+++ b/lib/javatests/com/google/android/material/checkbox/res/layout/test_design_checkbox.xml
@@ -19,6 +19,11 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
+
+