Skip to content

Commit 4da7ce1

Browse files
committedAug 11, 2022
[Checkbox] Added indeterminate state support to the checkbox.
A MaterialCheckBox can now be checked, unchecked, or indeterminate. Resolves #857 PiperOrigin-RevId: 465598224 (cherry picked from commit 8f149b1)
1 parent 81c2ded commit 4da7ce1

18 files changed

+874
-22
lines changed
 

‎catalog/java/io/material/catalog/checkbox/CheckBoxMainDemoFragment.java

+66
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,19 @@
2424
import android.view.ViewGroup;
2525
import android.widget.CheckBox;
2626
import android.widget.CompoundButton;
27+
import androidx.annotation.NonNull;
2728
import androidx.annotation.Nullable;
2829
import com.google.android.material.checkbox.MaterialCheckBox;
30+
import com.google.android.material.checkbox.MaterialCheckBox.OnCheckedStateChangedListener;
2931
import io.material.catalog.feature.DemoFragment;
3032
import io.material.catalog.feature.DemoUtils;
3133
import java.util.List;
3234

3335
/** A fragment that displays the main Checkbox demos for the Catalog app. */
3436
public class CheckBoxMainDemoFragment extends DemoFragment {
3537

38+
private boolean isUpdatingChildren = false;
39+
3640
@Override
3741
public View onCreateDemoView(
3842
LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle) {
@@ -57,6 +61,68 @@ public View onCreateDemoView(
5761
cb.setErrorShown(isChecked);
5862
}
5963
});
64+
65+
CheckBox firstChild = view.findViewById(R.id.checkbox_child_1);
66+
firstChild.setChecked(true);
67+
ViewGroup indeterminateContainer = view.findViewById(R.id.checkbox_indeterminate_container);
68+
List<CheckBox> childrenCheckBoxes =
69+
DemoUtils.findViewsWithType(indeterminateContainer, CheckBox.class);
70+
MaterialCheckBox checkBoxParent = view.findViewById(R.id.checkbox_parent);
71+
OnCheckedStateChangedListener parentOnCheckedStateChangedListener =
72+
(checkBox, state) -> {
73+
boolean isChecked = checkBox.isChecked();
74+
if (state != MaterialCheckBox.STATE_INDETERMINATE) {
75+
isUpdatingChildren = true;
76+
for (CheckBox child : childrenCheckBoxes) {
77+
child.setChecked(isChecked);
78+
}
79+
isUpdatingChildren = false;
80+
}
81+
};
82+
checkBoxParent.addOnCheckedStateChangedListener(parentOnCheckedStateChangedListener);
83+
84+
OnCheckedStateChangedListener childOnCheckedStateChangedListener =
85+
(checkBox, state) -> {
86+
if (isUpdatingChildren) {
87+
return;
88+
}
89+
setParentState(checkBoxParent, childrenCheckBoxes, parentOnCheckedStateChangedListener);
90+
};
91+
92+
for (CheckBox child : childrenCheckBoxes) {
93+
((MaterialCheckBox) child)
94+
.addOnCheckedStateChangedListener(childOnCheckedStateChangedListener);
95+
}
96+
97+
setParentState(checkBoxParent, childrenCheckBoxes, parentOnCheckedStateChangedListener);
98+
6099
return view;
61100
}
101+
102+
private void setParentState(
103+
@NonNull MaterialCheckBox checkBoxParent,
104+
@NonNull List<CheckBox> childrenCheckBoxes,
105+
@NonNull OnCheckedStateChangedListener parentOnCheckedStateChangedListener) {
106+
boolean allChecked = true;
107+
boolean noneChecked = true;
108+
for (CheckBox child : childrenCheckBoxes) {
109+
if (!child.isChecked()) {
110+
allChecked = false;
111+
} else {
112+
noneChecked = false;
113+
}
114+
if (!allChecked && !noneChecked) {
115+
break;
116+
}
117+
}
118+
checkBoxParent.removeOnCheckedStateChangedListener(parentOnCheckedStateChangedListener);
119+
if (allChecked) {
120+
checkBoxParent.setChecked(true);
121+
} else if (noneChecked) {
122+
checkBoxParent.setChecked(false);
123+
} else {
124+
checkBoxParent.setCheckedState(MaterialCheckBox.STATE_INDETERMINATE);
125+
}
126+
checkBoxParent.addOnCheckedStateChangedListener(parentOnCheckedStateChangedListener);
127+
}
62128
}

‎catalog/java/io/material/catalog/checkbox/res/layout/cat_checkbox.xml

+67-6
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@
1616
limitations under the License.
1717
-->
1818

19-
<ScrollView
20-
xmlns:android="http://schemas.android.com/apk/res/android"
19+
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
2120
android:layout_width="match_parent"
22-
android:layout_height="match_parent">
21+
android:layout_height="match_parent"
22+
xmlns:app="http://schemas.android.com/apk/res-auto">
2323

