Curious Head
Curious Head

Reputation: 100

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...")
            }
        }
    }
    ...
)

Upvotes: 1

Views: 370

Answers (1)

Miroslav Hýbler
Miroslav Hýbler

Reputation: 1275

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 = {

            }
        )
    }
}

Upvotes: 1

Related Questions