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
name | description |
---|---|
placeholderState | determines whether the placeholder is visible and controls animation effects for the placeholder. |
shape | the shape to apply to the placeholder |
color | the 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()
}
}
}