2424
<LinearLayout
2525
android:id="@+id/main_viewGroup"
@@ -31,13 +31,13 @@
3131
<GridLayout
3232
android:layout_width="wrap_content"
3333
android:layout_height="wrap_content"
34-
android:columnCount="2"
34+
android:columnCount="3"
3535
android:useDefaultMargins="true">
3636

3737
<TextView
3838
android:layout_width="match_parent"
3939
android:layout_height="match_parent"
40-
android:layout_columnSpan="2"
40+
android:layout_columnSpan="3"
4141
android:text="@string/cat_checkbox_intro" />
4242

4343
<CheckBox
@@ -54,10 +54,17 @@
5454
android:enabled="true"
5555
android:text="@string/cat_checkbox_enabled" />
5656

57+
<CheckBox
58+
android:layout_width="match_parent"
59+
android:layout_height="match_parent"
60+
app:checkedState="indeterminate"
61+
android:enabled="true"
62+
android:text="@string/cat_checkbox_enabled" />
63+
5764
<TextView
5865
android:layout_width="match_parent"
5966
android:layout_height="match_parent"
60-
android:layout_columnSpan="2"
67+
android:layout_columnSpan="3"
6168
android:text="@string/cat_checkbox_disabled_guide" />
6269

6370
<CheckBox
@@ -73,6 +80,13 @@
7380
android:checked="false"
7481
android:enabled="false"
7582
android:text="@string/cat_checkbox_disabled" />
83+
84+
<CheckBox
85+
android:layout_width="match_parent"
86+
android:layout_height="match_parent"
87+
app:checkedState="indeterminate"
88+
android:enabled="false"
89+
android:text="@string/cat_checkbox_disabled" />
7690
</GridLayout>
7791

7892
<com.google.android.material.divider.MaterialDivider
@@ -111,6 +125,53 @@
111125
android:text="@string/cat_checkbox_toggle" />
112126
</LinearLayout>
113127

128+
<com.google.android.material.divider.MaterialDivider
129+
android:layout_width="match_parent"
130+
android:layout_height="wrap_content"
131+
android:layout_marginTop="@dimen/cat_checkbox_line_separator_margin"
132+
android:layout_marginBottom="@dimen/cat_checkbox_line_separator_margin" />
133+
134+
<TextView
135+
android:layout_width="match_parent"
136+
android:layout_height="match_parent"
137+
android:text="@string/cat_indeterminate_description" />
138+
139+
<CheckBox
140+
android:id="@+id/checkbox_parent"
141+
android:layout_width="match_parent"
142+
android:layout_height="match_parent"
143+
app:checkedState="indeterminate"
144+
android:text="@string/cat_checkbox_indeterminate" />
145+
146+
<LinearLayout
147+
android:id="@+id/checkbox_indeterminate_container"
148+
android:layout_width="match_parent"
149+
android:layout_height="wrap_content"
150+
android:layout_marginStart="@dimen/cat_checkbox_indent"
151+
android:orientation="vertical">
152+
153+
<CheckBox
154+
android:id="@+id/checkbox_child_1"
155+
android:layout_width="match_parent"
156+
android:layout_height="match_parent"
157+
android:checked="true"
158+
android:text="@string/cat_checkbox_indeterminate_child" />
159+
160+
<CheckBox
161+
android:id="@+id/checkbox_child_2"
162+
android:layout_width="match_parent"
163+
android:layout_height="match_parent"
164+
android:checked="false"
165+
android:text="@string/cat_checkbox_indeterminate_child" />
166+
167+
<CheckBox
168+
android:id="@+id/checkbox_child_3"
169+
android:layout_width="match_parent"
170+
android:layout_height="match_parent"
171+
android:checked="false"
172+
android:text="@string/cat_checkbox_indeterminate_child" />
173+
</LinearLayout>
174+
114175
<com.google.android.material.divider.MaterialDivider
115176
android:layout_width="match_parent"
116177
android:layout_height="wrap_content"

‎catalog/java/io/material/catalog/checkbox/res/values/strings.xml

+8
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@
3131
Check this checkbox to control the enabled state of the boxes below.</string>
3232
<string name="cat_checkbox_toggle" translatable="false">
3333
Controlled by a checkbox</string>
34+
<string name="cat_indeterminate_description" translatable="false">
35+
The checkboxes below have a parent/child relationship. The parent\'s state
36+
depends on the state of the children.
37+
</string>
38+
<string name="cat_checkbox_indeterminate" translatable="false">
39+
Parent checkbox</string>
40+
<string name="cat_checkbox_indeterminate_child" translatable="false">
41+
Child checkbox</string>
3442
<string name="cat_checkbox_no_text" translatable="false">
3543
Below is a checkbox with no text.</string>
3644
<string name="cat_checkbox_toggle_error" translatable="false">

