androidandroid-jetpack-composejetpack-compose-animationandroid-jetpack-compose-animation

How to reduce recompositions when animating text color?


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,
    )
}

Solution

  • 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
                  )
              }
          )
      }
    }