[go: nahoru, domu]

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