diff --git a/crates/figma_import/src/variable_utils.rs b/crates/figma_import/src/variable_utils.rs index fd11bacf1..ade9c0e0d 100644 --- a/crates/figma_import/src/variable_utils.rs +++ b/crates/figma_import/src/variable_utils.rs @@ -114,10 +114,10 @@ pub(crate) fn create_variable(v: &figma_schema::Variable) -> Variable { create_variable_helper(VariableType::Number, common, values_by_mode) } figma_schema::Variable::String { common, values_by_mode } => { - create_variable_helper(VariableType::Bool, common, values_by_mode) + create_variable_helper(VariableType::Text, common, values_by_mode) } figma_schema::Variable::Color { common, values_by_mode } => { - create_variable_helper(VariableType::Text, common, values_by_mode) + create_variable_helper(VariableType::Color, common, values_by_mode) } } } diff --git a/designcompose/src/main/java/com/android/designcompose/VariableManager.kt b/designcompose/src/main/java/com/android/designcompose/VariableManager.kt index 4e35b18bb..6dbc7d015 100644 --- a/designcompose/src/main/java/com/android/designcompose/VariableManager.kt +++ b/designcompose/src/main/java/com/android/designcompose/VariableManager.kt @@ -16,6 +16,9 @@ package com.android.designcompose +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween import androidx.compose.material3.ColorScheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Shapes @@ -30,6 +33,7 @@ import com.android.designcompose.serdegen.ColorOrVar import com.android.designcompose.serdegen.NumOrVar import com.android.designcompose.serdegen.Variable import com.android.designcompose.serdegen.VariableMap +import com.android.designcompose.serdegen.VariableType import com.android.designcompose.serdegen.VariableValue import java.util.Optional @@ -51,6 +55,14 @@ private val LocalVariableModeValuesOverride = compositionLocalOf { hashMapOf() } +// A table of variable values that gets updated when themes or modes change +data class VariableValueTable( + // Map of var ID -> color + val colors: HashMap, + // Map of var ID -> number + val numbers: HashMap, +) + // Current boolean value to represent whether we should attempt to override the design specified // theme with the MaterialTheme private val LocalVariableUseMaterialTheme = compositionLocalOf { false } @@ -79,6 +91,7 @@ object LocalVariableState { internal class VariableState( val varCollection: VarCollectionName? = null, val varModeValues: VariableModeValues? = null, + var varColorValues: VariableValueTable? = null, val useMaterialTheme: Boolean = false, val materialColorScheme: ColorScheme? = null, val materialTypography: Typography? = null, @@ -87,14 +100,22 @@ internal class VariableState( companion object { @Composable fun create(updatedModeValues: VariableModeValues? = null): VariableState { - return VariableState( - varCollection = LocalVariableState.collection, - varModeValues = updatedModeValues ?: LocalVariableState.modeValues, - useMaterialTheme = LocalVariableState.useMaterialTheme, - materialColorScheme = MaterialTheme.colorScheme, - materialTypography = MaterialTheme.typography, - materialShapes = MaterialTheme.shapes, - ) + // Create a VariableState without the cached varColorValues. Use it to calculate the + // variable values by calling updateVariableValues, then set the variable values back + // into the VariableState object. + val varState = + VariableState( + varCollection = LocalVariableState.collection, + varModeValues = updatedModeValues ?: LocalVariableState.modeValues, + varColorValues = null, + useMaterialTheme = LocalVariableState.useMaterialTheme, + materialColorScheme = MaterialTheme.colorScheme, + materialTypography = MaterialTheme.typography, + materialShapes = MaterialTheme.shapes, + ) + val varColors = VariableManager.updateVariableValues(varState) + varState.varColorValues = varColors + return varState } } } @@ -162,6 +183,28 @@ internal object VariableManager { currentDocId = docId } + // Update all variable values with smooth transitions if they are changing and save their + // values into hash tables + @Composable + internal fun updateVariableValues(tempState: VariableState): VariableValueTable { + val newColors = HashMap() + val newNumbers = HashMap() + varMap.variables.forEach { (varId, v) -> + when (v.var_type) { + is VariableType.Color -> { + val color = getColor(varId, null, tempState) + color?.let { newColors[varId] = animateColor(targetValue = it) } + } + is VariableType.Number -> { + val num = getNumber(varId, null, tempState) + num?.let { newNumbers[varId] = animateFloat(targetValue = it) } + } + } + } + + return VariableValueTable(newColors, newNumbers) + } + // If the developer has not explicitly set variable override values, check to see if any // variable modes have been set on this node. If so, return the modeValues set. @Composable @@ -206,6 +249,12 @@ internal object VariableManager { // Given a variable ID, return the color associated with it internal fun getColor(varId: String, fallback: Color?, variableState: VariableState): Color? { + // If cached in varColorValues for animation updates, return it + val color = variableState.varColorValues?.colors?.get(varId) + color?.let { + return it + } + // Resolve varId into a variable. If a different collection has been set, this will return // a variable of the same name from the override collection. val variable = resolveVariable(varId, variableState) @@ -218,6 +267,12 @@ internal object VariableManager { // Given a variable ID, return the number associated with it internal fun getNumber(varId: String, fallback: Float?, variableState: VariableState): Float? { + // If cached in varColorValues for animation updates, return it + val num = variableState.varColorValues?.numbers?.get(varId) + num?.let { + return it + } + val variable = resolveVariable(varId, variableState) variable?.let { v -> return v.getNumber(varMap, variableState) @@ -330,3 +385,13 @@ internal fun ColorOrVar.getValue(variableState: VariableState): Color? { else -> null } } + +@Composable +private fun animateColor(targetValue: Color) = + animateColorAsState(targetValue = targetValue, animationSpec = tween(durationMillis = 1000)) + .value + +@Composable +private fun animateFloat(targetValue: Float) = + animateFloatAsState(targetValue = targetValue, animationSpec = tween(durationMillis = 1000)) + .value diff --git a/integration-tests/validation/src/main/java/com/android/designcompose/testapp/validation/examples/VariableModesTest.kt b/integration-tests/validation/src/main/java/com/android/designcompose/testapp/validation/examples/VariableModesTest.kt index d34eeba27..57d834248 100644 --- a/integration-tests/validation/src/main/java/com/android/designcompose/testapp/validation/examples/VariableModesTest.kt +++ b/integration-tests/validation/src/main/java/com/android/designcompose/testapp/validation/examples/VariableModesTest.kt @@ -35,6 +35,7 @@ import com.android.designcompose.DesignVariableModeValues import com.android.designcompose.annotation.Design import com.android.designcompose.annotation.DesignComponent import com.android.designcompose.annotation.DesignDoc +import com.android.designcompose.testapp.validation.TestButton enum class LightDarkMode { Default, @@ -120,108 +121,54 @@ fun VariableModesTest() { Column(Modifier.offset(10.dp, 800.dp)) { Row(verticalAlignment = Alignment.CenterVertically) { Text("Root Theme", fontSize = 30.sp, color = Color.Black) - com.android.designcompose.testapp.validation.TestButton( - "None", - "RootThemeNone", - theme.value == null - ) { + TestButton("None", "RootThemeNone", theme.value == null) { theme.value = null useMaterialOverride.value = false } - com.android.designcompose.testapp.validation.TestButton( - "Material (Figma)", - "MaterialFigma", - theme.value == Theme.Material - ) { + TestButton("Material (Figma)", "MaterialFigma", theme.value == Theme.Material) { theme.value = Theme.Material useMaterialOverride.value = false } - com.android.designcompose.testapp.validation.TestButton( - "MyTheme (Figma)", - "MyThemeFigma", - theme.value == Theme.MyTheme - ) { + TestButton("MyTheme (Figma)", "MyThemeFigma", theme.value == Theme.MyTheme) { theme.value = Theme.MyTheme useMaterialOverride.value = false } - com.android.designcompose.testapp.validation.TestButton( - "Material (Device)", - "MaterialDevice", - useMaterialOverride.value - ) { + TestButton("Material (Device)", "MaterialDevice", useMaterialOverride.value) { theme.value = null useMaterialOverride.value = true } } Row(verticalAlignment = Alignment.CenterVertically) { Text("Root Mode", fontSize = 30.sp, color = Color.Black) - com.android.designcompose.testapp.validation.TestButton( - "Default", - "RootModeDefault", - mode.value == LightDarkMode.Default - ) { + TestButton("Default", "RootModeDefault", mode.value == LightDarkMode.Default) { mode.value = LightDarkMode.Default } - com.android.designcompose.testapp.validation.TestButton( - "Light", - "RootModeLight", - mode.value == LightDarkMode.Light - ) { + TestButton("Light", "RootModeLight", mode.value == LightDarkMode.Light) { mode.value = LightDarkMode.Light } - com.android.designcompose.testapp.validation.TestButton( - "Dark", - "RootModeDark", - mode.value == LightDarkMode.Dark - ) { + TestButton("Dark", "RootModeDark", mode.value == LightDarkMode.Dark) { mode.value = LightDarkMode.Dark } } Row(verticalAlignment = Alignment.CenterVertically) { Text("Top Right Theme", fontSize = 30.sp, color = Color.Black) - com.android.designcompose.testapp.validation.TestButton( - "None", - "TopRightNone", - trTheme.value == null - ) { - trTheme.value = null - } - com.android.designcompose.testapp.validation.TestButton( - "Material", - "TopRightMaterial", - trTheme.value == Theme.Material - ) { + TestButton("None", "TopRightNone", trTheme.value == null) { trTheme.value = null } + TestButton("Material", "TopRightMaterial", trTheme.value == Theme.Material) { trTheme.value = Theme.Material } - com.android.designcompose.testapp.validation.TestButton( - "MyTheme", - "TopRightMyTheme", - trTheme.value == Theme.MyTheme - ) { + TestButton("MyTheme", "TopRightMyTheme", trTheme.value == Theme.MyTheme) { trTheme.value = Theme.MyTheme } } Row(verticalAlignment = Alignment.CenterVertically) { Text("Top Right Mode", fontSize = 30.sp, color = Color.Black) - com.android.designcompose.testapp.validation.TestButton( - "Default", - "TopRightDefault", - trMode.value == LightDarkMode.Default - ) { + TestButton("Default", "TopRightDefault", trMode.value == LightDarkMode.Default) { trMode.value = LightDarkMode.Default } - com.android.designcompose.testapp.validation.TestButton( - "Light", - "TopRightLight", - trMode.value == LightDarkMode.Light - ) { + TestButton("Light", "TopRightLight", trMode.value == LightDarkMode.Light) { trMode.value = LightDarkMode.Light } - com.android.designcompose.testapp.validation.TestButton( - "Dark", - "TopRightDark", - trMode.value == LightDarkMode.Dark - ) { + TestButton("Dark", "TopRightDark", trMode.value == LightDarkMode.Dark) { trMode.value = LightDarkMode.Dark } }