Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support animation when variables change values #1462

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions crates/figma_import/src/variable_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 } => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to merge these changes anyway? Looks like a bug...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup that one is a bug. It's not breaking anything right now but I'll merge it in separately if this change doesn't end up going in.

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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -51,6 +55,14 @@ private val LocalVariableModeValuesOverride = compositionLocalOf<VariableModeVal
// Current set of variable modes to use, as retrieved from the design source
private val LocalVariableModeValuesDoc = compositionLocalOf<VariableModeValues> { 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<String, Color>,
// Map of var ID -> number
val numbers: HashMap<String, Float>,
)

// Current boolean value to represent whether we should attempt to override the design specified
// theme with the MaterialTheme
private val LocalVariableUseMaterialTheme = compositionLocalOf { false }
Expand Down Expand Up @@ -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,
Expand All @@ -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
}
}
}
Expand Down Expand Up @@ -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<String, Color>()
val newNumbers = HashMap<String, Float>()
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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
}
}
Expand Down
Loading