Разработка
Выразительные анимации для всех
В новой версии Material 3 Expressive компания Google представила новую систему физики движений, призванную сделать пользовательский интерфейс приложений более живым, плавным и естественным.
Недавно Google анонсировала следующую итерацию своего языка Material Design — Material 3 Expressive.
Как объясняют в Google, они хотят привнести индивидуальность и эмоции в скучные пользовательские интерфейсы приложений, помогая пользователям чувствовать себя более связанными со своими смартфонами, которые, по словам Google, больше не являются простыми инструментами, а являются продолжением их самих.
В новой версии Material 3 Expressive компания Google представила новую систему физики движений, призванную сделать пользовательский интерфейс приложений более живым, плавным и естественным.
По мере изучения возможностей Material 3 Expressive одним из первых компонентов, которые я собираюсь внедрить, станут кнопки-переключатели. Благодаря новой выразительной системе движений и визуальных эффектов мне не терпится увидеть, как эти кнопки могут стать более динамичными, плавными и эмоционально привлекательными — выйти за рамки статичных элементов пользовательского интерфейса и стать тем, что действительно реагирует на взаимодействие с пользователем.
Давайте начнем!
Для начала создадим 3 кнопки, как в превью Google, но сделаем это пошагово.
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, ) } } }
Вот что мы получим в результате. Давайте сделаем так, чтобы это соответствовало примеру Google с изменением размеров, просто используя обычный размер Floating Action Button.
Теперь у нас есть все необходимое, не хватает только одной вещи. Давайте добавим немного анимации и сделаем интерфейс живым.
@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, ) } }
Я изменил фиксированную ширину элементов с помощью весов, чтобы пользовательский интерфейс имел одинаковые пропорции на всех размерах экрана.
И вот окончательный результат:
Я решил написать все это сам, но вы можете использовать и настраивать это по своему усмотрению.
Если вы не хотите разбираться с кастомными анимациями вручную, вы можете использовать ButtonGroup
, который уже поддерживает все новые анимации. Однако имейте в виду, что в настоящее время она находится в стадии альфа-версии — это означает, что API еще нестабилен и может быть изменен в будущих релизах — и ее функциональность пока довольно ограничена.
Подробнее о M3 Expressive вы можете узнать здесь.
-
Новости3 недели назад
Видео и подкасты о мобильной разработке 2025.22
-
Новости2 недели назад
Видео и подкасты о мобильной разработке 2025.24
-
Вовлечение пользователей4 недели назад
Небольшое изменение в интерфейсе Duolingo, которое меняет все
-
Маркетинг и монетизация4 недели назад
Институциональные покупки: понимание и обнаружение