Checkbox Rework 4
This CL reworks API and internals of the Checkbox to fix bugs, remove unnecessary features and add new functionality.
In particular:
* checkbox now support color customization for checked, unchecked, disabled states and for checkMark
* properly spec-ed colors out of the box
* support for disabled UI state
* new simplified animation
* more screenshot tests
Fixes: 144798073
Fixes: 152764755
Test: new screenshot tests were added, also will update golden screenshots for old tests
Change-Id: Iad338dbfbd2a12bc95d16fd2c895e67188d70f6a
diff --git a/ui/ui-material/api/0.1.0-dev14.txt b/ui/ui-material/api/0.1.0-dev14.txt
index 45c0703..7915cab 100644
--- a/ui/ui-material/api/0.1.0-dev14.txt
+++ b/ui/ui-material/api/0.1.0-dev14.txt
@@ -43,8 +43,8 @@
}
public final class CheckboxKt {
- method @androidx.compose.Composable public static void Checkbox-D4zOgQA(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, boolean enabled = true, androidx.ui.core.Modifier modifier = Modifier, long color = MaterialTheme.colors.secondary);
- method @androidx.compose.Composable public static void TriStateCheckbox--Buf9SY(androidx.ui.foundation.selection.ToggleableState state, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, boolean enabled = true, androidx.ui.core.Modifier modifier = Modifier, long color = MaterialTheme.colors.secondary);
+ method @androidx.compose.Composable public static void Checkbox-9ViTWCU(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, boolean enabled = true, androidx.ui.core.Modifier modifier = Modifier, long checkedColor = MaterialTheme.colors.secondary, long uncheckedColor = MaterialTheme.colors.onSurface, long disabledColor = MaterialTheme.colors.onSurface, long checkMarkColor = MaterialTheme.colors.surface);
+ method @androidx.compose.Composable public static void TriStateCheckbox-4vNNdMw(androidx.ui.foundation.selection.ToggleableState state, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, boolean enabled = true, androidx.ui.core.Modifier modifier = Modifier, long checkedColor = MaterialTheme.colors.secondary, long uncheckedColor = MaterialTheme.colors.onSurface, long disabledColor = MaterialTheme.colors.onSurface, long checkMarkColor = MaterialTheme.colors.surface);
}
public final class ColorKt {
diff --git a/ui/ui-material/api/current.txt b/ui/ui-material/api/current.txt
index 45c0703..7915cab 100644
--- a/ui/ui-material/api/current.txt
+++ b/ui/ui-material/api/current.txt
@@ -43,8 +43,8 @@
}
public final class CheckboxKt {
- method @androidx.compose.Composable public static void Checkbox-D4zOgQA(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, boolean enabled = true, androidx.ui.core.Modifier modifier = Modifier, long color = MaterialTheme.colors.secondary);
- method @androidx.compose.Composable public static void TriStateCheckbox--Buf9SY(androidx.ui.foundation.selection.ToggleableState state, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, boolean enabled = true, androidx.ui.core.Modifier modifier = Modifier, long color = MaterialTheme.colors.secondary);
+ method @androidx.compose.Composable public static void Checkbox-9ViTWCU(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, boolean enabled = true, androidx.ui.core.Modifier modifier = Modifier, long checkedColor = MaterialTheme.colors.secondary, long uncheckedColor = MaterialTheme.colors.onSurface, long disabledColor = MaterialTheme.colors.onSurface, long checkMarkColor = MaterialTheme.colors.surface);
+ method @androidx.compose.Composable public static void TriStateCheckbox-4vNNdMw(androidx.ui.foundation.selection.ToggleableState state, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, boolean enabled = true, androidx.ui.core.Modifier modifier = Modifier, long checkedColor = MaterialTheme.colors.secondary, long uncheckedColor = MaterialTheme.colors.onSurface, long disabledColor = MaterialTheme.colors.onSurface, long checkMarkColor = MaterialTheme.colors.surface);
}
public final class ColorKt {
diff --git a/ui/ui-material/api/public_plus_experimental_0.1.0-dev14.txt b/ui/ui-material/api/public_plus_experimental_0.1.0-dev14.txt
index 45c0703..7915cab 100644
--- a/ui/ui-material/api/public_plus_experimental_0.1.0-dev14.txt
+++ b/ui/ui-material/api/public_plus_experimental_0.1.0-dev14.txt
@@ -43,8 +43,8 @@
}
public final class CheckboxKt {
- method @androidx.compose.Composable public static void Checkbox-D4zOgQA(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, boolean enabled = true, androidx.ui.core.Modifier modifier = Modifier, long color = MaterialTheme.colors.secondary);
- method @androidx.compose.Composable public static void TriStateCheckbox--Buf9SY(androidx.ui.foundation.selection.ToggleableState state, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, boolean enabled = true, androidx.ui.core.Modifier modifier = Modifier, long color = MaterialTheme.colors.secondary);
+ method @androidx.compose.Composable public static void Checkbox-9ViTWCU(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, boolean enabled = true, androidx.ui.core.Modifier modifier = Modifier, long checkedColor = MaterialTheme.colors.secondary, long uncheckedColor = MaterialTheme.colors.onSurface, long disabledColor = MaterialTheme.colors.onSurface, long checkMarkColor = MaterialTheme.colors.surface);
+ method @androidx.compose.Composable public static void TriStateCheckbox-4vNNdMw(androidx.ui.foundation.selection.ToggleableState state, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, boolean enabled = true, androidx.ui.core.Modifier modifier = Modifier, long checkedColor = MaterialTheme.colors.secondary, long uncheckedColor = MaterialTheme.colors.onSurface, long disabledColor = MaterialTheme.colors.onSurface, long checkMarkColor = MaterialTheme.colors.surface);
}
public final class ColorKt {
diff --git a/ui/ui-material/api/public_plus_experimental_current.txt b/ui/ui-material/api/public_plus_experimental_current.txt
index 45c0703..7915cab 100644
--- a/ui/ui-material/api/public_plus_experimental_current.txt
+++ b/ui/ui-material/api/public_plus_experimental_current.txt
@@ -43,8 +43,8 @@
}
public final class CheckboxKt {
- method @androidx.compose.Composable public static void Checkbox-D4zOgQA(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, boolean enabled = true, androidx.ui.core.Modifier modifier = Modifier, long color = MaterialTheme.colors.secondary);
- method @androidx.compose.Composable public static void TriStateCheckbox--Buf9SY(androidx.ui.foundation.selection.ToggleableState state, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, boolean enabled = true, androidx.ui.core.Modifier modifier = Modifier, long color = MaterialTheme.colors.secondary);
+ method @androidx.compose.Composable public static void Checkbox-9ViTWCU(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, boolean enabled = true, androidx.ui.core.Modifier modifier = Modifier, long checkedColor = MaterialTheme.colors.secondary, long uncheckedColor = MaterialTheme.colors.onSurface, long disabledColor = MaterialTheme.colors.onSurface, long checkMarkColor = MaterialTheme.colors.surface);
+ method @androidx.compose.Composable public static void TriStateCheckbox-4vNNdMw(androidx.ui.foundation.selection.ToggleableState state, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, boolean enabled = true, androidx.ui.core.Modifier modifier = Modifier, long checkedColor = MaterialTheme.colors.secondary, long uncheckedColor = MaterialTheme.colors.onSurface, long disabledColor = MaterialTheme.colors.onSurface, long checkMarkColor = MaterialTheme.colors.surface);
}
public final class ColorKt {
diff --git a/ui/ui-material/api/restricted_0.1.0-dev14.txt b/ui/ui-material/api/restricted_0.1.0-dev14.txt
index 28f1ace..4c53601 100644
--- a/ui/ui-material/api/restricted_0.1.0-dev14.txt
+++ b/ui/ui-material/api/restricted_0.1.0-dev14.txt
@@ -44,8 +44,8 @@
}
public final class CheckboxKt {
- method @androidx.compose.Composable public static void Checkbox-D4zOgQA(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, boolean enabled = true, androidx.ui.core.Modifier modifier = Modifier, long color = MaterialTheme.colors.secondary);
- method @androidx.compose.Composable public static void TriStateCheckbox--Buf9SY(androidx.ui.foundation.selection.ToggleableState state, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, boolean enabled = true, androidx.ui.core.Modifier modifier = Modifier, long color = MaterialTheme.colors.secondary);
+ method @androidx.compose.Composable public static void Checkbox-9ViTWCU(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, boolean enabled = true, androidx.ui.core.Modifier modifier = Modifier, long checkedColor = MaterialTheme.colors.secondary, long uncheckedColor = MaterialTheme.colors.onSurface, long disabledColor = MaterialTheme.colors.onSurface, long checkMarkColor = MaterialTheme.colors.surface);
+ method @androidx.compose.Composable public static void TriStateCheckbox-4vNNdMw(androidx.ui.foundation.selection.ToggleableState state, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, boolean enabled = true, androidx.ui.core.Modifier modifier = Modifier, long checkedColor = MaterialTheme.colors.secondary, long uncheckedColor = MaterialTheme.colors.onSurface, long disabledColor = MaterialTheme.colors.onSurface, long checkMarkColor = MaterialTheme.colors.surface);
}
public final class ColorKt {
diff --git a/ui/ui-material/api/restricted_current.txt b/ui/ui-material/api/restricted_current.txt
index 28f1ace..4c53601 100644
--- a/ui/ui-material/api/restricted_current.txt
+++ b/ui/ui-material/api/restricted_current.txt
@@ -44,8 +44,8 @@
}
public final class CheckboxKt {
- method @androidx.compose.Composable public static void Checkbox-D4zOgQA(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, boolean enabled = true, androidx.ui.core.Modifier modifier = Modifier, long color = MaterialTheme.colors.secondary);
- method @androidx.compose.Composable public static void TriStateCheckbox--Buf9SY(androidx.ui.foundation.selection.ToggleableState state, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, boolean enabled = true, androidx.ui.core.Modifier modifier = Modifier, long color = MaterialTheme.colors.secondary);
+ method @androidx.compose.Composable public static void Checkbox-9ViTWCU(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, boolean enabled = true, androidx.ui.core.Modifier modifier = Modifier, long checkedColor = MaterialTheme.colors.secondary, long uncheckedColor = MaterialTheme.colors.onSurface, long disabledColor = MaterialTheme.colors.onSurface, long checkMarkColor = MaterialTheme.colors.surface);
+ method @androidx.compose.Composable public static void TriStateCheckbox-4vNNdMw(androidx.ui.foundation.selection.ToggleableState state, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, boolean enabled = true, androidx.ui.core.Modifier modifier = Modifier, long checkedColor = MaterialTheme.colors.secondary, long uncheckedColor = MaterialTheme.colors.onSurface, long disabledColor = MaterialTheme.colors.onSurface, long checkMarkColor = MaterialTheme.colors.surface);
}
public final class ColorKt {
diff --git a/ui/ui-material/samples/src/main/java/androidx/ui/material/samples/SelectionControlsSamples.kt b/ui/ui-material/samples/src/main/java/androidx/ui/material/samples/SelectionControlsSamples.kt
index 2336c26..52f77ac 100644
--- a/ui/ui-material/samples/src/main/java/androidx/ui/material/samples/SelectionControlsSamples.kt
+++ b/ui/ui-material/samples/src/main/java/androidx/ui/material/samples/SelectionControlsSamples.kt
@@ -58,7 +58,7 @@
onStateChange2(s)
}
- TriStateCheckbox(state = parentState, color = Color.Black)
+ TriStateCheckbox(state = parentState, checkedColor = Color.Black)
Column(Modifier.padding(10.dp, 0.dp, 0.dp, 0.dp)) {
Checkbox(state, onStateChange)
Checkbox(state2, onStateChange2)
diff --git a/ui/ui-material/src/androidTest/java/androidx/ui/material/CheckboxScreenshotTest.kt b/ui/ui-material/src/androidTest/java/androidx/ui/material/CheckboxScreenshotTest.kt
index ffd799b..9f2424b 100644
--- a/ui/ui-material/src/androidTest/java/androidx/ui/material/CheckboxScreenshotTest.kt
+++ b/ui/ui-material/src/androidTest/java/androidx/ui/material/CheckboxScreenshotTest.kt
@@ -23,13 +23,18 @@
import androidx.test.screenshot.assertAgainstGolden
import androidx.ui.core.Alignment
import androidx.ui.core.Modifier
+import androidx.ui.core.semantics.semantics
+import androidx.ui.core.testTag
+import androidx.ui.foundation.Box
+import androidx.ui.foundation.selection.ToggleableState
import androidx.ui.layout.wrapContentSize
import androidx.ui.test.captureToBitmap
import androidx.ui.test.createComposeRule
import androidx.ui.test.doClick
import androidx.ui.test.find
+import androidx.ui.test.findByTag
import androidx.ui.test.isToggleable
-import androidx.ui.test.runOnIdleCompose
+import androidx.ui.test.waitForIdle
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -48,32 +53,88 @@
val wrap = Modifier.wrapContentSize(Alignment.TopStart)
+ // TODO: this test tag as well as Boxes inside testa are temporarty, remove then b/157687898
+ // is fixed
+ private val wrapperTestTag = "checkboxWrapper"
+
@Test
fun checkBoxTest_checked() {
composeTestRule.setMaterialContent {
- Checkbox(modifier = wrap, checked = true, })
+ Box(wrap.semantics().testTag(wrapperTestTag)) {
+ Checkbox(checked = true, })
+ }
}
- find(isToggleable())
- .captureToBitmap()
- .assertAgainstGolden(screenshotRule, "checkbox_checked")
+ assertToggeableAgainstGolden("checkbox_checked")
}
@Test
fun checkBoxTest_unchecked() {
composeTestRule.setMaterialContent {
- Checkbox(modifier = wrap, checked = false, })
+ Box(wrap.semantics().testTag(wrapperTestTag)) {
+ Checkbox(modifier = wrap, checked = false, })
+ }
}
- find(isToggleable())
- .captureToBitmap()
- .assertAgainstGolden(screenshotRule, "checkbox_unchecked")
+ assertToggeableAgainstGolden("checkbox_unchecked")
+ }
+
+ @Test
+ fun checkBoxTest_indeterminate() {
+ composeTestRule.setMaterialContent {
+ Box(wrap.semantics().testTag(wrapperTestTag)) {
+ TriStateCheckbox(
+ state = ToggleableState.Indeterminate,
+ modifier = wrap,
+ >
+ }
+ }
+ assertToggeableAgainstGolden("checkbox_indeterminate")
+ }
+
+ @Test
+ fun checkBoxTest_disabled_checked() {
+ composeTestRule.setMaterialContent {
+ Box(wrap.semantics().testTag(wrapperTestTag)) {
+ Checkbox(modifier = wrap, checked = true, enabled = false, })
+ }
+ }
+ assertToggeableAgainstGolden("checkbox_disabled_checked")
+ }
+
+ @Test
+ fun checkBoxTest_disabled_unchecked() {
+ composeTestRule.setMaterialContent {
+ Box(wrap.semantics().testTag(wrapperTestTag)) {
+ Checkbox(modifier = wrap, checked = false, enabled = false, })
+ }
+ }
+ assertToggeableAgainstGolden("checkbox_disabled_unchecked")
+ }
+
+ @Test
+ fun checkBoxTest_disabled_indeterminate() {
+ composeTestRule.setMaterialContent {
+ Box(wrap.semantics().testTag(wrapperTestTag)) {
+ TriStateCheckbox(
+ state = ToggleableState.Indeterminate,
+ enabled = false,
+ modifier = wrap,
+ >
+ }
+ }
+ assertToggeableAgainstGolden("checkbox_disabled_indeterminate")
}
@Test
fun checkBoxTest_unchecked_animateToChecked() {
composeTestRule.setMaterialContent {
val isChecked = state { false }
- Checkbox(modifier = wrap, checked = isChecked.value,
- isChecked.value = it })
+ Box(wrap.semantics().testTag(wrapperTestTag)) {
+ Checkbox(
+ modifier = wrap,
+ checked = isChecked.value,
+ isChecked.value = it }
+ )
+ }
}
composeTestRule.clockTestRule.pauseClock()
@@ -81,21 +142,24 @@
find(isToggleable())
.doClick()
- runOnIdleCompose { }
+ waitForIdle()
- composeTestRule.clockTestRule.advanceClock(120)
+ composeTestRule.clockTestRule.advanceClock(60)
- find(isToggleable())
- .captureToBitmap()
- .assertAgainstGolden(screenshotRule, "checkbox_animateToChecked")
+ assertToggeableAgainstGolden("checkbox_animateToChecked")
}
@Test
fun checkBoxTest_checked_animateToUnchecked() {
composeTestRule.setMaterialContent {
val isChecked = state { true }
- Checkbox(modifier = wrap, checked = isChecked.value,
- isChecked.value = it })
+ Box(wrap.semantics().testTag(wrapperTestTag)) {
+ Checkbox(
+ modifier = wrap,
+ checked = isChecked.value,
+ isChecked.value = it }
+ )
+ }
}
composeTestRule.clockTestRule.pauseClock()
@@ -103,12 +167,17 @@
find(isToggleable())
.doClick()
- runOnIdleCompose { }
+ waitForIdle()
- composeTestRule.clockTestRule.advanceClock(120)
+ composeTestRule.clockTestRule.advanceClock(60)
- find(isToggleable())
+ assertToggeableAgainstGolden("checkbox_animateToUnchecked")
+ }
+
+ private fun assertToggeableAgainstGolden(goldenName: String) {
+ // TODO: replace with find(isToggeable()) after b/157687898 is fixed
+ findByTag(wrapperTestTag)
.captureToBitmap()
- .assertAgainstGolden(screenshotRule, "checkbox_animateToUnchecked")
+ .assertAgainstGolden(screenshotRule, goldenName)
}
}
\ No newline at end of file
diff --git a/ui/ui-material/src/main/java/androidx/ui/material/Checkbox.kt b/ui/ui-material/src/main/java/androidx/ui/material/Checkbox.kt
index cd5d33c..927aa89 100644
--- a/ui/ui-material/src/main/java/androidx/ui/material/Checkbox.kt
+++ b/ui/ui-material/src/main/java/androidx/ui/material/Checkbox.kt
@@ -16,36 +16,37 @@
package androidx.ui.material
+import android.graphics.PathMeasure
import androidx.animation.FloatPropKey
import androidx.animation.TransitionSpec
import androidx.animation.transitionDefinition
import androidx.compose.Composable
+import androidx.compose.Immutable
import androidx.compose.remember
import androidx.ui.animation.ColorPropKey
import androidx.ui.animation.Transition
+import androidx.ui.core.Alignment
import androidx.ui.core.Modifier
import androidx.ui.core.semantics.semantics
-import androidx.ui.foundation.Box
import androidx.ui.foundation.Canvas
-import androidx.ui.foundation.ContentGravity
import androidx.ui.foundation.selection.ToggleableState
import androidx.ui.foundation.selection.triStateToggleable
import androidx.ui.geometry.Offset
-import androidx.ui.geometry.RRect
import androidx.ui.geometry.Radius
import androidx.ui.geometry.Size
-import androidx.ui.geometry.outerRect
-import androidx.ui.geometry.shrink
-import androidx.ui.graphics.ClipOp
import androidx.ui.graphics.Color
+import androidx.ui.graphics.Path
import androidx.ui.graphics.StrokeCap
+import androidx.ui.graphics.asAndroidPath
import androidx.ui.graphics.drawscope.DrawScope
+import androidx.ui.graphics.drawscope.Fill
import androidx.ui.graphics.drawscope.Stroke
-import androidx.ui.graphics.drawscope.clipRect
import androidx.ui.layout.padding
-import androidx.ui.layout.preferredSize
+import androidx.ui.layout.size
+import androidx.ui.layout.wrapContentSize
import androidx.ui.material.ripple.RippleIndication
import androidx.ui.unit.dp
+import androidx.ui.util.lerp
/**
* A component that represents two states (checked / unchecked).
@@ -60,7 +61,10 @@
* @param enabled enabled whether or not this [Checkbox] will handle input events and appear
* enabled for semantics purposes
* @param modifier Modifier to be applied to the layout of the checkbox
- * @param color custom color for checkbox
+ * @param checkedColor color of the box when it is checked
+ * @param uncheckedColor color of the box border when it is unchecked
+ * @param disabledColor color for the checkbox to appear when disabled
+ * @param checkMarkColor color of the check mark of the [Checkbox]
*/
@Composable
fun Checkbox(
@@ -68,13 +72,19 @@
onCheckedChange: (Boolean) -> Unit,
enabled: Boolean = true,
modifier: Modifier = Modifier,
- color: Color = MaterialTheme.colors.secondary
+ checkedColor: Color = MaterialTheme.colors.secondary,
+ uncheckedColor: Color = MaterialTheme.colors.onSurface,
+ disabledColor: Color = MaterialTheme.colors.onSurface,
+ checkMarkColor: Color = MaterialTheme.colors.surface
) {
TriStateCheckbox(
state = ToggleableState(checked),
onCheckedChange(!checked) },
enabled = enabled,
- color = color,
+ checkedColor = checkedColor,
+ uncheckedColor = uncheckedColor,
+ checkMarkColor = checkMarkColor,
+ disabledColor = disabledColor,
modifier = modifier
)
}
@@ -96,7 +106,11 @@
* @param enabled enabled whether or not this [TriStateCheckbox] will handle input events and
* appear enabled for semantics purposes
* @param modifier Modifier to be applied to the layout of the checkbox
- * @param color custom color for checkbox
+ * @param checkedColor color of the box when it is in [ToggleableState.On] or [ToggleableState
+ * .Indeterminate] states
+ * @param uncheckedColor color of the box border when it is in [ToggleableState.Off] state
+ * @param disabledColor color for the checkbox to appear when disabled
+ * @param checkMarkColor color of the check mark of the [TriStateCheckbox]
*/
@Composable
fun TriStateCheckbox(
@@ -104,9 +118,13 @@
onClick: () -> Unit,
enabled: Boolean = true,
modifier: Modifier = Modifier,
- color: Color = MaterialTheme.colors.secondary
+ checkedColor: Color = MaterialTheme.colors.secondary,
+ uncheckedColor: Color = MaterialTheme.colors.onSurface,
+ disabledColor: Color = MaterialTheme.colors.onSurface,
+ checkMarkColor: Color = MaterialTheme.colors.surface
) {
- Box(
+ CheckboxImpl(
+ value = state,
modifier = modifier
.semantics(mergeAllDescendants = true)
.triStateToggleable(
@@ -114,110 +132,103 @@
>
enabled = enabled,
indication = RippleIndication(bounded = false)
- ),
- gravity = ContentGravity.Center
- ) {
- DrawCheckbox(
- value = state,
- activeColor = color,
- modifier = CheckboxDefaultPadding
- )
- }
+ )
+ .padding(CheckboxDefaultPadding),
+ enabled = enabled,
+ activeColor = checkedColor,
+ inactiveColor = uncheckedColor,
+ checkColor = checkMarkColor,
+ disabledColor = disabledColor
+ )
}
@Composable
-private fun DrawCheckbox(value: ToggleableState, activeColor: Color, modifier: Modifier) {
- val unselectedColor = MaterialTheme.colors.onSurface.copy(alpha = UncheckedBoxOpacity)
+private fun CheckboxImpl(
+ value: ToggleableState,
+ modifier: Modifier,
+ enabled: Boolean,
+ activeColor: Color,
+ inactiveColor: Color,
+ checkColor: Color,
+ disabledColor: Color
+) {
+ val unselectedColor = inactiveColor.copy(alpha = UncheckedBoxOpacity)
val definition = remember(activeColor, unselectedColor) {
generateTransitionDefinition(activeColor, unselectedColor)
}
+ val disabledEmphasis = EmphasisAmbient.current.disabled
+ val indeterminateDisabledColor = disabledEmphasis.applyEmphasis(activeColor)
+ val disabledEmphasisedColor = disabledEmphasis.applyEmphasis(disabledColor)
Transition(definition = definition, toState = value) { state ->
- Canvas(modifier.preferredSize(CheckboxSize)) {
+ val checkCache = remember { CheckDrawingCache() }
+ Canvas(modifier.wrapContentSize(Alignment.Center).size(CheckboxSize)) {
+ val boxColor =
+ if (enabled) {
+ activeColor.copy(alpha = state[BoxOpacityFraction])
+ } else if (value == ToggleableState.Indeterminate) {
+ indeterminateDisabledColor
+ } else if (value == ToggleableState.Off) {
+ Color.Transparent
+ } else {
+ disabledEmphasisedColor
+ }
+ val borderColor =
+ if (enabled) {
+ state[BoxBorderColor]
+ } else if (value == ToggleableState.Indeterminate) {
+ indeterminateDisabledColor
+ } else {
+ disabledEmphasisedColor
+ }
val strokeWidthPx = StrokeWidth.toPx()
drawBox(
- color = state[BoxColorProp],
- innerRadiusFraction = state[InnerRadiusFractionProp],
+ boxColor = boxColor,
+ borderColor = borderColor,
radius = RadiusSize.toPx(),
strokeWidth = strokeWidthPx
)
drawCheck(
- checkFraction = state[CheckFractionProp],
- crossCenterGravitation = state[CenterGravitationForCheck],
- strokeWidthPx = strokeWidthPx
+ checkColor = checkColor.copy(alpha = state[CheckOpacityFraction]),
+ checkFraction = state[CheckDrawFraction],
+ crossCenterGravitation = state[CheckCenterGravitationShiftFraction],
+ strokeWidthPx = strokeWidthPx,
+ drawingCache = checkCache
)
}
}
}
private fun DrawScope.drawBox(
- color: Color,
- innerRadiusFraction: Float,
+ boxColor: Color,
+ borderColor: Color,
radius: Float,
strokeWidth: Float
) {
val halfStrokeWidth = strokeWidth / 2.0f
val stroke = Stroke(strokeWidth)
val checkboxSize = size.width
-
- val outer = RRect(
- halfStrokeWidth,
- halfStrokeWidth,
- checkboxSize - halfStrokeWidth,
- checkboxSize - halfStrokeWidth,
- Radius(radius)
+ drawRoundRect(
+ boxColor,
+ topLeft = Offset(strokeWidth, strokeWidth),
+ size = Size(checkboxSize - strokeWidth * 2, checkboxSize - strokeWidth * 2),
+ radius = Radius(radius / 2),
+ style = Fill
)
-
- // Determine whether or not we need to offset the inset by a pixel
- // to ensure that there is no gap between the outer stroked round rect
- // and the inner rect.
- val offset = (halfStrokeWidth - halfStrokeWidth.toInt()) + 0.5f
-
- // TODO(malkov): this radius formula is not in material spec
-
- // If the inner region is to be filled such that it is larger than the outer stroke size
- // then create a difference clip to draw the stroke outside of the rectangular region
- // to be drawn within the interior rectangle. This is done to ensure that pixels do
- // not overlap which might cause unexpected blending if the target color has some
- // opacity. If the inner region is not to be drawn or will occupy a smaller width than
- // the outer stroke then just draw the outer stroke
- val innerStrokeWidth = innerRadiusFraction * checkboxSize / 2
- if (innerStrokeWidth > strokeWidth) {
- val clipRect = outer.shrink(strokeWidth / 2 - offset).outerRect()
- clipRect(clipRect.left, clipRect.top, clipRect.right, clipRect.bottom, ClipOp.difference) {
- drawRoundRect(
- color,
- Offset(outer.left, outer.top),
- Size(outer.width, outer.height),
- radius = Radius(radius),
- style = stroke
- )
- }
-
- clipRect(clipRect.left, clipRect.top, clipRect.right, clipRect.bottom) {
- val innerHalfStrokeWidth = innerStrokeWidth / 2
- val rect = outer.shrink(innerHalfStrokeWidth - offset).outerRect()
- drawRect(
- color = color,
- topLeft = Offset(rect.left, rect.top),
- size = Size(rect.width, rect.height),
- style = Stroke(innerStrokeWidth)
- )
- }
- } else {
- drawRoundRect(
- color,
- topLeft = Offset(outer.left, outer.top),
- size = Size(outer.width, outer.height),
- radius = Radius(radius),
- style = stroke
- )
- }
+ drawRoundRect(
+ borderColor,
+ topLeft = Offset(halfStrokeWidth, halfStrokeWidth),
+ size = Size(checkboxSize - strokeWidth, checkboxSize - strokeWidth),
+ radius = Radius(radius),
+ style = stroke
+ )
}
private fun DrawScope.drawCheck(
+ checkColor: Color,
checkFraction: Float,
crossCenterGravitation: Float,
- strokeWidthPx: Float
+ strokeWidthPx: Float,
+ drawingCache: CheckDrawingCache
) {
val stroke = Stroke(width = strokeWidthPx, cap = StrokeCap.square)
val width = size.width
@@ -228,111 +239,132 @@
val rightX = 0.8f
val rightY = 0.3f
- val gravitatedCrossX = calcMiddleValue(checkCrossX, 0.5f, crossCenterGravitation)
- val gravitatedCrossY = calcMiddleValue(checkCrossY, 0.5f, crossCenterGravitation)
-
+ val gravitatedCrossX = lerp(checkCrossX, 0.5f, crossCenterGravitation)
+ val gravitatedCrossY = lerp(checkCrossY, 0.5f, crossCenterGravitation)
// gravitate only Y for end to achieve center line
- val gravitatedLeftY = calcMiddleValue(leftY, 0.5f, crossCenterGravitation)
- val gravitatedRightY = calcMiddleValue(rightY, 0.5f, crossCenterGravitation)
+ val gravitatedLeftY = lerp(leftY, 0.5f, crossCenterGravitation)
+ val gravitatedRightY = lerp(rightY, 0.5f, crossCenterGravitation)
- val crossPoint = Offset(width * gravitatedCrossX, width * gravitatedCrossY)
- val rightBranch = Offset(
- width * calcMiddleValue(gravitatedCrossX, rightX, checkFraction),
- width * calcMiddleValue(gravitatedCrossY, gravitatedRightY, checkFraction)
- )
- val leftBranch = Offset(
- width * calcMiddleValue(gravitatedCrossX, leftX, checkFraction),
- width * calcMiddleValue(gravitatedCrossY, gravitatedLeftY, checkFraction)
- )
- drawLine(CheckStrokeDefaultColor, crossPoint, leftBranch, stroke)
- drawLine(CheckStrokeDefaultColor, crossPoint, rightBranch, stroke)
+ with(drawingCache) {
+ checkPath.reset()
+ checkPath.moveTo(width * leftX, width * gravitatedLeftY)
+ checkPath.lineTo(width * gravitatedCrossX, width * gravitatedCrossY)
+ checkPath.lineTo(width * rightX, width * gravitatedRightY)
+ // TODO: replace with proper declarative non-android alternative when ready (b/158188351)
+ pathMeasure.setPath(checkPath.asAndroidPath(), false)
+ pathToDraw.reset()
+ pathMeasure.getSegment(
+ 0f, pathMeasure.length * checkFraction, pathToDraw.asAndroidPath(), true
+ )
+ }
+ drawPath(drawingCache.pathToDraw, checkColor, style = stroke)
}
-private fun calcMiddleValue(start: Float, finish: Float, fraction: Float): Float {
- return start * (1 - fraction) + finish * fraction
-}
+@Immutable
+private class CheckDrawingCache(
+ val checkPath: Path = Path(),
+ val pathMeasure: PathMeasure = PathMeasure(),
+ val pathToDraw: Path = Path()
+)
// all float props are fraction now [0f .. 1f] as it seems convenient
-private val InnerRadiusFractionProp = FloatPropKey()
-private val CheckFractionProp = FloatPropKey()
-private val CenterGravitationForCheck = FloatPropKey()
-private val BoxColorProp = ColorPropKey()
+private val CheckDrawFraction = FloatPropKey()
+private val BoxOpacityFraction = FloatPropKey()
+private val CheckOpacityFraction = FloatPropKey()
+private val CheckCenterGravitationShiftFraction = FloatPropKey()
+private val BoxBorderColor = ColorPropKey()
-private val BoxAnimationDuration = 100
-private val CheckStrokeAnimationDuration = 100
+private val BoxInDuration = 50
+private val BoxOutDuration = 100
+private val CheckAnimationDuration = 100
private fun generateTransitionDefinition(color: Color, unselectedColor: Color) =
transitionDefinition {
state(ToggleableState.On) {
- this[CheckFractionProp] = 1f
- this[InnerRadiusFractionProp] = 1f
- this[CenterGravitationForCheck] = 0f
- this[BoxColorProp] = color
+ this[CheckDrawFraction] = 1f
+ this[BoxOpacityFraction] = 1f
+ this[CheckOpacityFraction] = 1f
+ this[CheckCenterGravitationShiftFraction] = 0f
+ this[BoxBorderColor] = color
}
state(ToggleableState.Off) {
- this[CheckFractionProp] = 0f
- this[InnerRadiusFractionProp] = 0f
- this[CenterGravitationForCheck] = 1f
- this[BoxColorProp] = unselectedColor
+ this[CheckDrawFraction] = 0f
+ this[BoxOpacityFraction] = 0f
+ this[CheckOpacityFraction] = 0f
+ this[CheckCenterGravitationShiftFraction] = 0f
+ this[BoxBorderColor] = unselectedColor
}
state(ToggleableState.Indeterminate) {
- this[CheckFractionProp] = 1f
- this[InnerRadiusFractionProp] = 1f
- this[CenterGravitationForCheck] = 1f
- this[BoxColorProp] = color
+ this[CheckDrawFraction] = 1f
+ this[BoxOpacityFraction] = 1f
+ this[CheckOpacityFraction] = 1f
+ this[CheckCenterGravitationShiftFraction] = 1f
+ this[BoxBorderColor] = color
}
- transition(fromState = ToggleableState.Off, toState = ToggleableState.On) {
- boxTransitionFromUnchecked()
- CenterGravitationForCheck using snap()
- }
- transition(fromState = ToggleableState.On, toState = ToggleableState.Off) {
- boxTransitionToUnchecked()
- CenterGravitationForCheck using tween {
- duration = CheckStrokeAnimationDuration
- }
+ transition(
+ ToggleableState.Off to ToggleableState.On,
+ ToggleableState.Off to ToggleableState.Indeterminate
+ ) {
+ boxTransitionToChecked()
}
transition(
ToggleableState.On to ToggleableState.Indeterminate,
ToggleableState.Indeterminate to ToggleableState.On
) {
- CenterGravitationForCheck using tween {
- duration = CheckStrokeAnimationDuration
+ CheckCenterGravitationShiftFraction using tween {
+ duration = CheckAnimationDuration
}
}
- transition(fromState = ToggleableState.Indeterminate, toState = ToggleableState.Off) {
- boxTransitionToUnchecked()
- }
- transition(fromState = ToggleableState.Off, toState = ToggleableState.Indeterminate) {
- boxTransitionFromUnchecked()
+ transition(
+ ToggleableState.Indeterminate to ToggleableState.Off,
+ ToggleableState.On to ToggleableState.Off
+ ) {
+ checkboxTransitionToUnchecked()
}
}
-private fun TransitionSpec<ToggleableState>.boxTransitionFromUnchecked() {
- BoxColorProp using snap()
- InnerRadiusFractionProp using tween {
- duration = BoxAnimationDuration
+private fun TransitionSpec<ToggleableState>.boxTransitionToChecked() {
+ CheckCenterGravitationShiftFraction using snap()
+ BoxBorderColor using tween {
+ duration = BoxInDuration
}
- CheckFractionProp using tween {
- duration = CheckStrokeAnimationDuration
- delay = BoxAnimationDuration
+ BoxOpacityFraction using tween {
+ duration = BoxInDuration
+ }
+ CheckOpacityFraction using tween {
+ duration = BoxInDuration
+ }
+ CheckDrawFraction using tween {
+ duration = CheckAnimationDuration
}
}
-private fun TransitionSpec<ToggleableState>.boxTransitionToUnchecked() {
- BoxColorProp using snap()
- InnerRadiusFractionProp using tween {
- duration = BoxAnimationDuration
- delay = CheckStrokeAnimationDuration
+private fun TransitionSpec<ToggleableState>.checkboxTransitionToUnchecked() {
+ BoxBorderColor using tween {
+ duration = BoxOutDuration
}
- CheckFractionProp using tween {
- duration = CheckStrokeAnimationDuration
+ BoxOpacityFraction using tween {
+ duration = BoxOutDuration
+ }
+ CheckOpacityFraction using tween {
+ duration = BoxOutDuration
+ }
+ // TODO: emulate delayed snap and replace when actual API is available b/158189074
+ CheckDrawFraction using keyframes {
+ duration = BoxOutDuration
+ 1f at 0
+ 1f at BoxOutDuration - 1
+ 0f at BoxOutDuration
+ }
+ CheckCenterGravitationShiftFraction using tween {
+ duration = 1
+ delay = BoxOutDuration - 1
}
}
-private val CheckboxDefaultPadding = Modifier.padding(2.dp)
+private val CheckboxDefaultPadding = 2.dp
private val CheckboxSize = 20.dp
private val StrokeWidth = 2.dp
private val RadiusSize = 2.dp
private val UncheckedBoxOpacity = 0.6f
-private val CheckStrokeDefaultColor = Color.White