‎docs/components/Checkbox.md

+24-4
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,37 @@ checkbox.errorShown = true
7373
// Optional listener:
7474
checkbox.addOnErrorChangedListener { checkBox, errorShown ->
7575
// Responds to when the checkbox enters/leaves error state
76-
}
7776
}
7877

7978
// To set a custom accessibility label:
8079
checkbox.errorAccessibilityLabel = "Error: custom error announcement."
8180

8281
```
8382

83+
### Making a checkbox indeterminate
84+
85+
In the layout:
86+
87+
```xml
88+
<CheckBox
89+
...
90+
app:checkedState="indeterminate"/>
91+
```
92+
93+
In code:
94+
95+
```kt
96+
// You can set the state of the checkbox (STATE_CHECKED, STATE_UNCHECKED,
97+
// or STATE_INDETERMINATE) via setCheckedState.
98+
checkBox.setCheckedState(MaterialCheckbox.STATE_INDETERMINATE);
99+
100+
// Checkbox state listener.
101+
checkbox.addOnCheckedStateChangedListener { checkBox, state ->
102+
// Responds to when the checkbox changes state.
103+
}
104+
```
105+
106+
84107
## Checkbox
85108

86109
A checkbox is a square button with a check to denote its current state.
@@ -193,9 +216,6 @@ enabled, disabled, hover, focused, and pressed states.
193216
pressed. Rows are selected, unselected, or
194217
indeterminite](assets/checkbox/checkbox_states.png)
195218

196-
**Note:** `MaterialCheckBox` does not support the indeterminate state. Only
197-
selected and unselected states are supported.
198-
199219
### Styles
200220

201221
Element | Style

‎lib/java/com/google/android/material/checkbox/MaterialCheckBox.java

+293-4
Large diffs are not rendered by default.

‎lib/java/com/google/android/material/checkbox/res-public/values/public.xml

+2
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323
<public name="buttonIconTint" type="attr"/>
2424
<public name="buttonIconTintMode" type="attr"/>
2525
<public name="centerIfNoTextEnabled" type="attr"/>
26+
<public name="checkedState" type="attr"/>
2627
<public name="errorShown" type="attr"/>
2728
<public name="errorAccessibilityLabel" type="attr"/>
2829
<public name="state_error" type="attr"/>
30+
<public name="state_indeterminate" type="attr"/>
2931
</resources>

‎lib/java/com/google/android/material/checkbox/res/color/m3_checkbox_button_icon_tint.xml

+3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
<!-- Error -->
2323
<item android:color="?attr/colorOnError" app:state_error="true"/>
2424

25+
<!-- Indeterminate -->
26+
<item android:color="?attr/colorOnPrimary" app:state_indeterminate="true"/>
27+
2528
<!-- Checked -->
2629
<item android:color="?attr/colorOnPrimary" android:state_checked="true"/>
2730

‎lib/java/com/google/android/material/checkbox/res/color/m3_checkbox_button_tint.xml

+3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
<!-- Error -->
2323
<item android:color="?attr/colorError" app:state_error="true"/>
2424

25+
<!-- Indeterminate -->
26+
<item android:color="?attr/colorPrimary" app:state_indeterminate="true"/>
27+
2528
<!-- Checked -->
2629
<item android:color="?attr/colorPrimary" android:state_checked="true"/>
2730

‎lib/java/com/google/android/material/checkbox/res/drawable/mtrl_checkbox_button.xml

+14
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,16 @@
1616
-->
1717
<animated-selector xmlns:android="http://schemas.android.com/apk/res/android"
1818
xmlns:tools="http://schemas.android.com/tools"
19+
xmlns:app="http://schemas.android.com/apk/res-auto"
1920
tools:ignore="NewApi">
2021
<item
2122
android:id="@+id/checked"
2223
android:drawable="@drawable/mtrl_ic_checkbox_checked"
2324
android:state_checked="true"/>
25+
<item
26+
android:id="@+id/indeterminate"
27+
android:drawable="@drawable/mtrl_ic_checkbox_checked"
28+
app:state_indeterminate="true"/>
2429
<item
2530
android:id="@+id/unchecked"
2631
android:drawable="@drawable/mtrl_ic_checkbox_unchecked"
@@ -34,4 +39,13 @@
3439
android:fromId="@+id/unchecked"
3540
android:toId="@+id/checked"
3641
android:drawable="@drawable/mtrl_checkbox_button_unchecked_checked" />
42+
43+
<transition
44+
android:fromId="@+id/indeterminate"
45+
android:toId="@+id/unchecked"
46+
android:drawable="@drawable/mtrl_checkbox_button_checked_unchecked" />
47+
<transition
48+
android:fromId="@+id/unchecked"
49+
android:toId="@+id/indeterminate"
50+
android:drawable="@drawable/mtrl_checkbox_button_unchecked_checked" />
3751
</animated-selector>

‎lib/java/com/google/android/material/checkbox/res/drawable/mtrl_checkbox_button_icon.xml

+23
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,16 @@
1616
-->
1717
<animated-selector xmlns:android="http://schemas.android.com/apk/res/android"
1818
xmlns:tools="http://schemas.android.com/tools"
19+
xmlns:app="http://schemas.android.com/apk/res-auto"
1920
tools:ignore="NewApi">
2021
<item
2122
android:id="@+id/checked"
2223
android:drawable="@drawable/mtrl_ic_check_mark"
2324
android:state_checked="true"/>
25+
<item
26+
android:id="@+id/indeterminate"
27+
android:drawable="@drawable/mtrl_ic_indeterminate"
28+
app:state_indeterminate="true"/>
2429
<item
2530
android:id="@+id/unchecked"
2631
android:drawable="@android:color/transparent"
@@ -34,4 +39,22 @@
3439
android:fromId="@+id/unchecked"
3540
android:toId="@+id/checked"
3641
android:drawable="@drawable/mtrl_checkbox_button_icon_unchecked_checked" />
42+
43+
<transition
44+
android:fromId="@+id/indeterminate"
45+
android:toId="@+id/unchecked"
46+
android:drawable="@drawable/mtrl_checkbox_button_icon_indeterminate_unchecked" />
47+
<transition
48+
android:fromId="@+id/unchecked"
49+
android:toId="@+id/indeterminate"
50+
android:drawable="@drawable/mtrl_checkbox_button_icon_unchecked_indeterminate" />
51+
52+
<transition
53+
android:fromId="@+id/indeterminate"
54+
android:toId="@+id/checked"
55+
android:drawable="@drawable/mtrl_checkbox_button_icon_indeterminate_checked" />
56+
<transition
57+
android:fromId="@+id/checked"
58+
android:toId="@+id/indeterminate"
59+
android:drawable="@drawable/mtrl_checkbox_button_icon_checked_indeterminate" />
3760
</animated-selector>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!--
3+
~ Copyright (C) 2022 The Android Open Source Project
4+
~
5+
~ Licensed under the Apache License, Version 2.0 (the "License");
6+
~ you may not use this file except in compliance with the License.
7+
~ You may obtain a copy of the License at
8+
~
9+
~ http://www.apache.org/licenses/LICENSE-2.0
10+
~
11+
~ Unless required by applicable law or agreed to in writing, software
12+
~ distributed under the License is distributed on an "AS IS" BASIS,
13+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
~ See the License for the specific language governing permissions and
15+
~ limitations under the License.
16+
-->
17+
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
18+
xmlns:aapt="http://schemas.android.com/aapt"
19+
xmlns:tools="http://schemas.android.com/tools"
20+
android:drawable="@drawable/mtrl_ic_check_mark"
21+
tools:ignore="NewApi">
22+
<target android:name="@string/mtrl_checkbox_button_icon_path_name">
23+
<aapt:attr name="android:animation">
24+
<objectAnimator
25+
android:duration="@integer/m3_sys_motion_duration_350"
26+
android:interpolator="?attr/motionEasingEmphasizedAccelerateInterpolator"
27+
android:propertyName="pathData"
28+
android:valueFrom="@string/mtrl_checkbox_button_icon_path_checked"
29+
android:valueTo="@string/mtrl_checkbox_button_icon_path_indeterminate"
30+
android:valueType="pathType"/>
31+
</aapt:attr>
32+
</target>
33+
</animated-vector>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!--
3+
~ Copyright (C) 2022 The Android Open Source Project
4+
~
5+
~ Licensed under the Apache License, Version 2.0 (the "License");
6+
~ you may not use this file except in compliance with the License.
7+
~ You may obtain a copy of the License at
8+
~
9+
~ http://www.apache.org/licenses/LICENSE-2.0
10+
~
11+
~ Unless required by applicable law or agreed to in writing, software
12+
~ distributed under the License is distributed on an "AS IS" BASIS,
13+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
~ See the License for the specific language governing permissions and
15+
~ limitations under the License.
16+
-->
17+
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
18+
xmlns:aapt="http://schemas.android.com/aapt"
19+
xmlns:tools="http://schemas.android.com/tools"
20+
android:drawable="@drawable/mtrl_ic_indeterminate"
21+
tools:ignore="NewApi">
22+
<target android:name="@string/mtrl_checkbox_button_icon_path_name">
23+
<aapt:attr name="android:animation">
24+
<objectAnimator
25+
android:duration="@integer/m3_sys_motion_duration_350"
26+
android:interpolator="?attr/motionEasingEmphasizedAccelerateInterpolator"
27+
android:propertyName="pathData"
28+
android:valueFrom="@string/mtrl_checkbox_button_icon_path_indeterminate"
29+
android:valueTo="@string/mtrl_checkbox_button_icon_path_checked"
30+
android:valueType="pathType"/>
31+
</aapt:attr>
32+
</target>
33+
</animated-vector>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!--
3+
~ Copyright (C) 2022 The Android Open Source Project
4+
~
5+
~ Licensed under the Apache License, Version 2.0 (the "License");
6+
~ you may not use this file except in compliance with the License.
7+
~ You may obtain a copy of the License at
8+
~
9+
~ http://www.apache.org/licenses/LICENSE-2.0
10+
~
11+
~ Unless required by applicable law or agreed to in writing, software
12+
~ distributed under the License is distributed on an "AS IS" BASIS,
13+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
~ See the License for the specific language governing permissions and
15+
~ limitations under the License.
16+
-->
17+
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
18+
xmlns:aapt="http://schemas.android.com/aapt"
19+
xmlns:tools="http://schemas.android.com/tools"
20+
android:drawable="@drawable/mtrl_ic_indeterminate"
21+
tools:ignore="NewApi">
22+
<target android:name="@string/mtrl_checkbox_button_icon_path_group_name">
23+
<aapt:attr name="android:animation">
24+
<objectAnimator
25+
android:duration="@integer/m3_sys_motion_duration_150"
26+
android:interpolator="?attr/motionEasingEmphasizedAccelerateInterpolator"
27+
android:propertyName="scaleX"
28+
android:valueFrom="1.0"
29+
android:valueTo="0.6"
30+
android:valueType="floatType"/>
31+
</aapt:attr>
32+
</target>
33+
<target android:name="@string/mtrl_checkbox_button_icon_path_group_name">
34+
<aapt:attr name="android:animation">
35+
<objectAnimator
36+
android:duration="@integer/m3_sys_motion_duration_150"
37+
android:interpolator="?attr/motionEasingEmphasizedAccelerateInterpolator"
38+
android:propertyName="scaleY"
39+
android:valueFrom="1.0"
40+
android:valueTo="0.6"
41+
android:valueType="floatType"/>
42+
</aapt:attr>
43+
</target>
44+
<target android:name="@string/mtrl_checkbox_button_icon_path_name">
45+
<aapt:attr name="android:animation">
46+
<objectAnimator
47+
android:duration="@integer/m3_sys_motion_duration_50"
48+
android:interpolator="?attr/motionEasingLinearInterpolator"
49+
android:propertyName="fillAlpha"
50+
android:valueFrom="1.0"
51+
android:valueTo="0.0"
52+
android:valueType="floatType"/>
53+
</aapt:attr>
54+
</target>
55+
</animated-vector>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!--
3+
~ Copyright (C) 2022 The Android Open Source Project
4+
~
5+
~ Licensed under the Apache License, Version 2.0 (the "License");
6+
~ you may not use this file except in compliance with the License.
7+
~ You may obtain a copy of the License at
8+
~
9+
~ http://www.apache.org/licenses/LICENSE-2.0
10+
~
11+
~ Unless required by applicable law or agreed to in writing, software
12+
~ distributed under the License is distributed on an "AS IS" BASIS,
13+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
~ See the License for the specific language governing permissions and
15+
~ limitations under the License.
16+
-->
17+
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
18+
xmlns:aapt="http://schemas.android.com/aapt"
19+
xmlns:tools="http://schemas.android.com/tools"
20+
android:drawable="@drawable/mtrl_ic_indeterminate"
21+
tools:ignore="NewApi">
22+
<target android:name="@string/mtrl_checkbox_button_icon_path_group_name">
23+
<aapt:attr name="android:animation">
24+
<objectAnimator
25+
android:duration="@integer/m3_sys_motion_duration_350"
26+
android:interpolator="?attr/motionEasingEmphasizedDecelerateInterpolator"
27+
android:propertyName="scaleX"
28+
android:valueFrom="0.6"
29+
android:valueTo="1.0"
30+
android:valueType="floatType"/>
31+
</aapt:attr>
32+
</target>
33+
<target android:name="@string/mtrl_checkbox_button_icon_path_group_name">
34+
<aapt:attr name="android:animation">
35+
<objectAnimator
36+
android:duration="@integer/m3_sys_motion_duration_350"
37+
android:interpolator="?attr/motionEasingEmphasizedDecelerateInterpolator"
38+
android:propertyName="scaleY"
39+
android:valueFrom="0.6"
40+
android:valueTo="1.0"
41+
android:valueType="floatType"/>
42+
</aapt:attr>
43+
</target>
44+
<target android:name="@string/mtrl_checkbox_button_icon_path_name">
45+
<aapt:attr name="android:animation">
46+
<objectAnimator
47+
android:duration="@integer/m3_sys_motion_duration_50"
48+
android:interpolator="?attr/motionEasingLinearInterpolator"
49+
android:propertyName="fillAlpha"
50+
android:valueFrom="0.0"
51+
android:valueTo="1.0"
52+
android:valueType="floatType"/>
53+
</aapt:attr>
54+
</target>
55+
</animated-vector>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!--
3+
~ Copyright (C) 2022 The Android Open Source Project
4+
~
5+
~ Licensed under the Apache License, Version 2.0 (the "License");
6+
~ you may not use this file except in compliance with the License.
7+
~ You may obtain a copy of the License at
8+
~
9+
~ http://www.apache.org/licenses/LICENSE-2.0
10+
~
11+
~ Unless required by applicable law or agreed to in writing, software
12+
~ distributed under the License is distributed on an "AS IS" BASIS,
13+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
~ See the License for the specific language governing permissions and
15+
~ limitations under the License.
16+
-->
17+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
18+
xmlns:tools="http://schemas.android.com/tools"
19+
android:width="32dp"
20+
android:height="32dp"
21+
android:viewportWidth="32"
22+
android:viewportHeight="32"
23+
tools:ignore="NewApi">
24+
<group
25+
android:name="@string/mtrl_checkbox_button_icon_path_group_name"
26+
android:pivotX="16"
27+
android:pivotY="16">
28+
<path
29+
android:name="@string/mtrl_checkbox_button_icon_path_name"
30+
android:fillColor="@android:color/white"
31+
android:pathData="@string/mtrl_checkbox_button_icon_path_indeterminate"/>
32+
</group>
33+
</vector>

‎lib/java/com/google/android/material/checkbox/res/values/attrs.xml

+14-2
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,24 @@
6161
<!-- The error accessibility label that is announced by screen readers when
6262
the checkbox is in the error state. -->
6363
<attr name="errorAccessibilityLabel" format="string"/>
64+
<!-- The state of the checkbox. Can be unchecked, checked, or indeterminate. -->
65+
<attr name="checkedState">
66+
<!-- The unchecked state of the checkbox. -->
67+
<enum name="unchecked" value="0"/>
68+
<!-- The checked state of the checkbox. -->
69+
<enum name="checked" value="1"/>
70+
<!-- The indeterminate state of the checkbox. -->
71+
<enum name="indeterminate" value="2"/>
72+
</attr>
6473
</declare-styleable>
6574

6675
<declare-styleable name="MaterialCheckBoxStates">
6776
<!-- Error state of the checkbox. Behaves as part of existing states; a
68-
checkbox is always one of checked/unchecked and can also be on error or
69-
not. -->
77+
checkbox is always one of checked/unchecked/indeterminate and can also
78+
be on error or not. -->
7079
<attr name="state_error" format="boolean"/>
80+
<!-- Indeterminate state of the checkbox. Behaves as a third state; a
81+
checkbox can be checked, unchecked, or indeterminate. -->
82+
<attr name="state_indeterminate" format="boolean"/>
7183
</declare-styleable>
7284
</resources>

‎lib/java/com/google/android/material/checkbox/res/values/strings.xml

+13
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,17 @@
1919
<string name="error_a11y_label" description="Screen reader announcement for when a checkbox is on error. [CHAR LIMIT=NONE]">
2020
Error: invalid
2121
</string>
22+
<string name="mtrl_checkbox_state_description_checked" description="Description for when checkbox is checked. [CHAR LIMIT=32]">
23+
Checked
24+
</string>
25+
<string name="mtrl_checkbox_state_description_unchecked" description="Description for when checkbox is unchecked. [CHAR LIMIT=32]">
26+
Not checked
27+
</string>
28+
<string name="mtrl_checkbox_state_description_indeterminate" description="Description for when checkbox is indeterminate. [CHAR LIMIT=32]">
29+
Indeterminate
30+
</string>
31+
32+
<!-- Drawables strings. -->
2233
<string name="mtrl_checkbox_button_path_group_name" translatable="false">button</string>
2334
<string name="mtrl_checkbox_button_path_name" translatable="false">button path</string>
2435
<string name="mtrl_checkbox_button_icon_path_group_name" translatable="false">icon</string>
@@ -29,4 +40,6 @@
2940
<string name="mtrl_checkbox_button_path_unchecked" translatable="false">M23,7H9C7.9,7,7,7.9,7,9v14c0,1.1,0.9,2,2,2h14c1.1,0,2-0.9,2-2V9C25,7.9,24.1,7,23,7z M23,23H9V9h14V23z</string>
3041
<!-- Path data to draw a checkmark icon when the checkbox is checked. -->
3142
<string name="mtrl_checkbox_button_icon_path_checked" translatable="false">M14,18.2 11.4,15.6 10,17 14,21 22,13 20.6,11.6z</string>
43+
<!-- Path data to draw the indeterminate icon. -->
44+
<string name="mtrl_checkbox_button_icon_path_indeterminate" translatable="false">M13.4,15 11,15 11,17 13.4,17 21,17 21,15z</string>
3245
</resources>

‎lib/javatests/com/google/android/material/checkbox/MaterialCheckBoxTest.java

+135-6
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@
2929
import androidx.appcompat.app.AppCompatActivity;
3030
import android.view.View;
3131
import android.widget.CheckBox;
32+
import android.widget.CompoundButton.OnCheckedChangeListener;
3233
import androidx.core.widget.CompoundButtonCompat;
34+
import com.google.android.material.checkbox.MaterialCheckBox.OnCheckedStateChangedListener;
3335
import com.google.android.material.checkbox.MaterialCheckBox.OnErrorChangedListener;
3436
import com.google.android.material.color.MaterialColors;
3537
import org.junit.Before;
@@ -52,6 +54,12 @@ public class MaterialCheckBoxTest {
5254
private AppCompatActivity activity;
5355
private View checkboxes;
5456
private MaterialCheckBox materialCheckBox;
57+
private final OnCheckedChangeListener mockCheckedListener =
58+
Mockito.mock(OnCheckedChangeListener.class);
59+
private final OnErrorChangedListener mockErrorListener =
60+
Mockito.mock(OnErrorChangedListener.class);
61+
private final OnCheckedStateChangedListener mockStateListener =
62+
Mockito.mock(OnCheckedStateChangedListener.class);
5563

5664
@Before
5765
public void createAndThemeApplicationContext() {
@@ -60,6 +68,129 @@ public void createAndThemeApplicationContext() {
6068
materialCheckBox = checkboxes.findViewById(R.id.test_checkbox);
6169
}
6270

71+
@Test
72+
public void testSetCheckedState_checked_succeeds() {
73+
materialCheckBox.setCheckedState(MaterialCheckBox.STATE_CHECKED);
74+
75+
assertThat(materialCheckBox.isChecked()).isTrue();
76+
assertThat(materialCheckBox.getCheckedState()).isEqualTo(MaterialCheckBox.STATE_CHECKED);
77+
}
78+
79+
@Test
80+
public void testSetCheckedState_unchecked_succeeds() {
81+
materialCheckBox.setCheckedState(MaterialCheckBox.STATE_CHECKED);
82+
assertThat(materialCheckBox.isChecked()).isTrue();
83+
84+
materialCheckBox.setCheckedState(MaterialCheckBox.STATE_UNCHECKED);
85+
86+
assertThat(materialCheckBox.isChecked()).isFalse();
87+
assertThat(materialCheckBox.getCheckedState()).isEqualTo(MaterialCheckBox.STATE_UNCHECKED);
88+
}
89+
90+
@Test
91+
public void testSetCheckedState_indeterminate_succeeds() {
92+
materialCheckBox.setCheckedState(MaterialCheckBox.STATE_INDETERMINATE);
93+
94+
assertThat(materialCheckBox.isChecked()).isFalse();
95+
assertThat(materialCheckBox.getCheckedState()).isEqualTo(MaterialCheckBox.STATE_INDETERMINATE);
96+
}
97+
98+
@Test
99+
public void testSetCheckedState_checkedToIndeterminate_succeeds() {
100+
materialCheckBox.setCheckedState(MaterialCheckBox.STATE_CHECKED);
101+
102+
materialCheckBox.setCheckedState(MaterialCheckBox.STATE_INDETERMINATE);
103+
104+
assertThat(materialCheckBox.isChecked()).isFalse();
105+
assertThat(materialCheckBox.getCheckedState()).isEqualTo(MaterialCheckBox.STATE_INDETERMINATE);
106+
}
107+
108+
@Test
109+
public void testSetCheckedState_checked_callsStateAndCheckedListeners() {
110+
materialCheckBox.addOnCheckedStateChangedListener(mockStateListener);
111+
materialCheckBox.setOnCheckedChangeListener(mockCheckedListener);
112+
113+
materialCheckBox.setCheckedState(MaterialCheckBox.STATE_CHECKED);
114+
115+
verify(mockStateListener)
116+
.onCheckedStateChangedListener(materialCheckBox, MaterialCheckBox.STATE_CHECKED);
117+
verify(mockCheckedListener).onCheckedChanged(materialCheckBox, /* isChecked= */ true);
118+
}
119+
120+
@Test
121+
public void testSetCheckedState_unchecked_callsStateAndCheckedListeners() {
122+
materialCheckBox.setCheckedState(MaterialCheckBox.STATE_CHECKED);
123+
materialCheckBox.addOnCheckedStateChangedListener(mockStateListener);
124+
materialCheckBox.setOnCheckedChangeListener(mockCheckedListener);
125+
126+
materialCheckBox.setCheckedState(MaterialCheckBox.STATE_UNCHECKED);
127+
128+
verify(mockStateListener)
129+
.onCheckedStateChangedListener(materialCheckBox, MaterialCheckBox.STATE_UNCHECKED);
130+
verify(mockCheckedListener).onCheckedChanged(materialCheckBox, /* isChecked= */ false);
131+
}
132+
133+
@Test
134+
public void testSetCheckedState_indeterminate_callsStateListener() {
135+
materialCheckBox.addOnCheckedStateChangedListener(mockStateListener);
136+
materialCheckBox.setOnCheckedChangeListener(mockCheckedListener);
137+
138+
materialCheckBox.setCheckedState(MaterialCheckBox.STATE_INDETERMINATE);
139+
140+
verify(mockStateListener)
141+
.onCheckedStateChangedListener(materialCheckBox, MaterialCheckBox.STATE_INDETERMINATE);
142+
verify(mockCheckedListener, never()).onCheckedChanged(materialCheckBox, /* isChecked= */ false);
143+
}
144+
145+
@Test
146+
public void testSetChecked_succeeds() {
147+
materialCheckBox.setChecked(true);
148+
149+
assertThat(materialCheckBox.isChecked()).isTrue();
150+
assertThat(materialCheckBox.getCheckedState()).isEqualTo(MaterialCheckBox.STATE_CHECKED);
151+
}
152+
153+
@Test
154+
public void testSetUnchecked_succeeds() {
155+
materialCheckBox.setChecked(true);
156+
assertThat(materialCheckBox.isChecked()).isTrue();
157+
158+
materialCheckBox.setChecked(false);
159+
160+
assertThat(materialCheckBox.isChecked()).isFalse();
161+
assertThat(materialCheckBox.getCheckedState()).isEqualTo(MaterialCheckBox.STATE_UNCHECKED);
162+
}
163+
164+
@Test
165+
public void testIndeterminate_onClick_becomesChecked() {
166+
materialCheckBox.setCheckedState(MaterialCheckBox.STATE_INDETERMINATE);
167+
168+
materialCheckBox.performClick();
169+
170+
assertThat(materialCheckBox.isChecked()).isTrue();
171+
assertThat(materialCheckBox.getCheckedState()).isEqualTo(MaterialCheckBox.STATE_CHECKED);
172+
}
173+
174+
@Test
175+
public void testIndeterminate_setChecked_becomesChecked() {
176+
materialCheckBox.setCheckedState(MaterialCheckBox.STATE_INDETERMINATE);
177+
178+
materialCheckBox.setChecked(true);
179+
180+
assertThat(materialCheckBox.isChecked()).isTrue();
181+
assertThat(materialCheckBox.getCheckedState()).isEqualTo(MaterialCheckBox.STATE_CHECKED);
182+
}
183+
184+
@Test
185+
public void testIndeterminate_setUnchecked_becomesUnchecked() {
186+
materialCheckBox.setCheckedState(MaterialCheckBox.STATE_INDETERMINATE);
187+
188+
materialCheckBox.setChecked(false);
189+
190+
assertThat(materialCheckBox.isChecked()).isFalse();
191+
assertThat(materialCheckBox.getCheckedState()).isEqualTo(MaterialCheckBox.STATE_UNCHECKED);
192+
}
193+
63194
@Test
64195
public void testSetError_succeeds() {
65196
materialCheckBox.setErrorShown(true);
@@ -69,24 +200,22 @@ public void testSetError_succeeds() {
69200

70201
@Test
71202
public void testSetError_callsListener() {
72-
OnErrorChangedListener mockListener = Mockito.mock(OnErrorChangedListener.class);
73203
materialCheckBox.setErrorShown(false);
74-
materialCheckBox.addOnErrorChangedListener(mockListener);
204+
materialCheckBox.addOnErrorChangedListener(mockErrorListener);
75205

76206
materialCheckBox.setErrorShown(true);
77207

78-
verify(mockListener).onErrorChanged(materialCheckBox, /* errorShown= */ true);
208+
verify(mockErrorListener).onErrorChanged(materialCheckBox, /* errorShown= */ true);
79209
}
80210

81211
@Test
82212
public void testSetError_withSameValue_doesNotCallListener() {
83-
OnErrorChangedListener mockListener = Mockito.mock(OnErrorChangedListener.class);
84213
materialCheckBox.setErrorShown(false);
85-
materialCheckBox.addOnErrorChangedListener(mockListener);
214+
materialCheckBox.addOnErrorChangedListener(mockErrorListener);
86215

87216
materialCheckBox.setErrorShown(false);
88217

89-
verify(mockListener, never()).onErrorChanged(materialCheckBox, /* errorShown= */ false);
218+
verify(mockErrorListener, never()).onErrorChanged(materialCheckBox, /* errorShown= */ false);
90219
}
91220

92221
@Test

0 commit comments

Comments
 (0)
Please sign in to comment.