diff --git a/docs/components/Checkbox.md b/docs/components/Checkbox.md index 7dcd7332eeb..74427fef34f 100644 --- a/docs/components/Checkbox.md +++ b/docs/components/Checkbox.md @@ -35,6 +35,13 @@ Material Components for Android library. For more information, go to the [Getting started](https://github.com/material-components/material-components-android/tree/master/docs/getting-started.md) page. +```xml + +``` + **Note:** `` is auto-inflated as `` via `MaterialComponentsViewInflater` when using a `Theme.Material3.*` @@ -47,6 +54,33 @@ screen readers, such as TalkBack. Text rendered in check boxes is automatically provided to accessibility services. Additional content labels are usually unnecessary. +### Setting the error state on checkbox + +In the layout: + +```xml + +``` + +In code: + +```kt +// Set error. +checkbox.errorShown = true + +// Optional listener: +checkbox.addOnErrorChangedListener { checkBox, errorShown -> + // Responds to when the checkbox enters/leaves error state + } +} + +// To set a custom accessibility label: +checkbox.errorAccessibilityLabel = "Error: custom error announcement." + +``` + ## Checkbox A checkbox is a square button with a check to denote its current state. @@ -75,24 +109,24 @@ In the layout: ```xml ``` diff --git a/lib/java/com/google/android/material/checkbox/MaterialCheckBox.java b/lib/java/com/google/android/material/checkbox/MaterialCheckBox.java index 4023b3f58a1..76578d221aa 100644 --- a/lib/java/com/google/android/material/checkbox/MaterialCheckBox.java +++ b/lib/java/com/google/android/material/checkbox/MaterialCheckBox.java @@ -29,13 +29,17 @@ import androidx.appcompat.widget.AppCompatCheckBox; import android.text.TextUtils; import android.util.AttributeSet; +import android.view.accessibility.AccessibilityNodeInfo; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.StringRes; import androidx.core.graphics.drawable.DrawableCompat; import androidx.core.widget.CompoundButtonCompat; import com.google.android.material.color.MaterialColors; import com.google.android.material.internal.ThemeEnforcement; import com.google.android.material.internal.ViewUtils; import com.google.android.material.resources.MaterialResources; +import java.util.LinkedHashSet; /** * A class that creates a Material Themed CheckBox. @@ -49,16 +53,36 @@ public class MaterialCheckBox extends AppCompatCheckBox { private static final int DEF_STYLE_RES = R.style.Widget_MaterialComponents_CompoundButton_CheckBox; - private static final int[][] ENABLED_CHECKED_STATES = + private static final int[] ERROR_STATE_SET = {R.attr.state_error}; + private static final int[][] CHECKBOX_STATES = new int[][] { - new int[] {android.R.attr.state_enabled, android.R.attr.state_checked}, // [0] - new int[] {android.R.attr.state_enabled, -android.R.attr.state_checked}, // [1] - new int[] {-android.R.attr.state_enabled, android.R.attr.state_checked}, // [2] - new int[] {-android.R.attr.state_enabled, -android.R.attr.state_checked} // [3] + new int[] {android.R.attr.state_enabled, R.attr.state_error}, // [0] + new int[] {android.R.attr.state_enabled, android.R.attr.state_checked}, // [1] + new int[] {android.R.attr.state_enabled, -android.R.attr.state_checked}, // [2] + new int[] {-android.R.attr.state_enabled, android.R.attr.state_checked}, // [3] + new int[] {-android.R.attr.state_enabled, -android.R.attr.state_checked} // [4] }; + @NonNull private final LinkedHashSet 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 @@