placeholderShimmer

Functions summary

Modifier
@ExperimentalWearMaterialApi
@Composable
Modifier.placeholderShimmer(
    placeholderState: PlaceholderState,
    shape: Shape,
    color: Color
)

Modifier to draw a placeholder shimmer over a component.

Functions

Modifier.placeholderShimmer

@ExperimentalWearMaterialApi
@Composable
fun Modifier.placeholderShimmer(
    placeholderState: PlaceholderState,
    shape: Shape = MaterialTheme.shapes.small,
    color: Color = MaterialTheme.colors.onSurface
): Modifier

Modifier to draw a placeholder shimmer over a component. The placeholder shimmer is a 45 degree gradient from Top|Left of the screen to Bottom|Right. The shimmer is coordinated via the animation frame clock which orchestrates the shimmer so that every component will shimmer as the gradient progresses across the screen.

Example of a Chip with icon and a label that put placeholders over individual content slots and then draws a placeholder shimmer over the result:

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.wear.compose.material.Chip
import androidx.wear.compose.material.ChipDefaults
import androidx.wear.compose.material.Icon
import androidx.wear.compose.material.PlaceholderDefaults
import androidx.wear.compose.material.Text
import androidx.wear.compose.material.placeholder
import androidx.wear.compose.material.placeholderShimmer
import androidx.wear.compose.material.rememberPlaceholderState

var labelText by remember { mutableStateOf("") }
var iconResource: Int? by remember { mutableStateOf(null) }
val chipPlaceholderState = rememberPlaceholderState {
    labelText.isNotEmpty() && iconResource != null
}

Chip(
    onClick = { /* Do something */ },
    enabled = true,
    label = {
        Text(
            text = labelText,
            maxLines = 2,
            overflow = TextOverflow.Ellipsis,
            modifier = Modifier.fillMaxWidth().placeholder(chipPlaceholderState),
        )
    },
    icon = {
        Box(modifier = Modifier.size(ChipDefaults.IconSize).placeholder(chipPlaceholderState)) {
            if (iconResource != null) {
                Icon(
                    painter = painterResource(id = R.drawable.ic_airplanemode_active_24px),
                    contentDescription = "airplane",
                    modifier =
                        Modifier.wrapContentSize(align = Alignment.Center)
                            .size(ChipDefaults.IconSize)
                            .fillMaxSize(),
                )
            }
        }
    },
    colors =
        PlaceholderDefaults.placeholderChipColors(
            originalChipColors = ChipDefaults.primaryChipColors(),
            placeholderState = chipPlaceholderState,
        ),
    modifier = Modifier.fillMaxWidth().placeholderShimmer(chipPlaceholderState),
)
// Simulate content loading completing in stages
LaunchedEffect(Unit) {
    delay(2000)
    iconResource = R.drawable.ic_airplanemode_active_24px
    delay(1000)
    labelText = "A label"
}
if (!chipPlaceholderState.isShowContent) {
    LaunchedEffect(chipPlaceholderState) { chipPlaceholderState.startPlaceholderAnimation() }
}

Example of a Chip with icon and a primary and secondary labels that draws another Chip over the top of it when waiting for placeholder data to load and then draws a placeholder shimmer over the top:

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.wear.compose.material.Chip
import androidx.wear.compose.material.ChipDefaults
import androidx.wear.compose.material.Icon
import androidx.wear.compose.material.PlaceholderDefaults
import androidx.wear.compose.material.Text
import androidx.wear.compose.material.placeholder
import androidx.wear.compose.material.placeholderShimmer
import androidx.wear.compose.material.rememberPlaceholderState

var labelText by remember { mutableStateOf("") }
var secondaryLabelText by remember { mutableStateOf("") }
var iconResource: Int? by remember { mutableStateOf(null) }

