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

placeholder

Android

Modifier in Wear Material 3 Compose

Draws a placeholder shape over the top of a composable and animates a wipe off effect to remove the placeholder. Typically used whilst content is 'loading' and then 'revealed'.

Last updated:

Installation

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

Overloads

@Composable
fun Modifier.placeholder(
    placeholderState: PlaceholderState,
    shape: Shape = PlaceholderDefaults.shape,
    color: Color =
        MaterialTheme.colorScheme.onSurface
            .copy(alpha = 0.1f)
            .compositeOver(MaterialTheme.colorScheme.surfaceContainer)
): Modifier

Parameters

namedescription
placeholderStatedetermines whether the placeholder is visible and controls animation effects for the placeholder.
shapethe shape to apply to the placeholder
colorthe color of the placeholder.

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