让你的AndroidUI更亮眼:Jetpack Compose中的可视状态

news2024/11/26 21:31:09

logo

让你的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被取消时发出(例如,当手指在未抬起的情况下移动到组件边界外部时)
    为了支持多个同时进行的相同类型的交互,例如当用户用多个手指触摸一个组件时进行多个按下操作,相应于事件“结束”的交互,即ReleaseCancel,包含对事件“开始”的引用,以便清楚地确定何时完成了交互。

交互的主要入口点是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 扩展而来的。这种分离与 SharedFlowMutableSharedFlowStateMutableStateListMutableList 等一致 — 它允许在 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.hoverableModifier.focusableModifier.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 / ExitFocusInteraction.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是比hoverablefocusable更高层次的抽象——要使组件可点击,它必须是隐式可悬停的,并且可点击的组件还应该是可聚焦的。通过使用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#collectIsFocusedAsStateInteractionSource#collectIsDraggedAsState,和InteractionSource#collectIsHoveredAsState 来观察其他互动。

虽然这些API是为了方便提供的,但实现很小并且非常适合在处理互动时了解通用模式。例如,假设你关心按钮是按下还是被拖动。虽然你可以使用collectIsPressedAsStatecollectIsDraggedAsState两个函数,但这会导致重复工作,而且会丢失细粒度信息(例如互动的顺序)——你可能只想关心最近的互动,而不是优先考虑其中一个。

为了实现这一点,你需要观察并跟踪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(本地指示器)——这是一种允许在层次结构中提供IndicationCompositionLocal。默认情况下,它由像clickable这样的修饰符使用,因此如果您正在构建新的可点击组件,它将自动使用应用程序中提供的Indication。如前所述,Material库在此使用涟漪作为默认提示。

要将此缩放效果转换为Indication,您首先需要创建负责应用缩放效果的IndicationInstanceIndicationInstance公开一个需要实现的函数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的示例相同-唯一的区别在于,您可以直接在实例中使用animateToPressedanimateToResting函数来动画显示比例效果,而不是将交互转换为状态。

// 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,你可以绘制任何类型的效果,无论是在内容上面还是下面。例如,在组件周围绘制动画边框以及在其被按下时在其上面叠加一个覆盖层。
在使用非ripple指示时要注意性能影响,如上所述

这里的指示实现与前面的示例非常类似 - 它只是创建实例并启动动画。由于动画边框取决于指示所用的组件的形状和边框,因此指示实现还需要提供形状和边框宽度作为参数。

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/627300.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

关于接口的安全性测试,这几点你必须掌握!

01、接口防刷 1.为什么会有人要刷接口&#xff1f; 牟利&#xff1a;黄牛在 12306 网上抢票再倒卖。 恶意攻击竞争对手&#xff1a;如短信接口被请求一次&#xff0c;会触发几分钱的运营商费用&#xff0c;当量级大了也很可观。 压测&#xff1a;用apache bench 做压力测试。…

高性能计算与AI融合成为刚需|什么是高性能计算?应用领域有哪些?与人工智能的关系梳理

本文一部分转载自杨净 整理自 MEET2023量子位 算力的需求&#xff0c;远比以往来得更为猛烈。甚至有人直呼&#xff1a;得算力者得未来。 元宇宙、AIGC、AI for Science的涌现&#xff0c;又给高性能计算&#xff08;HPC&#xff09;平添了好几把火。 在诸多挑战与机遇共存交…

大数据治理入门系列:数据管理

在如今的大数据时代&#xff0c;每天都会产生大量的新数据&#xff0c;已有数据可能也会频繁更新或转换。因此&#xff0c;需要对数据进行治理和管理&#xff0c;以便高效地开展数据分析、获取数据洞见、挖掘数据价值。否则&#xff0c;杂乱无章的数据只会白白浪费存储空间&…

python基础----10-----python操作mysql

一 前言 对于SQL章节前言->SQL_DQL_排序分页的课程&#xff0c;这里不做记录&#xff0c;因为都是讲MYSQL本身的内容&#xff0c;与python无关。 当然&#xff0c;接下来的课需要用到mysql&#xff0c;所以大家需要自行下载&#xff0c;这并不难。 二 python操作MYSQL基础…

当BPM遇上低代码 “自定义”提升业务管理效率

业务流程管理&#xff08;BPM&#xff09;的历史可以追溯到科学管理和质量管理的发展&#xff0c;并逐步演变为一个更加系统化和综合的管理方法。它在现代组织中起到了优化业务流程、提高效率和质量、增强灵活性和创新能力的重要作用。 从20世纪初的科学管理理论中&#xff0c…

【Linux】什么是进程(process)?

目录 进程与程序概念描述进程-PCBWindows进程与Linux进程Linux进程操作子进程父进程创建进程(初识-fork) 进程与程序概念 在Linux系统中&#xff0c;触发任何一个事件时&#xff0c;系统都会将它定义成为一个进程&#xff0c;并且给予这个进程一个ID&#xff0c;成为PID&#x…

Apikit 自学日记:如何安装 Apikit

