让你的Android UI更亮眼:Jetpack Compose中的可视状态
任何设计系统的重要责任是清晰地表明哪些组件可以与之交互,哪些不行,并让用户知道交互已发生。本博客文章将解释如何监听Jetpack Compose中的用户交互,并创建可重用的视觉指示,可在整个应用程序中应用,以实现一致且响应灵敏的用户体验。
为什么视觉反馈很重要
比较以下两个UI:
缺乏视觉反馈会导致应用程序感觉缓慢或“卡顿”,并导致用户体验不美观。为不同的用户交互提供有意义的反馈,可以帮助用户识别可交互组件,并确认他们的交互是否成功。
用户可用的交互取决于许多因素 - 有些取决于组件是什么(例如,按钮通常可以按下,但不能拖动),有些取决于应用程序的状态(例如数据是否正在加载),还有些取决于用于与应用程序进行交互的输入设备。
常见的交互包括:
- 按下(Press)
- 悬停(Hover)
- 焦点(Focus)
- 拖动(Drag)
为这些交互显示视觉效果为用户提供了即时反馈,帮助他们了解他们的操作如何影响应用程序的状态。例如,在按钮上显示悬停突出显示可以清楚地表明该按钮可以使用,并且在单击时会执行某些操作。相比之下,不显示悬停的组件不太可能在单击时执行任何操作。
组件的外观受到不仅仅是交互的影响 - 其他常见的视觉状态包括:
- 禁用(Disabled)
- 选定(Selected)
- 已激活(Activated)
- 加载中(Loading)
尽管设计系统通常将这些状态与由交互引起的状态视为相似,但存在一些基本差异。最重要的差异是这些状态是由外部控制的,不属于组件。这些状态不是由一个事件引起的,而是表示应用程序的持续状态。没有单一的“禁用”或“启用”事件 - 相反,组件将保持在该状态,直到应用程序中的其他状态发生更改。
相比之下,交互是导致瞬时状态的事件。按下开始,按下结束,'按下’的视觉状态存在于这些事件之间的时间内。此外,多个交互可以同时发生 - 组件可以同时具有焦点和悬停。在这种情况下,对于造成的视觉状态应该是什么没有单一的答案:不同的设计系统以不同的方式处理重叠状态。
在Material Design中,交互状态表示为覆盖在内容上的叠加层。点击涟漪被特别处理,并绘制在其他状态的上方(如果有)。对于非按压交互,将显示最近的交互。因此,如果一个组件聚焦,然后稍后悬停,该组件将看起来被悬停。当取消悬停时,它将返回到聚焦状态。在具有不同状态的不同效果的设计系统中,例如用于悬停的叠加层和用于聚焦的边框效果,同时表示两种效果可能是可取的。
为了支持这些不同的用例,Compose提供了一组不带偏见的API,不会对交互的顺序或优先级做出假设。
交互的基本解剖
每种类型的用户交互都由一个特定事件的独特交互表示。例如,按下事件被分为三种不同的类型:
PressInteraction.Press
——在按下组件时发出(还包含按下相对于组件边界的位置)PressInteraction.Release
——在先前的PressInteraction.Press
被释放时发出(例如,当手指松开时)PressInteraction.Cancel
——在先前的PressInteraction.Press
被取消时发出(例如,当手指在未抬起的情况下移动到组件边界外部时)
为了支持多个同时进行的相同类型的交互,例如当用户用多个手指触摸一个组件时进行多个按下操作,相应于事件“结束”的交互,即Release
和Cancel
,包含对事件“开始”的引用,以便清楚地确定何时完成了交互。
交互的主要入口点是InteractionSource
。交互是与用户交互类型相对应的事件,而InteractionSource
是一个可观察的Interactions
流。通过观察InteractionSource
,您可以了解事件何时开始和停止,并将该信息减少到视觉状态。
InteractionSource
使用Kotlin Flows构建-它公开了一个交互属性,该属性是代表特定组件的Interactions流的Flow <Interaction>
。
在大多数情况下,您不需要直接从Flow中收集数据-在Compose中直接使用状态,以及反应性地声明组件在不同状态下会如何显示更加容易和自然。因此,在内部将InteractionSource
建模为状态而不是事件流似乎很直观,但是这种方法存在一些缺点:
-
生成交互的底层系统,例如指针输入和焦点系统,使用事件而不是状态。将这些事件减少到状态是一种有损转换——这些事件的排序和事件之间的时间被遗失,因为您最终只得到当前交互的列表。这使得构建关心事件顺序的组件(例如涟漪效应)变得具有挑战性,因为您无法重新创建在转换中丢失的信息。
-
在Compose中,
MutableState
是某个时间点上数据的快照。为了效率,对MutableState
的多次写入将合并成一次写入,以限制所执行的工作量。对于真正的应用状态来说,这是理想的,但是对于表示事件来说,这意味着在短时间内发生的多个事件可能会合并为一个事件——例如,两个快速按下可能只会出现一个按下,这可能会导致错过涟漪或其他按下效果。 -
对于大多数用例,将按下和释放表示为“按下”状态就足够了,但是有些情况需要关注每个事件的具体情况-例如,按下发生的位置以及是否释放或取消。以这种方式表示多个次按下也很困难,因为没有易于区分的方法,可以区分“按下”状态和“按下但多次按下”状态。
最初,Compose使用基于状态的实现InteractionSource
(当时称为InteractionState
),但由于这些原因而更改为事件流模型——将事件减少为状态要比尝试从状态重新创建事件更容易。
生产者和消费者
InteractionSource
代表一个只读的 Interactions
流 — 它不可能要向 InteractionSource
发射 Interactions
。要发射 Interactions
,需要使用 MutableInteractionSource
,它是从 InteractionSource
扩展而来的。这种分离与 SharedFlow
和 MutableSharedFlow
、State
和 MutableState
、List
和 MutableList
等一致 — 它允许在 API surface 上定义 producer 和 consumer 的职责,而不是组件实现的细节。
例如,如果您想要构建一个修饰符,用于在聚焦状态下绘制边框,则只需要观察 Interactions
,因此可以接受一个 InteractionSource
。
fun Modifier.focusBorder(interactionSource: InteractionSource): Modifier
在这种情况下,从函数签名可以明显看出,这个修饰符是一个 consumer(消费者) — 它没有办法向外发射 Interactions
,它只能消费 Interactions
。
如果您想要构建一个像 Modifier.hoverable
那样处理鼠标悬停事件的修饰符,请考虑要发射 Interactions
,并接受一个 MutableInteractionSource
作为参数。
fun Modifier.hoverable(interactionSource: MutableInteractionSource, enabled: Boolean): Modifier
这个修饰符是一个 producer(生产者)— 它可以使用提供的 MutableInteractionSource
在鼠标悬停或取消悬停时发射 HoverInteractions
。
高级组件,如 Material Button,既充当 producer 也充当 consumer:它们处理输入和焦点事件,并根据这些事件改变其外观,例如显示涟漪或在响应焦点事件时动画改变它们的高度。因此,它们将 MutableInteractionSource
直接暴露为一个参数,以便您可以提供自己的 remembered instance
。
@Composable
fun Button(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
// exposes MutableInteractionSource as a parameter
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
elevation: ButtonElevation? = ButtonDefaults.elevation(),
shape: Shape = MaterialTheme.shapes.small,
border: BorderStroke? = null,
colors: ButtonColors = ButtonDefaults.buttonColors(),
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
content: @Composable RowScope.() -> Unit
) { /* content() */ }
这使得可变交互源的提升超出组件并观察组件产生的所有交互成为可能。您可以使用此功能控制该组件的外观或 UI 中的任何其他组件。
如果您正在构建自己的交互式高级组件,我们建议以此方式将MutableInteractionSource
公开为参数。除遵循最佳状态提升实践外,这还使得在控制组件的外观时阅读和管理可视化状态与读取和控制任何其他类型的状态(例如启用状态)一样容易。
Compose 采用分层架构方法,这种方法也在此处体现。高级 Material 组件是在产生控制涟漪和其他视觉效果所需的基础构建块之上构建的。基础库提供高级交互修饰符,如 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!")
}
为了使该组件也可以被聚焦,您可以添加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。因此,如果您想使您的组件可点击,您可以只用一个可点击的修改器替换可悬停和可聚焦的修改器。
// 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, this is covered later in ‘Indicating Indications’
indication = rememberRipple()
),
contentAlignment = Alignment.Center
) {
Text("Hello!")
}
内部实现Material组件(例如Button)的方式是:Button使用一个可点击的Surface
,它本质上只是使用Modifier.clickable
的Box。
使用交互
如前所述,通常情况下,您希望与组件上当前交互的状态表示进行交互,而不是每个单独的事件。对于每种类型的交互,都有相应的API观察InteractionSource
,并返回表示该类型交互是否存在的状态。
例如,假设有以下Button:
Button(onClick = { /* do something */ }) {
Text("Hello!")
}
如果你想观察这个按钮是否被按下,可以使用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
一样使用 InteractionSource#collectIsFocusedAsState
,InteractionSource#collectIsDraggedAsState
,和InteractionSource#collectIsHoveredAsState
来观察其他互动。
虽然这些API是为了方便提供的,但实现很小并且非常适合在处理互动时了解通用模式。例如,假设你关心按钮是按下还是被拖动。虽然你可以使用collectIsPressedAsState
和collectIsDraggedAsState
两个函数,但这会导致重复工作,而且会丢失细粒度信息(例如互动的顺序)——你可能只想关心最近的互动,而不是优先考虑其中一个。
为了实现这一点,你需要观察并跟踪InteractionSource
发出的互动。与启动事件对应的新互动将添加到SnapshotStateList
(由mutableStateListOf
创建)中——当该列表发生变化时,读取该列表将导致重新组合。
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)
}
}
}
}
现在你需要做的就是观察与结束事件相对应的交互,因为这些交互(如PressInteraction.Release
)始终携带对开始交互的引用,所以你只需要从列表中移除该引用即可。
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.add(interaction.start)
}
}
}
}
如果按下或拖动按钮,则至少有一个交互未从交互中移除,因此总体结果只是交互不为空:
val isPressedOrDragged = interactions.isNotEmpty()
如果您想知道最近的交互是什么,而不是计算组合状态,则只需查看列表中的最后一个交互-这就是Compose涟漪实现如何显示最近用户交互类型的状态覆盖层的方式。
val lastInteraction = when (interactions.lastOrNull()) {
is DragInteraction.Start -> "Dragged"
is PressInteraction.Press -> "Pressed"
else -> "No 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 lambda内开始和停止动画,而不是编写状态——在高级指示部分中有此模式的示例。
构建交互式组件
您可以使用相同的模式来观察现有组件上的交互以构建更高级别的可重用组件。例如,在构建显示鼠标悬停时显示图标的按钮时(例如在使用Chrome OS设备或连接了鼠标的平板电脑时)。
@Composable
fun HoverButton(
onClick: () -> Unit,
icon: @Composable () -> Unit,
text: @Composable () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) {
val isHovered by interactionSource.collectIsHoveredAsState()
Button(onClick, modifier, enabled, interactionSource = interactionSource) {
AnimatedVisibility(visible = isHovered) {
icon()
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
}
text()
}
}
使用方式如下:
HoverButton(
onClick = {},
icon = { Icon(Icons.Filled.ShoppingCart, contentDescription = null) },
text = { Text("Add to cart") }
)
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")
}
HoverButton
在内部包装了一个Material Button
,但是在悬停时除了正常的悬停状态外还会显示一个图标。以这种方式使用InteractionSource
与先前的示例相同,但现在您有一个更高级别的按钮,它在实现中内部使用InteractionSource
,就像内部按钮在悬停时使用InteractionSource
来改变其高度一样。
添加指示
先前的示例涵盖了在响应不同的交互时要更改组件的部分 - 例如在悬停时显示一个图标的情况。也可以使用相同的方法来更改您提供给组件的参数的值,或更改组件内显示的内容,但这仅适用于每个组件。通常,应用程序或设计系统将具有用于有状态视觉效果的通用系统 - 应将该效果应用于所有组件,并且甚至作为默认提供,以便在修改器(例如可点击)中使用 - 如果您正在使用Material库(并且在MaterialTheme中),则使用Modifier.clickable
将自动显示按下时的涟漪效果。这使得轻松构建自定义组件以在响应不同的交互时显示一致的视觉效果成为可能。
例如,假设您正在构建一个设计系统,其中组件应在按下时缩小 - 按照先前的示例,您可以为按钮编写类似以下内容的内容:
然而,这种方法并不是很重复使用——设计系统中的每个组件都需要相同的样板文件,并且很容易忘记将此效果应用于新构建的组件和自定义可点击组件。同时,将其与其他效果组合也很困难——例如,如果您想除了按压缩放效果外,还要添加焦点和悬停叠加层。
针对这些用例,Compose提供了指示器(Indication
)。提示器代表可重复使用的视觉效果,可在应用程序或设计系统的组件中应用,例如涟漪。指示器分为三个部分:
-
指示器(
Indication
)——用于创建IndicationInstances
的工厂。对于那些在组件之间不发生变化的简单Indication实现,这可以成为一个单例(对象)并在整个应用程序中重复使用。更高级的实现(例如涟漪)可以提供其他功能,例如使涟漪有边界或无边界,并手动更改涟漪的颜色。 -
IndicationInstance
(指示器实例)——应用于特定组件的特定视觉效果实例。IndicationInstances
可以是有状态的或无状态的,并且由于它们是每个组件创建的,因此它们可以从CompositionLocal
中检索值,以更改它们在特定组件中的外观或行为。例如,Material中的涟漪使用LocalRippleTheme
来确定涟漪与不同交互的颜色和不透明度。 -
Modifier.indication
(指示器修改器)——为组件绘制Indication
的修改器。Modifier.clickable
和其他高级交互修改器内部包含Modifier.indication
,因此它们不仅会发出交互,还可以为它们发出的交互绘制视觉效果,因此对于简单情况,您只需使用Modifier.clickable
即可,而不需要Modifier.indication
。
Compose还提供了LocalIndication
(本地指示器)——这是一种允许在层次结构中提供Indication
的CompositionLocal
。默认情况下,它由像clickable
这样的修饰符使用,因此如果您正在构建新的可点击组件,它将自动使用应用程序中提供的Indication
。如前所述,Material库在此使用涟漪作为默认提示。
要将此缩放效果转换为Indication
,您首先需要创建负责应用缩放效果的IndicationInstance
。IndicationInstance
公开一个需要实现的函数ContentDrawScope.drawIndication()
。由于ContentDrawScope
只是DrawScope
实现,因此您可以像在Compose的任何其他图形API中一样使用相同的绘图命令。从ContentDrawScope
接收器中可用的drawContent()
函数将绘制应用Indication的实际组件,因此您只需要在缩放转换中调用此函数。确保您的Indication实现始终在某个点上调用drawContent()
,否则您应用Indication
的组件将不会被绘制。
此示例公开了两个函数来使缩放效果动画化到和从压缩状态,还接受按压位置作为偏移量,以便从按压的确切位置绘制缩放效果。
private class ScaleIndicationInstance : IndicationInstance {
var currentPressPosition: Offset = Offset.Zero
val animatedScalePercent = Animatable(1f)
suspend fun animateToPressed(pressPosition: Offset) {
currentPressPosition = pressPosition
animatedScalePercent.animateTo(0.9f, spring())
}
suspend fun animateToResting() {
animatedScalePercent.animateTo(1f, spring())
}
override fun ContentDrawScope.drawIndication() {
scale(
scale = animatedScalePercent.value,
pivot = currentPressPosition
) {
this@drawIndication.drawContent()
}
}
}
然后,您需要创建指示。它应该创建一个IndicationInstance
,并使用提供的InteractionSource
更新其状态。这与前面观察InteractionSource
的示例相同-唯一的区别在于,您可以直接在实例中使用animateToPressed
和animateToResting
函数来动画显示比例效果,而不是将交互转换为状态。
// Singleton that can be reused
object ScaleIndication : Indication {
@Composable
override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance {
// key the remember against interactionSource, so if it changes we create a new instance
val instance = remember(interactionSource) { ScaleIndicationInstance() }
LaunchedEffect(interactionSource) {
interactionSource.interactions.collectLatest { interaction ->
when (interaction) {
is PressInteraction.Press -> instance.animateToPressed(interaction.pressPosition)
is PressInteraction.Release -> instance.animateToResting()
is PressInteraction.Cancel -> instance.animateToResting()
}
}
}
return instance
}
}
如先前所述,Modifier.clickable 在内部使用 Modifier.indication
,因此要创建一个带有 ScaleIndication
的可点击组件,你只需要将 Indication
作为参数提供给 clickable
。
Box(
modifier = Modifier
.size(100.dp)
.clickable(
onClick = {},
indication = ScaleIndication,
interactionSource = remember { MutableInteractionSource() }
)
.background(Color.Blue),
contentAlignment = Alignment.Center
) {
Text("Hello!", color = Color.White)
}
这也使得使用自定义指示器轻松构建高级可重复使用的组件变得很容易——例如,按钮可以呈现如下外观:
@Composable
fun ScaleButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
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!")
}
您可以使用 LocalIndication 来在应用程序中提供自定义的指示,以便任何新的自定义组件默认使用它。
CompositionLocalProvider(LocalIndication provides ScaleIndication) {
// content()
}
注意:涟漪是在 RenderThread 上绘制的(在幕后使用框架 RippleDrawable),这意味着当 UI 线程忙碌时它们可以继续平滑地动画,例如当按下按钮使您的应用程序导航到新屏幕时。没有公共 API 来允许手动向 RenderThread 绘图,因此,如果您正在尝试构建指示,使其在点击完成后仍具有动画效果(例如涟漪或下一节中的示例),请注意这可能会导致 UI 线程上发生抖动。
高级指示
指示不仅仅局限于转换效果,比如缩放组件 - 由于IndicationInstance提供了ContentDrawScope,你可以绘制任何类型的效果,无论是在内容上面还是下面。例如,在组件周围绘制动画边框以及在其被按下时在其上面叠加一个覆盖层。
这里的指示实现与前面的示例非常类似 - 它只是创建实例并启动动画。由于动画边框取决于指示所用的组件的形状和边框,因此指示实现还需要提供形状和边框宽度作为参数。
class NeonIndication(private val shape: Shape, private val borderWidth: Dp) : Indication {
@Composable
override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance {
// key the remember against interactionSource, so if it changes we create a new instance
val instance = remember(interactionSource) {
NeonIndicationInstance(
shape,
// Double the border size for a stronger press effect
borderWidth * 2
)
}
LaunchedEffect(interactionSource) {
interactionSource.interactions.collect { interaction ->
when (interaction) {
is PressInteraction.Press -> instance.animateToPressed(interaction.pressPosition, this)
is PressInteraction.Release -> instance.animateToResting(this)
is PressInteraction.Cancel -> instance.animateToResting(this)
}
}
}
return instance
}
private class NeonIndicationInstance(
private val shape: Shape,
private val borderWidth: Dp
) : IndicationInstance …
}
即使绘制代码必然更加复杂,IndicationInstance
的概念也是相同的。与以前一样,它公开了动画到按下和休息状态的函数,并实现了drawIndication
来绘制效果(为了简洁起见省略了部分绘制代码)。
class NeonIndication(private val shape: Shape, private val borderWidth: Dp) : Indication {
@Composable
override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance {
// key the remember against interactionSource, so if it changes we create a new instance
val instance = remember(interactionSource) {
NeonIndicationInstance(
shape,
// Double the border size for a stronger press effect
borderWidth * 2
)
}
LaunchedEffect(interactionSource) {
interactionSource.interactions.collect { interaction ->
when (interaction) {
is PressInteraction.Press -> instance.animateToPressed(interaction.pressPosition, this)
is PressInteraction.Release -> instance.animateToResting(this)
is PressInteraction.Cancel -> instance.animateToResting(this)
}
}
}
return instance
}
private class NeonIndicationInstance(
private val shape: Shape,
private val borderWidth: Dp
) : IndicationInstance …
}
主要的不同之处在于现在动画有了最小的持续时间,因此即使立即释放按键,按压动画也会继续播放。还有处理多次快速按压的功能 - 如果按压发生在现有的按压或休息动画期间,先前的动画将被取消,按压动画将从头开始。为了支持多个并发效果(例如涟漪),您可以在列表中跟踪动画,而不是取消现有动画并启动新动画。上面示例的完整实现可以在此处找到。
进一步阅读
有关此处讨论的API的更多信息,请参见指南、API参考文档和示例:
https://developer.android.com/jetpack/compose/handling-interaction
https://developer.android.com/reference/kotlin/androidx/compose/foundation/interaction/Interaction
“Ripple的源代码可在此处找到。”
https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/Ripple.kt