How to use `AnimatedContent` in Jetpack Compose for `TextField`?

435 views Asked by At

I want to use AnimatedContent to switch between TextField and Text components. I am using text != null as targetState, such that Text is shown when targetState is null, and TextField otherwise.

Following is the implementation:

@OptIn(ExperimentalAnimationApi::class, ExperimentalMaterial3Api::class)
@Preview
@Composable
private fun ViewTest() {
    val durationMillis = 2000
    var text by remember { mutableStateOf<String?>(null) }

    Surface {
        Column(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally,
        ) {
            AnimatedContent(
                targetState = text != null,
                label = "",
                transitionSpec = {
                    ContentTransform(
                        targetContentEnter = fadeIn(animationSpec = tween(durationMillis)),
                        initialContentExit = fadeOut(animationSpec = tween(durationMillis)),
                        sizeTransform = SizeTransform(sizeAnimationSpec = { _, _ -> tween(durationMillis) })
                    )
                }
            ) { show ->
                if (show)
                    TextField(
                        value = text ?: "",
                        onValueChange = { text = it },
                        placeholder = { Text(text = "Search") }
                    )
                else
                    Text(text = "Title")
            }

            AnimatedContent(targetState = text != null, label = "") { show ->
                if (show)
                    IconButton(onClick = { text = null }) {
                        Icon(Icons.Rounded.Clear, contentDescription = null)
                    }
                else
                    IconButton(onClick = { text = "" }) {
                        Icon(Icons.Rounded.Search, contentDescription = null)
                    }
            }
        }
    }
}

However, when targetState is non-empty, and is set to null from the IconButton, following is the animation:

animation

I want that the intermediate placeholder should not be shown, but I am not able to do that. If I use targetState = text, then it doesn't work at all.

How can I transition directly without showing the placeholder text "Search"?

2

There are 2 answers

0
Name On
@OptIn(ExperimentalAnimationApi::class, ExperimentalMaterial3Api::class)
@Preview
@Composable
private fun ViewTest() {
    val durationMillis = 2000
    var text by remember { mutableStateOf<String?>(null) }
    var placeHolderAlpha by remember {
        mutableStateOf(1f)
    }

    Surface {
        Column(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally,
        ) {
            AnimatedContent(
                targetState = text != null,
                label = "",
                transitionSpec = {
                    ContentTransform(
                        targetContentEnter = fadeIn(animationSpec = tween(durationMillis)),
                        initialContentExit = fadeOut(animationSpec = tween(durationMillis)),
                        sizeTransform = SizeTransform(sizeAnimationSpec = { _, _ ->
                            tween(
                                durationMillis
                            )
                        })
                    )
                }
            ) { show ->
                if (show)
                    TextField(
                        value = text ?: "",
                        onValueChange = { text = it },
                        placeholder = {
                            Text(
                                modifier = Modifier.alpha(placeHolderAlpha),
                                text = "Search"
                            )
                        }
                    )
                else
                    Text(text = "Title")
            }

            AnimatedContent(targetState = text != null, label = "") { show ->
                if (show)
                    IconButton(onClick = {
                        text = null
                        placeHolderAlpha = 0f
                    }) {
                        Icon(Icons.Rounded.Clear, contentDescription = null)
                    }
                else
                    IconButton(onClick = {
                        text = ""
                        placeHolderAlpha = 1f
                    }) {
                        Icon(Icons.Rounded.Search, contentDescription = null)
                    }
            }
        }
    }
}

Try this.

0
Giedrius Kairys On

I would suggest not using text as a way to control show/hide state. Instead, use a separate variable for controlling it, like isSearchOpen:

var isSearchOpen by remember { mutableStateOf(false) }

So then in places where you check for targetState = text != null you can simply use targetState = isSearchOpen. And on button clicks you change isSearchOpen to true or false.

In this way, your component UI state is independent of the value and could be more easily controlled. For example, if you still want to show the text while hiding animation is happening, but reset the text on "Clear" button click (so that search results could be updated on click), you would do something like this:

@OptIn(ExperimentalAnimationApi::class, ExperimentalMaterial3Api::class)
@Preview
@Composable
private fun ViewTest() {
    val durationMillis = 2000
    var isSearchOpen by remember { mutableStateOf(false) }
    var textForAnimation by remember { mutableStateOf("") }
    var text by remember { mutableStateOf("") }.also {
        if (isSearchOpen)
            textForAnimation = it.value
    }

    Surface {
        Column(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally,
        ) {
            AnimatedContent(
                targetState = isSearchOpen,
                label = "",
                transitionSpec = {
                    ContentTransform(
                        targetContentEnter = fadeIn(animationSpec = tween(durationMillis)),
                        initialContentExit = fadeOut(animationSpec = tween(durationMillis)),
                        sizeTransform = SizeTransform(sizeAnimationSpec = { _, _ -> tween(durationMillis) })
                    )
                }
            ) { show ->
                if (show)
                    TextField(
                        value = if (isSearchOpen) text else textForAnimation,
                        onValueChange = { text = it },
                        placeholder = { Text(text = "Search") }
                    )
                else
                    Text(text = "Title")
            }

            AnimatedContent(targetState = isSearchOpen, label = "") { show ->
                if (show)
                    IconButton(onClick = {
                        isSearchOpen = false
                        text = ""
                    }) {
                        Icon(Icons.Rounded.Clear, contentDescription = null)
                    }
                else
                    IconButton(onClick = { isSearchOpen = true }) {
                        Icon(Icons.Rounded.Search, contentDescription = null)
                    }
            }
        }
    }
}

Or if you only need to not show the placeholder then simply:

placeholder = { if (isSearchOpen) Text(text = "Search") }