val chipPlaceholderState = rememberPlaceholderState {
    labelText.isNotEmpty() && secondaryLabelText.isNotEmpty() && iconResource != null
}
Box {
    if (chipPlaceholderState.isShowContent || chipPlaceholderState.isWipeOff) {
        Chip(
            onClick = { /* Do something */ },
            enabled = true,
            label = {
                Text(
                    text = labelText,
                    maxLines = 1,
                    overflow = TextOverflow.Ellipsis,
                    modifier = Modifier.fillMaxWidth(),
                )
            },
            secondaryLabel = {
                Text(
                    text = secondaryLabelText,
                    maxLines = 1,
                    overflow = TextOverflow.Ellipsis,
                    modifier = Modifier.fillMaxWidth(),
                )
            },
            icon = {
                if (iconResource != null) {
                    Icon(
                        painter = painterResource(id = R.drawable.ic_airplanemode_active_24px),
                        contentDescription = "airplane",
                        modifier =
                            Modifier.wrapContentSize(align = Alignment.Center)
                                .size(ChipDefaults.IconSize),
                    )
                }
            },
            colors = ChipDefaults.gradientBackgroundChipColors(),
            modifier = Modifier.fillMaxWidth(),
        )
    }
    if (!chipPlaceholderState.isShowContent) {
        Chip(
            onClick = { /* Do something */ },
            enabled = true,
            label = {
                Box(
                    modifier =
                        Modifier.fillMaxWidth()
                            .height(16.dp)
                            .padding(top = 1.dp, bottom = 1.dp)
                            .placeholder(placeholderState = chipPlaceholderState)
                )
            },
            secondaryLabel = {
                Box(
                    modifier =
                        Modifier.fillMaxWidth()
                            .height(16.dp)
                            .padding(top = 1.dp, bottom = 1.dp)
                            .placeholder(placeholderState = chipPlaceholderState)
                )
            },
            icon = {
                Box(
                    modifier =
                        Modifier.size(ChipDefaults.IconSize).placeholder(chipPlaceholderState)
                )
                // Simulate the icon becoming ready after a period of time
                LaunchedEffect(Unit) {
                    delay(2000)
                    iconResource = R.drawable.ic_airplanemode_active_24px
                }
            },
            colors =
                PlaceholderDefaults.placeholderChipColors(
                    placeholderState = chipPlaceholderState
                ),
            modifier = Modifier.fillMaxWidth().placeholderShimmer(chipPlaceholderState),
        )
    }
}
// Simulate data being loaded after a delay
LaunchedEffect(Unit) {
    delay(2500)
    secondaryLabelText = "A secondary label"
    delay(500)
    labelText = "A label"
}
if (!chipPlaceholderState.isShowContent) {
    LaunchedEffect(chipPlaceholderState) { chipPlaceholderState.startPlaceholderAnimation() }
}

NOTE: The order of modifiers is important. If you are adding both Modifier.placeholder and Modifier.placeholderShimmer to the same composable then the shimmer must be before in the modifier chain. Example of Text composable with both placeholderShimmer and placeholder modifiers.

import androidx.compose.foundation.layout.width
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.wear.compose.material.Text
import androidx.wear.compose.material.placeholder
import androidx.wear.compose.material.placeholderShimmer
import androidx.wear.compose.material.rememberPlaceholderState

var labelText by remember { mutableStateOf("") }
val chipPlaceholderState = rememberPlaceholderState { labelText.isNotEmpty() }

Text(
    text = labelText,
    overflow = TextOverflow.Ellipsis,
    textAlign = TextAlign.Center,
    modifier =
        Modifier.width(90.dp)
            .placeholderShimmer(chipPlaceholderState)
            .placeholder(chipPlaceholderState),
)

// Simulate content loading
LaunchedEffect(Unit) {
    delay(3000)
    labelText = "A label"
}
if (!chipPlaceholderState.isShowContent) {
    LaunchedEffect(chipPlaceholderState) { chipPlaceholderState.startPlaceholderAnimation() }
}
Parameters
placeholderState: PlaceholderState

the current placeholder state that determine whether the placeholder shimmer should be shown.

shape: Shape = MaterialTheme.shapes.small

the shape of the component.

color: Color = MaterialTheme.colors.onSurface

the color to use in the shimmer.