Компоненты интерфейса предоставляют пользователю устройства обратную связь, реагируя на его действия. Каждый компонент по-своему реагирует на действия пользователя, что помогает ему понять, что происходит. Например, если пользователь нажимает кнопку на сенсорном экране устройства, она, скорее всего, изменится каким-либо образом, например будет выделена цветом. Это изменение позволяет пользователю понять, что он нажал на кнопку. Если пользователь не хочет выполнять это действие, он может отвести палец от кнопки, прежде чем отпустить ее.
В документации по жестам Compose описано, как компоненты 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(), функция "Создать" также предлагает 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 при наведении или снятии указателя.
Создание компонентов, которые потребляют и производят данные
Основные компоненты, например материал 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, необходимые для управления эффектами волн и другими визуальными эффектами. Библиотека Foundation предоставляет высокоуровневые модификаторы взаимодействия, такие как Modifier.hoverable, Modifier.focusable и Modifier.draggable.
Чтобы создать компонент, реагирующий на наведение указателя, просто используйте Modifier.hoverable и передайте MutableInteractionSource в качестве параметра.
При наведении указателя на компонент он излучает сигнал HoverInteractions, который можно использовать, чтобы изменить внешний вид компонента.
// This InteractionSource will emit hover interactions val interactionSource = remember { MutableInteractionSource() } Box( Modifier .size(100.dp) .hoverable(interactionSource = interactionSource), contentAlignment = Alignment.Center ) { Text("Hello!") }
Чтобы сделать этот компонент доступным для перехода с помощью клавиши Tab, добавьте 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. Если компонент можно нажать, то на него можно навести указатель, а если на него можно навести указатель, то он должен быть доступен для фокуса. С помощью 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 с анимированной рамкой.
Пример: создание компонента с обработкой специальных взаимодействий
Чтобы узнать, как создавать компоненты с настраиваемым ответом на ввод, посмотрите пример измененной кнопки. Предположим, вы хотите, чтобы кнопка меняла внешний вид при нажатии:
Для этого создайте собственную composable-функцию на основе 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 создан на основе существующего Material Button, он реагирует на действия пользователя всеми обычными способами. Когда пользователь нажимает на кнопку, ее прозрачность немного меняется, как у обычного элемента Material Button.
Как создать и применить многоразовый специальный эффект с помощью Indication
В предыдущих разделах вы узнали, как изменить часть компонента в ответ на различные Interaction, например показать значок при нажатии. Этот же подход можно использовать для изменения значения параметров, которые вы передаете компоненту, или для изменения контента, отображаемого внутри компонента, но это применимо только на уровне компонента. Часто в приложении или системе дизайна есть универсальная система для визуальных эффектов, связанных с состоянием, – эффект, который должен применяться ко всем компонентам единообразно.
Если вы создаете такую систему дизайна, то настроить один компонент и использовать эти настройки для других может быть сложно по следующим причинам:
- Каждому компоненту системы дизайна требуется один и тот же шаблон.
- Легко забыть применить этот эффект к новым компонентам и пользовательским интерактивным компонентам.
- Сложно сочетать с другими эффектами.
Чтобы избежать этих проблем и легко масштабировать пользовательский компонент в системе, можно использовать Indication.
Indication – это многократно используемый визуальный эффект, который можно применять к разным компонентам приложения или системы дизайна. Indication состоит из двух частей:
IndicationNodeFactory– фабрика, которая создает экземплярыModifier.Node, чтобы отрисовывать визуальные эффекты для компонента. Для более простых реализаций, которые не меняются в разных компонентах, можно использовать объект-одиночку и повторно использовать его во всем приложении.Эти экземпляры могут быть с сохранением состояния или без него. Поскольку они создаются для каждого компонента, они могут получать значения из
CompositionLocal, чтобы изменять внешний вид или поведение компонента, как и любые другиеModifier.Node.Modifier.indication: Модификатор, который рисуетIndicationдля компонента.Modifier.clickableи другие модификаторы взаимодействия высокого уровня принимают параметр индикации напрямую, поэтому они не только генерируют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, отвечающий за применение эффекта масштабирования. Прикрепленный узел отслеживает источник взаимодействия, как и в предыдущих примерах. Единственное отличие заключается в том, что он запускает анимацию напрямую, а не преобразует входящие взаимодействия в состояние.Узел должен реализовать
DrawModifierNode, чтобы переопределитьContentDrawScope#draw()и отрисовать эффект масштабирования, используя те же команды рисования, что и в любом другом графическом API в Compose.Вызов
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
- Компоненты и макеты Material