Blog Infos
Author
Published
Topics
, , , ,
Published

Recently, Google announced the next iteration of its Material Design language – The Material 3 Expressive.

As Google explains, they want to bring personality and emotion to otherwise boring app UIs, helping users feel more connected to their smartphones — which, according to Google, are no longer just simple tools, but extensions of ourselves.

Image from Dribbble (https://dribbble.com/shots/22986605-Emotions)

With the new version of Material 3 Expressive, Google introduces a new motion-physics system designed to make app UIs feel more alive, fluid, and natural.

Image from material.io

As I explore the capabilities of Material 3 Expressive, one of the first components I’m implementing is button toggles. With the new expressive motion and visual system, I’m excited to see how these toggles can feel more dynamic, fluid, and emotionally engaging — moving beyond static UI elements to something that truly responds to user interaction.

Let’s start!

First, Let’s create 3 buttons as in the Google’s preview, but let’s do it step by step.

var selectedIndex by remember { mutableIntStateOf(-1) }
val selectedColor = Color(0xFF554F6E)
val unselectedColor = Color(0xFFEAE5FF)
val icons = listOf(Icons.Outlined.Alarm, Icons.Outlined.LinkOff, Icons.Outlined.Wifi)
val iconSize = 60.dp

Row(Modifier.padding(horizontal = 8.dp),) {
  icons.forEachIndexed { index, icon ->
    FloatingActionButton(
      modifier = Modifier
          .padding(4.dp)
          .width(
            when (index) {
              1 -> iconSize / 1.5f
              2 -> iconSize * 1.5f
              else -> iconSize
            }
          )
          .height(iconSize),
      elevation = FloatingActionButtonDefaults.elevation(
        defaultElevation = 0.dp,
        pressedElevation = 0.dp
       ),
       containerColor = if (selectedIndex == index) {
         selectedColor
       } else {
         unselectedColor
       },
       contentColor = if (selectedIndex == index) {
         unselectedColor
       } else {
         selectedColor
       },
       onClick = { selectedIndex = index },
     ) {
        Icon(
          imageVector = icon,
          contentDescription = null,
        )
    }
  }
}

This is what we’ll get as a result. Let’s make this match the Google’s example with changing it’s sizes by just using regular Floating Action Button size.

Image from code example above

We have all we need now and there’s only one thing which is missing. Let’s add some animations and make it alive 🪄.

@Composable
fun ExpressiveButtonAnimation() {
var selectedIndex by remember { mutableIntStateOf(-1) }
val checkedColor = Color(0xFF554F6E)
val uncheckedColor = Color(0xFFEAE5FF)
val icons = listOf(Icons.Outlined.Alarm, Icons.Outlined.LinkOff, Icons.Outlined.Wifi)
var weights = remember { mutableStateListOf(0.85f, 0.65f, 1.5f) }
Box(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFFDAD2FF)),
contentAlignment = Alignment.Center,
) {
Row(
modifier = Modifier
.fillMaxWidth(0.6f)
.padding(horizontal = 8.dp)
) {
icons.forEachIndexed { index, icon ->
ExpressiveFloatingActionButton(
icon = icon,
itemWeight = weights[index],
checked = selectedIndex == index,
checkedColor = checkedColor,
uncheckedColor = uncheckedColor,
onClick = {
selectedIndex = if (index == selectedIndex) {
-1
} else {
index
}
}
)
}
}
}
}
@Composable
fun RowScope.ExpressiveFloatingActionButton(
icon: ImageVector,
itemWeight: Float,
checked: Boolean,
checkedColor: Color,
uncheckedColor: Color,
onClick: () -> Unit,
) {
var shapeSelected by remember { mutableStateOf(false) }
val animatedRadius by animateDpAsState(
targetValue = if (shapeSelected) 6.dp else 16.dp,
label = "animatedRadius"
)
val animatedWeight by animateFloatAsState(
targetValue = if (shapeSelected) 0.25f else 0f
)
val iconSize = 70.dp
IconButton(
modifier = Modifier
.padding(4.dp)
.weight(itemWeight + animatedWeight)
.height(iconSize)
.pointerInput(Unit) {
awaitPointerEventScope {
while (true) {
val event = awaitPointerEvent(PointerEventPass.Main)
event.changes.forEach { pointerInputChange ->
shapeSelected = pointerInputChange.pressed
}
}
}
},
colors = IconButtonDefaults.filledIconButtonColors(
containerColor = if (shapeSelected || checked) checkedColor else uncheckedColor,
contentColor = if (shapeSelected || checked) uncheckedColor else checkedColor,
),
shape = MaterialTheme.shapes.large.copy(CornerSize(animatedRadius)),
onClick = onClick,
) {
Icon(
imageVector = icon,
contentDescription = null,
)
}
}
view raw expressive.kt hosted with ❤ by GitHub

I have changed the fixed item width with weights, so the UI will have same proportions on all screen sizes.

And here is the final result:

Image from code example above

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

,

Compose Beyond Material Design

In the implementation of Jetpack compose at our organization, one of the issues that popped up was how to implement a Design system that is not adapted from material design but true to what our…
Watch Video

Compose Beyond Material Design

Frank Tamre

Compose Beyond Material Design

Frank Tamre

Compose Beyond Material Design

Frank Tamre

Jobs

I decided to write all of this myself, so you’re free to use and customize it however you like.

If you’d rather not deal with the custom animations manually, you can use ButtonGroup, which already supports all the new animations. However, keep in mind that it’s currently in alpha — meaning the API is still unstable and subject to change in future releases — and its functionality is still quite limited.

You can find more about M3 Expressive here.

If you liked this article, you can check my other articles and some of my open source projects.

Happy coding!

This article was previously published on proandroiddev.com.

Menu