Search code examples
androidandroid-jetpack-composeandroid-animationsearchbar

How to animate SearchBox placeholder text using Android Jetpack Compose?


I want to animate the placeholder of my SearchBar Composable just like the Google Play Store searchbar placeholder. I tried to do it, but it does not seem to be as subtle as the Google Play Store.

This is what I want to achieve,

enter image description here

This is what I have done so far,

enter image description here

Here is my code,

var showInitialPlaceholder by remember { mutableStateOf(true) }

LaunchedEffect(Unit) {
    delay(1000)
    showInitialPlaceholder = false
}

SearchBar(
    ...
    placeholder = {
        Crossfade(targetState = showInitialPlaceholder) { isInitial ->
            if (isInitial) {
                Text("Google Play")
            } else {
                Text("Search...")
            }
        }
    }
    ...
)

Solution

  • I've crated simple searchBox example. I used two AnimatedVisibility blocks for the placeholders because in the desired gif you can see second placeholder appearing after the initial one is hidden, so Crossfade is not the best option.

    Basically there is:

    1. Initial slideIn animation of SearchBox
    2. fadeOut animation of initial placeholder
    3. fadeIn animation of final placeholder

    Unfortunately i don't have good eyes when it comes to these little details so i debugged it with Window animation scale turned on and implement it too. Maybe try to play with animation durations, delays and choose desired easing. Try this and see how it will work for you, hope it helps at least a little.

    @Composable
    fun AnimatedSearchBox(modifier: Modifier = Modifier) {
        val context = LocalContext.current
        //scale for debugging with window scale option
        val animScale = remember {
            Settings.Global.getFloat(
                context.contentResolver,
                Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f
            )
        }
    
        //Searchbar slide animation
        var isSearchBarVisible by remember { mutableStateOf(false) }
        //Initial "Google Play" visibility
        var isInitialVisible by remember { mutableStateOf(true) }
        //Placeholder visibility
        var isSecondPhaseVisible by remember { mutableStateOf(false) }
    
        //Searchbar properties
        var isActive by remember { mutableStateOf(false) }
        var query by remember { mutableStateOf("") }
    
        LaunchedEffect(Unit) {
            //Initial delay for searchbar slide animation
            delay((animScale * 1000).toLong())
            isSearchBarVisible = true
    
            //Delay before hiding initial placeholder
            delay((1000 * animScale.toLong()))
            isInitialVisible = false
    
            //Delay before showing placeholder, must be lower than initial placeholder animation duration
            //To make it more smooth
            delay(timeMillis = (250 * animScale).toLong())
            isSecondPhaseVisible = true
        }
    
        AnimatedVisibility(
            visible = isSearchBarVisible,
            enter = fadeIn() + slideInVertically { it },
        ) {
            SearchBar(
                modifier = modifier,
                query = query,
                onQueryChange = { query = it },
                active = isActive,
                placeholder = {
                    AnimatedVisibility(
                        visible = isInitialVisible,
                        exit = fadeOut(
                            animationSpec = tween(
                                durationMillis = 400,
                                easing = EaseInOutExpo
                            )
                        )
                    ) {
                        Text("Google Play")
                    }
    
                    AnimatedVisibility(
                        visible = isSecondPhaseVisible,
                        enter = fadeIn(
                            animationSpec = tween(
                                durationMillis = 200,
                                easing = EaseInOutExpo
                            )
                        )
                    ) {
                        Text("Search...")
                    }
                },
                onActiveChange = {
                    isActive = it
                },
                onSearch = {},
                leadingIcon = {
                    Icon(
                        painter = painterResource(id = R.drawable.outline_search_24),
                        contentDescription = null
                    )
                },
                trailingIcon = {
                    Icon(
                        painter = painterResource(id = R.drawable.baseline_checklist_rtl_24),
                        contentDescription = null
                    )
                },
                content = {
    
                }
            )
        }
    }