How to reduce recompositions when animating text color?

278 views Asked by At

Current code

@Composable
fun TextDemo() {
    var selectedIndex by remember {
        mutableIntStateOf(0)
    }
    Row {
        TabText(
            text = "First",
            isSelected = selectedIndex == 0,
            onClick = {
                selectedIndex = 0
            },
        )
        TabText(
            text = "Second",
            isSelected = selectedIndex == 1,
            onClick = {
                selectedIndex = 1
            },
        )
    }
}

@Composable
fun TabText(
    text: String,
    isSelected: Boolean,
    onClick: () -> Unit,
) {
    val tabTextColor by animateColorAsState(
        targetValue = if (isSelected) {
            Color.Red
        } else {
            Color.Black
        },
        animationSpec = tween(
            easing = LinearEasing,
        ),
        label = "tab_text_color",
    )

    Text(
        modifier = Modifier
            .padding(8.dp)
            .clickable {
                onClick()
            },
        text = text,
        color = tabTextColor,
    )
}

UI for reference Two Text in a Row

UI Screenshot

Layout Inspector recompositions

Screenshot of Layout inspector

Question

How to reduce the recompositions when text color changes?

For properties like alpha, transition, etc, It is possible to avoid recompositions when animating using Modifier.graphicsLayer {}

The same code with alpha animation instead of color is recomposed only once per selection change.

Layout inspector screenshot

Code

@Composable
fun TabText(
    text: String,
    isSelected: Boolean,
    onClick: () -> Unit,
) {
    val alphaValue by animateFloatAsState(
        targetValue = if (isSelected) {
            1F
        } else {
            0.5F
        },
        animationSpec = tween(
            easing = LinearEasing,
        ),
        label = "tab_text_color",
    )

    Text(
        modifier = Modifier
            .graphicsLayer {
                alpha = alphaValue
            }
            .padding(8.dp)
            .clickable {
                onClick()
            },
        text = text,
    )
}
2

There are 2 answers

6
Thracian On BEST ANSWER

First of all when you log recomposition that reads a State it should better be done inside SideEffect otherwise it's possible to get false positives, because logging itself also counts as a State read.

To have one recomposition for text color change you can use Canvas or any draw Modifier inside Tab and call only draw phase while Color changes using TextMeasurer and drawText function of DrawScope.

Second option is to use BlendModes with Modifier.drawContent{} to change color with one recompostion as

@Preview
@Composable
private fun TextAnimationTest() {

    var isSelected by remember {
        mutableStateOf(false)
    }

    SideEffect {
        println("Recomposing...")
    }

    val tabTextColor by animateColorAsState(
        targetValue = if (isSelected) {
            Color.Red
        } else {
            Color.Black
        },
        animationSpec = tween(
            easing = LinearEasing,
        ),
        label = "tab_text_color",
    )


  Column {
      Button(
          onClick = {
              isSelected = isSelected.not()
          }
      ) {
          Text("Selected: $isSelected")
      }

      Text("Some Text", modifier = Modifier
          .graphicsLayer {
              compositingStrategy = CompositingStrategy.Offscreen
          }
          .drawWithContent {
              drawContent()

              drawRect(
                  color = tabTextColor,
                  blendMode = BlendMode.SrcIn
              )
          }
      )
  }
}
0
Abhimanyu On

Given I am able to replace my Text usage with BasicText for my use-case, I can use the ColorProvider provided by BasicText.

Source.


This may not be possible in all scenarios and for such use cases, this solution by Thracian can be used.


Created an issue to provide support for color provider in Text as well. https://issuetracker.google.com/issues/305542850