用户界面组件通过其对用户交互的响应方式向设备用户提供反馈。每个组件都有自己响应交互的方式,这有助于用户了解其交互的作用。例如,如果用户触摸设备触摸屏上的按钮,则该按钮可能会以某种方式发生变化,例如可能添加高亮颜色。这种变化让用户知道他们触摸了按钮。如果用户不想这样做,他们会知道在释放之前将手指从按钮上移开——否则,按钮将被激活。
Compose 手势 文档介绍了 Compose 组件如何处理低级指针事件,例如指针移动和点击。开箱即用,Compose 将这些低级事件抽象为更高级别的交互——例如,一系列指针事件可能会累加为按钮按下和释放。了解这些更高级别的抽象可以帮助您自定义 UI 对用户的响应方式。例如,您可能希望自定义组件在用户与其交互时外观的变化方式,或者您可能只想维护这些用户操作的日志。本文档提供了修改标准 UI 元素或设计您自己的 UI 元素所需的信息。
交互
在许多情况下,您不需要知道您的 Compose 组件是如何解释用户交互的。例如,Button
依赖于 Modifier.clickable
来确定用户是否点击了按钮。如果您正在向应用添加一个典型的按钮,您可以定义按钮的 onClick
代码,并且 Modifier.clickable
会在适当的时候运行该代码。这意味着您不需要知道用户是点击屏幕还是使用键盘选择了按钮;Modifier.clickable
会确定用户执行了点击操作,并通过运行您的 onClick
代码来响应。
但是,如果您想自定义 UI 组件对用户行为的响应,您可能需要了解幕后发生的事情。本节将为您提供一些此类信息。
当用户与 UI 组件交互时,系统会通过生成许多 Interaction
事件来表示其行为。例如,如果用户触摸按钮,则按钮会生成 PressInteraction.Press
。如果用户在按钮内抬起手指,则会生成 PressInteraction.Release
,让按钮知道点击已完成。另一方面,如果用户将手指拖到按钮外部,然后抬起手指,则按钮会生成 PressInteraction.Cancel
,以指示对按钮的按下操作已取消,而不是已完成。
这些交互是无意见的。也就是说,这些低级交互事件不打算解释用户操作的含义或其顺序。它们也不会解释哪些用户操作可能优先于其他操作。
这些交互通常成对出现,包括开始和结束。第二个交互包含对第一个交互的引用。例如,如果用户触摸按钮然后抬起手指,则触摸会生成 PressInteraction.Press
交互,而释放会生成 PressInteraction.Release
;Release
具有一个 press
属性,用于识别初始的 PressInteraction.Press
。
您可以通过观察特定组件的 InteractionSource
来查看其交互。 InteractionSource
基于 Kotlin 流 构建,因此您可以像处理任何其他流一样从其中收集交互。有关此设计决策的更多信息,请参阅 阐明交互 博客文章。
交互状态
您可能希望通过自己跟踪交互来扩展组件的内置功能。例如,您可能希望按钮在按下时更改颜色。跟踪交互的最简单方法是观察相应的交互状态。 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()
。这些方法实际上是构建在更低级 InteractionSource
API 之上的便利方法。在某些情况下,您可能希望直接使用这些更低级的函数。
例如,假设您需要知道按钮是否正在被按下,以及是否正在被拖动。如果您同时使用 collectIsPressedAsState()
和 collectIsDraggedAsState()
,Compose 会执行大量重复工作,并且无法保证您会以正确的顺序获得所有交互。对于这种情况,您可能希望直接使用 InteractionSource
。有关使用 InteractionSource
自己跟踪交互的更多信息,请参阅 使用 InteractionSource
。
下一节介绍如何分别使用 InteractionSource
和 MutableInteractionSource
使用和发出交互。
使用和发出 Interaction
InteractionSource
表示 Interactions
的只读流——无法向 InteractionSource
发出 Interaction
。要发出 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
。您可以使用它来控制该组件或 UI 中任何其他组件的外观。
如果您正在构建自己的交互式高级组件,我们建议您以这种方式公开 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
更高级别的抽象 - 对于一个可点击的组件,它隐式地是可悬停的,并且可点击的组件也应该是可聚焦的。您可以使用 Modifier.clickable
创建一个处理悬停、聚焦和按下交互的组件,而无需组合低级 API。如果您想使您的组件也可点击,您可以用 clickable
替换 hoverable
和 focusable
。
// 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
如果您需要有关组件交互的低级信息,您可以为此组件的 InteractionSource
使用标准的 flow API。例如,假设您想维护一个组件 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 波纹效果实现确定要用于最近一次交互的适当状态覆盖的方式。
val lastInteraction = when (interactions.lastOrNull()) { is DragInteraction.Start -> "Dragged" is PressInteraction.Press -> "Pressed" else -> "No state" }
因为所有 Interaction
都遵循相同的结构,所以在处理不同类型的用户交互时,代码差别不大 - 总体模式相同。
请注意,本节中的前几个示例使用 State
表示 Flow
的交互 - 这使得观察更新的值变得很容易,因为读取状态值会自动导致重新组合。但是,组合是在帧前**批量处理**的。这意味着,如果状态发生变化,然后在同一帧内恢复到原始状态,则观察该状态的组件将看不到变化。
这对交互很重要,因为交互可以定期地在同一帧内开始和结束。例如,使用前面带有 Button
的示例。
val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() Button(onClick = { /* do something */ }, interactionSource = interactionSource) { Text(if (isPressed) "Pressed!" else "Not pressed") }
如果按下操作在同一帧内开始和结束,则文本将永远不会显示为“Pressed!”。在大多数情况下,这不是问题 - 显示如此短时间的视觉效果会导致闪烁,用户不会注意到。在某些情况下,例如显示波纹效果或类似的动画,您可能希望至少显示一段时间的效果,而不是在按钮不再被按下时立即停止。为此,您可以直接从 collect lambda 内部启动和停止动画,而不是写入状态。在 构建具有动画边框的增强型 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
是建立在现有的 Material 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
替换效果
本节介绍如何将手动应用于一个特定按钮的缩放效果替换为一个等效的 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
。附加后,该节点会观察交互源,类似于前面的示例。这里唯一的区别是它直接启动动画,而不是将传入的 Interactions 转换为状态。该节点需要实现
DrawModifierNode
,以便它可以覆盖ContentDrawScope#draw()
,并使用与 Compose 中任何其他图形 API 相同的绘图命令呈现缩放效果。调用
ContentDrawScope
接收器提供的drawContent()
将绘制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
。它的唯一职责是为提供的交互源创建一个新的节点实例。由于没有参数可用于配置 indication,因此该工厂可以是对象: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
不仅限于转换效果,例如缩放组件。因为 IndicationNodeFactory
返回一个 Modifier.Node
,所以您可以像使用其他绘图 API 一样在内容的上方或下方绘制任何类型的效果。例如,您可以在组件周围绘制一个动画边框,并在按下时在组件顶部绘制一个覆盖层。
此处的 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 关闭时,显示链接文本。
- 了解手势
- Jetpack Compose 的 Kotlin
- Material Components 和布局