Build apps faster with our new App builder! Check it out →

placeholderShimmer

Android

Modifier in Wear Material 3 Compose

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.

Last updated:

Installation

dependencies {
   implementation("androidx.wear.compose:compose-material3:1.0.0-alpha27")
}

Overloads

@OptIn(ExperimentalWearFoundationApi::class)
@Composable
fun Modifier.placeholderShimmer(
    placeholderState: PlaceholderState,
    shape: Shape = PlaceholderDefaults.shape,
    color: Color = MaterialTheme.colorScheme.onSurface,
): Modifier

Parameters

namedescription
placeholderStatethe current placeholder state that determine whether the placeholder shimmer should be shown.
shapethe shape of the component.
colorthe color to use in the shimmer.

Code Examples

ButtonWithIconAndLabelAndPlaceholders

/**
 * This sample applies placeholders directly over the content that is waiting to be loaded. This
 * approach is suitable for situations where the developer is confident that the stadium shaped
 * placeholder will cover the content in the period between when it is loaded and when the
 * placeholder visual effects will have finished.
 */
@Composable
fun ButtonWithIconAndLabelAndPlaceholders() {
    var labelText by remember { mutableStateOf("") }
    var imageVector: ImageVector? by remember { mutableStateOf(null) }
    val buttonPlaceholderState = rememberPlaceholderState {
        labelText.isNotEmpty() && imageVector != null
    }

    Button(
        onClick = { /* Do something */ },
        enabled = true,
        label = {
            Text(
                text = labelText,
                maxLines = 2,
                overflow = TextOverflow.Ellipsis,
                modifier = Modifier.fillMaxWidth().placeholder(buttonPlaceholderState)
            )
        },
        icon = {
            Box(
                modifier =
                    Modifier.size(ButtonDefaults.IconSize).placeholder(buttonPlaceholderState)
            ) {
                if (imageVector != null) {
                    Icon(
                        imageVector = imageVector!!,
                        contentDescription = "Heart",
                        modifier =
                            Modifier.wrapContentSize(align = Alignment.Center)
                                .size(ButtonDefaults.IconSize)
                                .fillMaxSize(),
                    )
                }
            }
        },
        colors =
            PlaceholderDefaults.placeholderButtonColors(
                originalButtonColors = ButtonDefaults.buttonColors(),
                placeholderState = buttonPlaceholderState
            ),
        modifier = Modifier.fillMaxWidth().placeholderShimmer(buttonPlaceholderState)
    )
    // Simulate content loading completing in stages
    LaunchedEffect(Unit) {
        delay(2000)
        imageVector = Icons.Filled.Favorite
        delay(1000)
        labelText = "A label"
    }
    if (!buttonPlaceholderState.isShowContent) {
        LaunchedEffect(buttonPlaceholderState) {
            buttonPlaceholderState.startPlaceholderAnimation()
        }
    }
}

ButtonWithIconAndLabelsAndOverlaidPlaceholder

/**
 * This sample places a [Button] with placeholder effects applied to it on top of the [Button] that
 * contains the actual content.
 *
 * This approach is needed in situations where the developer wants a higher degree of control over
 * what is shown as a placeholder before the loaded content is revealed. This approach can be used
 * when there would otherwise be content becoming visible as it is loaded and before the
 * placeholders have been wiped-off to reveal the content underneath, e.g. If the content contains
 * left|top aligned Text that would be visible in the part of the content slot not covered by the
 * stadium placeholder shape.
 */
@Composable
fun ButtonWithIconAndLabelsAndOverlaidPlaceholder() {
    var labelText by remember { mutableStateOf("") }
    var secondaryLabelText by remember { mutableStateOf("") }
    var imageVector: ImageVector? by remember { mutableStateOf(null) }

    val buttonPlaceholderState = rememberPlaceholderState {
        labelText.isNotEmpty() && secondaryLabelText.isNotEmpty() && imageVector != null
    }
    Box {
        if (buttonPlaceholderState.isShowContent || buttonPlaceholderState.isWipeOff) {
            Button(
                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 (imageVector != null) {
                        Icon(
                            imageVector = imageVector!!,
                            contentDescription = "Heart",
                            modifier =
                                Modifier.wrapContentSize(align = Alignment.Center)
                                    .size(ButtonDefaults.IconSize)
                                    .fillMaxSize(),
                        )
                    }
                },
                colors = ButtonDefaults.filledTonalButtonColors(),
                modifier = Modifier.fillMaxWidth()
            )
        }
        if (!buttonPlaceholderState.isShowContent) {
            Button(
                onClick = { /* Do something */ },
                enabled = true,
                label = {
                    Box(
                        modifier =
                            Modifier.fillMaxWidth()
                                .height(16.dp)
                                .padding(top = 1.dp, bottom = 1.dp)
                                .placeholder(placeholderState = buttonPlaceholderState)
                    )
                },
                secondaryLabel = {
                    Box(
                        modifier =
                            Modifier.fillMaxWidth()
                                .height(16.dp)
                                .padding(top = 1.dp, bottom = 1.dp)
                                .placeholder(placeholderState = buttonPlaceholderState)
                    )
                },
                icon = {
                    Box(
                        modifier =
                            Modifier.size(ButtonDefaults.IconSize)
                                .placeholder(buttonPlaceholderState)
                    )
                    // Simulate the icon becoming ready after a period of time
                    LaunchedEffect(Unit) {
                        delay(2000)
                        imageVector = Icons.Filled.Favorite
                    }
                },
                colors =
                    PlaceholderDefaults.placeholderButtonColors(
                        placeholderState = buttonPlaceholderState
                    ),
                modifier = Modifier.fillMaxWidth().placeholderShimmer(buttonPlaceholderState)
            )
        }
    }
    // Simulate data being loaded after a delay
    LaunchedEffect(Unit) {
        delay(2500)
        secondaryLabelText = "A secondary label"
        delay(500)
        labelText = "A label"
    }
    if (!buttonPlaceholderState.isShowContent) {
        LaunchedEffect(buttonPlaceholderState) {
            buttonPlaceholderState.startPlaceholderAnimation()
        }
    }
}

TextPlaceholder

/**
 * This sample applies a placeholder and placeholderShimmer directly over a single composable.
 *
 * Note that the modifier ordering is important, the placeholderShimmer must be before the
 * placeholder in the modifier chain - otherwise the shimmer will be drawn underneath the
 * placeholder and will not be visible.
 */
@Composable
fun TextPlaceholder() {
    var labelText by remember { mutableStateOf("") }
    val buttonPlaceholderState = rememberPlaceholderState { labelText.isNotEmpty() }

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

    // Simulate content loading
    LaunchedEffect(Unit) {
        delay(3000)
        labelText = "A label"
    }
    if (!buttonPlaceholderState.isShowContent) {
        LaunchedEffect(buttonPlaceholderState) {
            buttonPlaceholderState.startPlaceholderAnimation()
        }
    }
}
by @alexstyl