Компоненты пользовательского интерфейса предоставляют пользователю устройства обратную связь, реагируя на его действия. Каждый компонент имеет свой собственный способ реагирования на взаимодействия, что помогает пользователю понять, к чему приводят его действия. Например, если пользователь коснется кнопки на сенсорном экране устройства, кнопка, скорее всего, изменится каким-либо образом, например, добавится цвет подсветки. Это изменение даст пользователю понять, что он коснулся кнопки. Если пользователь не хотел этого делать, он будет знать, что нужно отвести палец от кнопки перед тем, как отпустить ее — в противном случае кнопка активируется.


В документации по Compose Gestures описывается, как компоненты Compose обрабатывают низкоуровневые события указателя, такие как перемещение указателя и щелчки. По умолчанию Compose абстрагирует эти низкоуровневые события в высокоуровневые взаимодействия — например, серия событий указателя может суммироваться в нажатие и отпускание кнопки. Понимание этих высокоуровневых абстракций может помочь вам настроить реакцию вашего пользовательского интерфейса на действия пользователя. Например, вы можете захотеть настроить изменение внешнего вида компонента при взаимодействии с ним пользователем, или, возможно, вы просто хотите вести журнал действий пользователя. Этот документ предоставляет вам информацию, необходимую для изменения стандартных элементов пользовательского интерфейса или для создания собственных.
Взаимодействия
Во многих случаях вам не нужно знать, как именно ваш компонент Compose интерпретирует действия пользователя. Например, Button полагается на Modifier.clickable , чтобы определить, нажал ли пользователь на кнопку. Если вы добавляете в свое приложение обычную кнопку, вы можете определить код onClick для кнопки, и Modifier.clickable будет выполнять этот код при необходимости. Это означает, что вам не нужно знать, коснулся ли пользователь экрана или выбрал кнопку с помощью клавиатуры; Modifier.clickable определяет, что пользователь совершил клик, и реагирует, выполняя ваш код onClick .
Однако, если вы хотите настроить реакцию компонента пользовательского интерфейса на поведение пользователя, вам может потребоваться узнать больше о том, что происходит «под капотом». В этом разделе вы найдете некоторую необходимую информацию.
Когда пользователь взаимодействует с компонентом пользовательского интерфейса, система отображает его поведение, генерируя ряд событий Interaction . Например, если пользователь касается кнопки, кнопка генерирует событие PressInteraction.Press . Если пользователь убирает палец внутри кнопки, генерируется PressInteraction.Release , сообщающее кнопке о завершении нажатия. С другой стороны, если пользователь проводит пальцем за пределы кнопки, а затем убирает палец, кнопка генерирует PressInteraction.Cancel , указывающее на то, что нажатие на кнопку было отменено, а не завершено.
Эти взаимодействия не содержат предвзятого мнения . То есть, эти низкоуровневые события взаимодействия не направлены на интерпретацию смысла действий пользователя или их последовательности. Они также не определяют, какие действия пользователя могут иметь приоритет над другими действиями.
Эти взаимодействия обычно происходят парами, имея начало и конец. Второе взаимодействие содержит ссылку на первое. Например, если пользователь касается кнопки, а затем отпускает палец, касание генерирует взаимодействие PressInteraction.Press , а отпускание — PressInteraction.Release ; у Release есть свойство press , идентифицирующее начальное значение PressInteraction.Press .
Вы можете увидеть взаимодействия для конкретного компонента, изучив его InteractionSource . InteractionSource построен на основе потоков Kotlin , поэтому вы можете собирать данные о взаимодействиях из него так же, как и из любого другого потока. Для получения дополнительной информации об этом проектном решении см. статью в блоге Illuminating Interactions .
Состояние взаимодействия
Возможно, вам захочется расширить встроенную функциональность ваших компонентов, отслеживая взаимодействия самостоятельно. Например, вы можете захотеть, чтобы кнопка меняла цвет при нажатии. Самый простой способ отслеживать взаимодействия — наблюдать за соответствующим состоянием взаимодействия. InteractionSource предлагает ряд методов, которые отображают различные статусы взаимодействия в виде состояния. Например, если вы хотите узнать, нажата ли определенная кнопка, вы можете вызвать ее метод InteractionSource.collectIsPressedAsState() :
val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() Button( onClick = { /* do something */ }, interactionSource = interactionSource ) { Text(if (isPressed) "Pressed!" else "Not pressed") }
Помимо collectIsPressedAsState() , Compose также предоставляет collectIsFocusedAsState() , collectIsDraggedAsState() и collectIsHoveredAsState() . Эти методы на самом деле являются вспомогательными методами, построенными на основе низкоуровневых API InteractionSource . В некоторых случаях вам может потребоваться использовать эти низкоуровневые функции напрямую.
Например, предположим, вам нужно знать, нажата ли кнопка, а также перетаскивается ли она. Если вы используете одновременно collectIsPressedAsState() и collectIsDraggedAsState() , Compose выполнит много дублирующей работы, и нет гарантии, что вы получите все взаимодействия в правильном порядке. В подобных ситуациях вам может быть удобнее работать напрямую с InteractionSource . Дополнительную информацию о самостоятельном отслеживании взаимодействий с помощью InteractionSource см. в разделе «Работа с InteractionSource .
В следующем разделе описывается, как получать и отправлять данные о взаимодействиях с помощью InteractionSource и MutableInteractionSource соответственно.
Потребление и излучение Interaction
InteractionSource представляет собой поток Interactions только для чтения — невозможно отправить Interaction в InteractionSource . Для отправки Interaction необходимо использовать MutableInteractionSource , который наследуется от InteractionSource .
Модификаторы и компоненты могут потреблять, испускать или потреблять и испускать Interactions . В следующих разделах описано, как потреблять и испускать взаимодействия как от модификаторов, так и от компонентов.
Пример модификатора потребления
Для модификатора, который рисует границу для состояния фокусировки, достаточно отслеживать Interactions , поэтому можно принять InteractionSource :
fun Modifier.focusBorder(interactionSource: InteractionSource): Modifier { // ... }
Из сигнатуры функции ясно, что этот модификатор является потребителем — он может потреблять Interaction , но не может их испускать.
Пример модификатора, производящего продукт
Для модификатора, обрабатывающего события наведения курсора, например Modifier.hoverable , необходимо генерировать Interactions и принимать в качестве параметра объект MutableInteractionSource :
fun Modifier.hover(interactionSource: MutableInteractionSource, enabled: Boolean): Modifier { // ... }
Этот модификатор является производителем — он может использовать предоставленный MutableInteractionSource для генерации HoverInteractions при наведении курсора или снятии курсора.
Создавайте компоненты, которые потребляют и производят ресурсы.
Компоненты высокого уровня, такие как Material Button выступают одновременно и в роли производителей, и в роли потребителей. Они обрабатывают события ввода и фокусировки, а также изменяют свой внешний вид в ответ на эти события, например, отображают рябь или анимируют свое поднятие. В результате они напрямую предоставляют MutableInteractionSource в качестве параметра, так что вы можете указать свой собственный запомненный экземпляр:
@Composable fun Button( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, // exposes MutableInteractionSource as a parameter interactionSource: MutableInteractionSource? = null, elevation: ButtonElevation? = ButtonDefaults.elevatedButtonElevation(), shape: Shape = MaterialTheme.shapes.small, border: BorderStroke? = null, colors: ButtonColors = ButtonDefaults.buttonColors(), contentPadding: PaddingValues = ButtonDefaults.ContentPadding, content: @Composable RowScope.() -> Unit ) { /* content() */ }
Это позволяет вынести MutableInteractionSource за пределы компонента и отслеживать все Interaction , создаваемые компонентом. Вы можете использовать это для управления внешним видом этого компонента или любого другого компонента в вашем пользовательском интерфейсе.
Если вы создаёте собственные интерактивные высокоуровневые компоненты, мы рекомендуем предоставлять MutableInteractionSource в качестве параметра таким образом . Помимо соблюдения лучших практик в области поднятия состояния, это также упрощает чтение и управление визуальным состоянием компонента так же, как и любым другим состоянием (например, состоянием активности).
Compose использует многоуровневый архитектурный подход , поэтому высокоуровневые компоненты Material строятся на основе базовых строительных блоков, которые создают необходимые им элементы Interaction для управления рябью и другими визуальными эффектами. Базовая библиотека предоставляет высокоуровневые модификаторы взаимодействия, такие как Modifier.hoverable , Modifier.focusable и Modifier.draggable .
Чтобы создать компонент, реагирующий на события наведения курсора, вы можете просто использовать Modifier.hoverable и передать в качестве параметра MutableInteractionSource . При каждом наведении курсора на компонент он генерирует события HoverInteraction , и вы можете использовать это для изменения внешнего вида компонента.
// This InteractionSource will emit hover interactions val interactionSource = remember { MutableInteractionSource() } Box( Modifier .size(100.dp) .hoverable(interactionSource = interactionSource), contentAlignment = Alignment.Center ) { Text("Hello!") }
Чтобы сделать этот компонент также фокусируемым, можно добавить Modifier.focusable и передать тот же MutableInteractionSource в качестве параметра. Теперь и HoverInteraction.Enter/Exit , и FocusInteraction.Focus/Unfocus будут генерироваться через один и тот же MutableInteractionSource , и вы сможете настроить внешний вид для обоих типов взаимодействия в одном и том же месте:
// This InteractionSource will emit hover and focus interactions val interactionSource = remember { MutableInteractionSource() } Box( Modifier .size(100.dp) .hoverable(interactionSource = interactionSource) .focusable(interactionSource = interactionSource), contentAlignment = Alignment.Center ) { Text("Hello!") }
Modifier.clickable — это ещё более высокий уровень абстракции, чем hoverable и focusable — чтобы компонент был кликабельным, он по умолчанию должен быть hoverable, а компоненты, по которым можно кликнуть, также должны быть focusable. Вы можете использовать Modifier.clickable для создания компонента, который обрабатывает взаимодействия при наведении курсора, фокусировке и нажатии, без необходимости комбинировать низкоуровневые API. Если вы хотите сделать свой компонент также кликабельным, вы можете заменить hoverable и focusable на clickable :
// This InteractionSource will emit hover, focus, and press interactions val interactionSource = remember { MutableInteractionSource() } Box( Modifier .size(100.dp) .clickable( onClick = {}, interactionSource = interactionSource, // Also show a ripple effect indication = ripple() ), contentAlignment = Alignment.Center ) { Text("Hello!") }
Работа с InteractionSource
Если вам нужна информация низкого уровня о взаимодействиях с компонентом, вы можете использовать стандартные API потока для InteractionSource этого компонента. Например, предположим, вы хотите вести список взаимодействий типа «нажатие» и «перетаскивание» для InteractionSource . Этот код выполняет половину работы, добавляя новые нажатия в список по мере их поступления:
val interactionSource = remember { MutableInteractionSource() } val interactions = remember { mutableStateListOf<Interaction>() } LaunchedEffect(interactionSource) { interactionSource.interactions.collect { interaction -> when (interaction) { is PressInteraction.Press -> { interactions.add(interaction) } is DragInteraction.Start -> { interactions.add(interaction) } } } }
Но помимо добавления новых взаимодействий, необходимо также удалять взаимодействия по завершении (например, когда пользователь убирает палец с компонента). Это легко сделать, поскольку завершающие взаимодействия всегда содержат ссылку на соответствующее начальное взаимодействие. Этот код показывает, как удалить завершившиеся взаимодействия:
val interactionSource = remember { MutableInteractionSource() } val interactions = remember { mutableStateListOf<Interaction>() } LaunchedEffect(interactionSource) { interactionSource.interactions.collect { interaction -> when (interaction) { is PressInteraction.Press -> { interactions.add(interaction) } is PressInteraction.Release -> { interactions.remove(interaction.press) } is PressInteraction.Cancel -> { interactions.remove(interaction.press) } is DragInteraction.Start -> { interactions.add(interaction) } is DragInteraction.Stop -> { interactions.remove(interaction.start) } is DragInteraction.Cancel -> { interactions.remove(interaction.start) } } } }
Теперь, если вы хотите узнать, нажат ли в данный момент или перетаскивается компонент, вам достаточно проверить, пуст ли interactions :
val isPressedOrDragged = interactions.isNotEmpty()
Если вы хотите узнать, какое было последнее взаимодействие, просто посмотрите на последний элемент в списке. Например, именно так реализация Compose Ripple определяет подходящее наложение состояния для последнего взаимодействия:
val lastInteraction = when (interactions.lastOrNull()) { is DragInteraction.Start -> "Dragged" is PressInteraction.Press -> "Pressed" else -> "No state" }
Поскольку все Interaction имеют одинаковую структуру, при работе с различными типами взаимодействия с пользователем разница в коде невелика — общая схема остается той же.
Обратите внимание, что предыдущие примеры в этом разделе демонстрируют Flow взаимодействий с использованием State — это упрощает отслеживание обновленных значений, поскольку чтение значения состояния автоматически приводит к перекомпозиции. Однако компоновка осуществляется пакетно перед кадром. Это означает, что если состояние изменяется, а затем возвращается к исходному состоянию в течение одного кадра, компоненты, отслеживающие это состояние, не увидят изменения.
Это важно для взаимодействия, поскольку взаимодействие может регулярно начинаться и заканчиваться в пределах одного и того же кадра. Например, используя предыдущий пример с Button :
val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() Button(onClick = { /* do something */ }, interactionSource = interactionSource) { Text(if (isPressed) "Pressed!" else "Not pressed") }
Если нажатие начинается и заканчивается в одном и том же кадре, текст никогда не отобразится как «Нажато!». В большинстве случаев это не проблема — отображение визуального эффекта в течение такого короткого промежутка времени приведет к мерцанию и будет малозаметно для пользователя. В некоторых случаях, например, при отображении эффекта ряби или подобной анимации, может потребоваться показывать эффект хотя бы в течение минимального времени, а не останавливать его сразу, если кнопка больше не нажата. Для этого можно запускать и останавливать анимацию непосредственно из лямбда-функции `collect`, вместо записи в состояние. Пример такого подхода приведен в разделе «Создание расширенного Indication с анимированной рамкой» .
Пример: Создание компонента с пользовательской обработкой взаимодействия.
Чтобы увидеть, как можно создавать компоненты с настраиваемой реакцией на ввод, вот пример модифицированной кнопки. В этом случае предположим, что вам нужна кнопка, которая реагирует на нажатия изменением своего внешнего вида:

Для этого создайте собственный компонент на основе Button и добавьте в него дополнительный параметр icon для отрисовки значка (в данном случае, корзины покупок). Вызывайте collectIsPressedAsState() , чтобы отслеживать, находится ли пользователь над кнопкой; когда это происходит, добавляйте значок. Вот как выглядит код:
@Composable fun PressIconButton( onClick: () -> Unit, icon: @Composable () -> Unit, text: @Composable () -> Unit, modifier: Modifier = Modifier, interactionSource: MutableInteractionSource? = null ) { val isPressed = interactionSource?.collectIsPressedAsState()?.value ?: false Button( onClick = onClick, modifier = modifier, interactionSource = interactionSource ) { AnimatedVisibility(visible = isPressed) { if (isPressed) { Row { icon() Spacer(Modifier.size(ButtonDefaults.IconSpacing)) } } } text() } }
А вот как выглядит использование этого нового составного элемента:
PressIconButton( onClick = {}, icon = { Icon(Icons.Filled.ShoppingCart, contentDescription = null) }, text = { Text("Add to cart") } )
Поскольку новая PressIconButton построена на основе существующей Button Material Button, она реагирует на действия пользователя всеми обычными способами. Когда пользователь нажимает на кнопку, ее прозрачность слегка изменяется, как у обычной Button Material Button.
Создавайте и применяйте многоразовые пользовательские эффекты с помощью Indication
В предыдущих разделах вы узнали, как изменять часть компонента в ответ на различные Interaction , например, отображать значок при нажатии. Этот же подход можно использовать для изменения значений параметров, передаваемых компоненту, или для изменения содержимого, отображаемого внутри компонента, но это применимо только к каждому компоненту в отдельности. Часто в приложениях или системах проектирования используется общая система для визуальных эффектов с сохранением состояния — эффект, который должен применяться ко всем компонентам согласованным образом.
При создании подобной дизайн-системы настройка одного компонента и повторное использование этой настройки для других компонентов может быть затруднительной по следующим причинам:
- Для каждого компонента в системе проектирования необходим один и тот же стандартный шаблон.
- Легко забыть применить этот эффект к новым компонентам и пользовательским компонентам с возможностью клика.
- Возможно, будет сложно сочетать этот пользовательский эффект с другими эффектами.
Чтобы избежать этих проблем и легко масштабировать пользовательский компонент в рамках всей системы, вы можете использовать Indication . Indication представляет собой многократно используемый визуальный эффект, который можно применять к компонентам в приложении или системе дизайна. Indication состоит из двух частей:
IndicationNodeFactory: Фабрика, создающая экземплярыModifier.Node, отвечающие за рендеринг визуальных эффектов для компонента. Для более простых реализаций, не изменяющихся между компонентами, это может быть синглтон (объект), используемый во всем приложении.Эти экземпляры могут быть с состоянием или без состояния. Поскольку они создаются для каждого компонента, они могут получать значения из
CompositionLocal, чтобы изменять свой внешний вид или поведение внутри конкретного компонента, как и любой другойModifier.Node.Modifier.indication: Модификатор, который отображаетIndicationдля компонента.Modifier.clickableи другие высокоуровневые модификаторы взаимодействия принимают параметр indication напрямую, поэтому они не только генерируют эффектыInteraction, но и могут отображать визуальные эффекты для этихInteraction. Таким образом, в простых случаях можно использоватьModifier.clickableбез необходимостиModifier.indication.
Замените слово «эффект» на « Indication .
В этом разделе описывается, как заменить эффект ручного масштабирования, применяемый к одной конкретной кнопке, на эквивалентный индикатор, который можно использовать повторно в нескольких компонентах.
Следующий код создает кнопку, которая при нажатии уменьшается в размере:
val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() val scale by animateFloatAsState(targetValue = if (isPressed) 0.9f else 1f, label = "scale") Button( modifier = Modifier.scale(scale), onClick = { }, interactionSource = interactionSource ) { Text(if (isPressed) "Pressed!" else "Not pressed") }
Чтобы преобразовать эффект масштабирования в приведенном выше фрагменте кода в Indication , выполните следующие действия:
Создайте узел
Modifier.Nodeотвечающий за применение эффекта масштабирования . После подключения узел будет отслеживать источник взаимодействия, как и в предыдущих примерах. Единственное отличие заключается в том, что он напрямую запускает анимации, а не преобразует входящие взаимодействия в состояние.Для того чтобы переопределить
ContentDrawScope#draw()и отобразить эффект масштабирования, используя те же команды рисования, что и в любом другом графическом API в Compose, узлу необходимо реализовать интерфейсDrawModifierNodeВызов функции
drawContent()доступной в приемникеContentDrawScopeотобразит фактический компонент, к которому должно быть примененоIndication, поэтому вам нужно просто вызвать эту функцию внутри преобразования масштабирования. Убедитесь, что ваши реализацииIndicationвсегда вызываютdrawContent()в какой-то момент; в противном случае компонент, к которому применяетсяIndication, не будет отрисован.private class ScaleNode(private val interactionSource: InteractionSource) : Modifier.Node(), DrawModifierNode { var currentPressPosition: Offset = Offset.Zero val animatedScalePercent = Animatable(1f) private suspend fun animateToPressed(pressPosition: Offset) { currentPressPosition = pressPosition animatedScalePercent.animateTo(0.9f, spring()) } private suspend fun animateToResting() { animatedScalePercent.animateTo(1f, spring()) } override fun onAttach() { coroutineScope.launch { interactionSource.interactions.collectLatest { interaction -> when (interaction) { is PressInteraction.Press -> animateToPressed(interaction.pressPosition) is PressInteraction.Release -> animateToResting() is PressInteraction.Cancel -> animateToResting() } } } } override fun ContentDrawScope.draw() { scale( scale = animatedScalePercent.value, pivot = currentPressPosition ) { this@draw.drawContent() } } }
Создайте объект
IndicationNodeFactory. Его единственная задача — создать новый экземпляр узла для заданного источника взаимодействия. Поскольку для настройки индикации нет параметров, фабрика может быть объектом:object ScaleIndication : IndicationNodeFactory { override fun create(interactionSource: InteractionSource): DelegatableNode { return ScaleNode(interactionSource) } override fun equals(other: Any?): Boolean = other === ScaleIndication override fun hashCode() = 100 }
Modifier.clickableиспользуетModifier.indicationвнутри себя, поэтому для создания кликабельного компонента соScaleIndicationдостаточно передатьIndicationв качестве параметра компонентуclickable:Box( modifier = Modifier .size(100.dp) .clickable( onClick = {}, indication = ScaleIndication, interactionSource = null ) .background(Color.Blue), contentAlignment = Alignment.Center ) { Text("Hello!", color = Color.White) }
Это также упрощает создание высокоуровневых, многократно используемых компонентов с помощью пользовательской
Indication— кнопка может выглядеть так:@Composable fun ScaleButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, interactionSource: MutableInteractionSource? = null, shape: Shape = CircleShape, content: @Composable RowScope.() -> Unit ) { Row( modifier = modifier .defaultMinSize(minWidth = 76.dp, minHeight = 48.dp) .clickable( enabled = enabled, indication = ScaleIndication, interactionSource = interactionSource, onClick = onClick ) .border(width = 2.dp, color = Color.Blue, shape = shape) .padding(horizontal = 16.dp, vertical = 8.dp), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, content = content ) }
Затем вы можете использовать кнопку следующим образом:
ScaleButton(onClick = {}) { Icon(Icons.Filled.ShoppingCart, "") Spacer(Modifier.padding(10.dp)) Text(text = "Add to cart!") }

Indication . Создайте продвинутую Indication с анимированной рамкой.
Indication не ограничивается только эффектами трансформации, такими как масштабирование компонента. Поскольку IndicationNodeFactory возвращает Modifier.Node , вы можете рисовать любые эффекты над или под содержимым, как и с другими API для рисования. Например, вы можете нарисовать анимированную рамку вокруг компонента и наложение поверх компонента при нажатии:

Indication . Реализация функции Indication здесь очень похожа на предыдущий пример — она просто создает узел с некоторыми параметрами. Поскольку анимированная граница зависит от формы и границы компонента, для которого используется Indication , реализация Indication также требует указания формы и ширины границы в качестве параметров:
data class NeonIndication(private val shape: Shape, private val borderWidth: Dp) : IndicationNodeFactory { override fun create(interactionSource: InteractionSource): DelegatableNode { return NeonNode( shape, // Double the border size for a stronger press effect borderWidth * 2, interactionSource ) } }
Реализация Modifier.Node также концептуально идентична, хотя код отрисовки более сложный. Как и прежде, она отслеживает InteractionSource при подключении, запускает анимации и реализует DrawModifierNode для отрисовки эффекта поверх контента:
private class NeonNode( private val shape: Shape, private val borderWidth: Dp, private val interactionSource: InteractionSource ) : Modifier.Node(), DrawModifierNode { var currentPressPosition: Offset = Offset.Zero val animatedProgress = Animatable(0f) val animatedPressAlpha = Animatable(1f) var pressedAnimation: Job? = null var restingAnimation: Job? = null private suspend fun animateToPressed(pressPosition: Offset) { // Finish any existing animations, in case of a new press while we are still showing // an animation for a previous one restingAnimation?.cancel() pressedAnimation?.cancel() pressedAnimation = coroutineScope.launch { currentPressPosition = pressPosition animatedPressAlpha.snapTo(1f) animatedProgress.snapTo(0f) animatedProgress.animateTo(1f, tween(450)) } } private fun animateToResting() { restingAnimation = coroutineScope.launch { // Wait for the existing press animation to finish if it is still ongoing pressedAnimation?.join() animatedPressAlpha.animateTo(0f, tween(250)) animatedProgress.snapTo(0f) } } override fun onAttach() { coroutineScope.launch { interactionSource.interactions.collect { interaction -> when (interaction) { is PressInteraction.Press -> animateToPressed(interaction.pressPosition) is PressInteraction.Release -> animateToResting() is PressInteraction.Cancel -> animateToResting() } } } } override fun ContentDrawScope.draw() { val (startPosition, endPosition) = calculateGradientStartAndEndFromPressPosition( currentPressPosition, size ) val brush = animateBrush( startPosition = startPosition, endPosition = endPosition, progress = animatedProgress.value ) val alpha = animatedPressAlpha.value drawContent() val outline = shape.createOutline(size, layoutDirection, this) // Draw overlay on top of content drawOutline( outline = outline, brush = brush, alpha = alpha * 0.1f ) // Draw border on top of overlay drawOutline( outline = outline, brush = brush, alpha = alpha, style = Stroke(width = borderWidth.toPx()) ) } /** * Calculates a gradient start / end where start is the point on the bounding rectangle of * size [size] that intercepts with the line drawn from the center to [pressPosition], * and end is the intercept on the opposite end of that line. */ private fun calculateGradientStartAndEndFromPressPosition( pressPosition: Offset, size: Size ): Pair<Offset, Offset> { // Convert to offset from the center val offset = pressPosition - size.center // y = mx + c, c is 0, so just test for x and y to see where the intercept is val gradient = offset.y / offset.x // We are starting from the center, so halve the width and height - convert the sign // to match the offset val width = (size.width / 2f) * sign(offset.x) val height = (size.height / 2f) * sign(offset.y) val x = height / gradient val y = gradient * width // Figure out which intercept lies within bounds val intercept = if (abs(y) <= abs(height)) { Offset(width, y) } else { Offset(x, height) } // Convert back to offsets from 0,0 val start = intercept + size.center val end = Offset(size.width - start.x, size.height - start.y) return start to end } private fun animateBrush( startPosition: Offset, endPosition: Offset, progress: Float ): Brush { if (progress == 0f) return TransparentBrush // This is *expensive* - we are doing a lot of allocations on each animation frame. To // recreate a similar effect in a performant way, it would be better to create one large // gradient and translate it on each frame, instead of creating a whole new gradient // and shader. The current approach will be janky! val colorStops = buildList { when { progress < 1 / 6f -> { val adjustedProgress = progress * 6f add(0f to Blue) add(adjustedProgress to Color.Transparent) } progress < 2 / 6f -> { val adjustedProgress = (progress - 1 / 6f) * 6f add(0f to Purple) add(adjustedProgress * MaxBlueStop to Blue) add(adjustedProgress to Blue) add(1f to Color.Transparent) } progress < 3 / 6f -> { val adjustedProgress = (progress - 2 / 6f) * 6f add(0f to Pink) add(adjustedProgress * MaxPurpleStop to Purple) add(MaxBlueStop to Blue) add(1f to Blue) } progress < 4 / 6f -> { val adjustedProgress = (progress - 3 / 6f) * 6f add(0f to Orange) add(adjustedProgress * MaxPinkStop to Pink) add(MaxPurpleStop to Purple) add(MaxBlueStop to Blue) add(1f to Blue) } progress < 5 / 6f -> { val adjustedProgress = (progress - 4 / 6f) * 6f add(0f to Yellow) add(adjustedProgress * MaxOrangeStop to Orange) add(MaxPinkStop to Pink) add(MaxPurpleStop to Purple) add(MaxBlueStop to Blue) add(1f to Blue) } else -> { val adjustedProgress = (progress - 5 / 6f) * 6f add(0f to Yellow) add(adjustedProgress * MaxYellowStop to Yellow) add(MaxOrangeStop to Orange) add(MaxPinkStop to Pink) add(MaxPurpleStop to Purple) add(MaxBlueStop to Blue) add(1f to Blue) } } } return linearGradient( colorStops = colorStops.toTypedArray(), start = startPosition, end = endPosition ) } companion object { val TransparentBrush = SolidColor(Color.Transparent) val Blue = Color(0xFF30C0D8) val Purple = Color(0xFF7848A8) val Pink = Color(0xFFF03078) val Orange = Color(0xFFF07800) val Yellow = Color(0xFFF0D800) const val MaxYellowStop = 0.16f const val MaxOrangeStop = 0.33f const val MaxPinkStop = 0.5f const val MaxPurpleStop = 0.67f const val MaxBlueStop = 0.83f } }
Главное отличие здесь в том, что теперь существует минимальная продолжительность анимации для функции animateToResting() , поэтому даже если нажатие будет немедленно отпущено, анимация нажатия продолжится. Также предусмотрена обработка нескольких быстрых нажатий в начале animateToPressed — если нажатие происходит во время существующей анимации нажатия или покоя, предыдущая анимация отменяется, и анимация нажатия начинается с начала. Для поддержки нескольких одновременных эффектов (например, с эффектом ряби, когда новая анимация ряби будет отображаться поверх других анимаций) можно отслеживать анимации в списке, вместо отмены существующих анимаций и запуска новых.
Рекомендуем вам
- Примечание: текст ссылки отображается, когда JavaScript отключен.
- Понимать жесты
- Kotlin для Jetpack Compose
- Компоненты материалов и компоновка