Apikit 有三种客户端&#xff0c;你可以依据自己的情况选择。三种客户端的数据是共用的&#xff0c;因此你可以随时切换不同的客户端。 肯定会有和我一样的小白&#xff0c;第一次听说 Apikit这个工具&#xff0c;那么我今天和大家一起学习下这个工具如何安装。 我们推荐使用新…

校招失败后,在小公司熬了 2 年终于进了华为,这次真是竭尽全力了····

其实两年前校招的时候就往华为投了一次简历&#xff0c;结果很明显凉了&#xff0c;随后这个理想就被暂时放下了&#xff0c;但是这个种子一直埋在心里这两年除了工作以外&#xff0c;也会坚持写博客&#xff0c;也因此结识了很多优秀的小伙伴&#xff0c;从他们身上学到了特别…

Angular实现用ng-class和ng-style控制数组最后一个数据为红色

首先我们来看class方案实现 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document</title>…

Python模块的安装

1.用pip命令安装模块 pip是python官方的编程环境提供的一个命令&#xff0c;主要功能就是安装和卸载第三方模块。 用pip命令安装模块的方法最简单也最常用&#xff0c;这种默认将模块安装在python安装目录中的“site-packages"文件下。 1.1 pip命令安装模块的具体方法 …

MySQL的索引为什么要下推?

文章目录 一、回表操作 二、主键索引 三、非主键索引 四、低版本操作 五、高版本操作 六、总结 一、回表操作 对于数据库来说&#xff0c;只要涉及到索引&#xff0c;必然绕不过去回表操作。当然这也是我们今天所讲的内容的前提基础。说到回表&#xff0c;我们需要从索引…

HarmonyOS学习路之开发篇—Java UI框架(DirectionalLayout)

DirectionalLayout DirectionalLayout是Java UI中的一种重要组件布局&#xff0c;用于将一组组件(Component)按照水平或者垂直方向排布&#xff0c;能够方便地对齐布局内的组件。该布局和其他布局的组合&#xff0c;可以实现更加丰富的布局方式。 DirectionalLayout示意图 支持…

微信小程序实现瀑布流布局效果

一、效果 二、内容 1、xml代码 <!-- 列表 --><view class"list_title">推荐</view><view class"waterfall"><block wx:for"{{list}}" wx:key"index" wx:for-index"id" wx:for-item"itemNam…

cmd进入mysql及常用的mysql操作

cmd进入mysql操作 winR&#xff0c;输入cmd&#xff0c;打开cmd窗口&#xff0c;进入到 mysql bin目录的路径下 第一步&#xff1a;启动mysql服务&#xff0c;可以通过“net start myql”命令实现&#xff1b; 第二步&#xff1a;先使用DOS命令进入mysql的安装目录下的bin目…

ChatGPT 国内镜像网站独家汇总:发现最优秀的人工智能对话体验

欢迎来到我们的 ChatGPT 镜像网站汇总博客&#xff01;在这个令人激动的人工智能时代&#xff0c;ChatGPT 作为一款顶尖的语言模型&#xff0c;已经引起了全球范围内的热议。但是&#xff0c;您是否曾经为了找到最佳的 ChatGPT 使用体验而苦苦搜寻&#xff1f;别担心&#xff0…

程序员工作中的 80/20 法则如何体现?

80/20 原则的基础概念 该原则认为&#xff0c; 大部分效果出自少数起因。例如&#xff0c; 少数人得到大部分收入&#xff0c; 少数研究者做出大多数创新成果&#xff0c;少数作者写出大部分图书&#xff0c;等等。 您可能听说过 80/20 原则——它在个人生产力文献中随处可见…

国产文档控件Spire.Doc for.NET ,增强从 Word 到 PDF 和 HTML 的转换

Spire.Doc pack &#xff08;hotfix&#xff09; 11.5.12 增强了从 Word 到 PDF 和 HTML 的转换 很高兴地宣布发布 Spire.Doc 11.5.12。此版本增强了从 Word 到 PDF 和 HTML 的转换。此外&#xff0c;此版本还修复了一些已知问题&#xff0c;例如应用程序在加载文档时抛出“Sy…

django实现读取数据导出生成excel表格

目录 一、简单示例&#xff1a; 1.创建文件对象&#xff1a; 2.添加工作表&#xff1a; 3.写入数据&#xff1a; 二、实践出真理 需要先安装xlwt模块 pip install -i https://pypi.douban.com/simple xlwt一、简单示例&#xff1a; import xlwt# 创建一个Excel文件对象 …

教你该如何设计接口测试用例方法!

目录 前言&#xff1a; 一、用例设计过程&#xff1a; 二、接口测试用例构思结构&#xff1a; 三、测试过程验证点&#xff1a; 前言&#xff1a; 接口测试用例设计方法是进行接口测试的重要步骤。设计有效的测试用例可以帮助我们发现软件中可能存在的问题和缺陷&#xff…

有了IP地址,还需要MAC地址嘛?二选一可否?

概要 在计算机网络中&#xff0c;IP地址和MAC地址是两个最基本的概念。IP地址在互联网中是用于标识主机的逻辑地址&#xff0c;而MAC地址则是用于标识网卡的物理地址。虽然它们都是用于标识一个设备的地址&#xff0c;但是它们的作用和使用场景是不同的。 IP地址是在网络层&am…