Kotlin Compose Button 实现长按监听并实现动画效果(完整代码)

2025-05-24 03:50

本文主要是介绍Kotlin Compose Button 实现长按监听并实现动画效果(完整代码),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

《KotlinComposeButton实现长按监听并实现动画效果(完整代码)》想要实现长按按钮开始录音,松开发送的功能,因此为了实现这些功能就需要自己写一个Button来解决问题,下面小编给大...

想要实现长按按钮开始录音,松开发送的功能。发现 Button 这个控件如果去监听这些按下,松开,长按等事件,发现是不会触发的,究其原因是 Button 已经提前消耗了这些事件所以导致,这些监听无法被触发。因此为了实现这些功能就需要自己写一个 Button 来解决问题。

Button 实现原理

在 Jetpack Compose 中,Button 是一个高度封装的可组合函数(Composable),其底层是由多个 UI 组件组合而成,关键组成包括:Surface、Text、Row、InteractionSource 等

源码

@Composable
fun Button(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    shape: Shape = ButtonDefaults.shape,
    colors: ButtonColors javascript= ButtonDefaults.buttonColors(),
    elevation: ButtonElevation? = ButtonDefaults.buttonElevation(),
    border: BorderStroke? = null,
    contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
    interactionSource: MutableInteractionSource? = null,
    content: @Composable RowScope.() -> Unit
) {
    @Suppress("NAME_SHADOWING")
    val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
    val containerColor = colors.containerColor(enabled)
    val contentColor = colors.contentColor(enabled)
    val shadowElevation = elevation?.shadowElevation(enabled, interactionSource)?.value ?: 0.dp
    Surface(
        onClick = onClick,
        modifier = modifier.semantics { role = Role.Button },
        enabled = enabled,
        shape = shape,
        color = containerColor,
        contentColor = contentColor,
        shadowElevation = shadowElevation,
        border = border,
        interactionSource = interactionSource
    ) {
        ProvideContentColorTextStyle(
            contentColor = contentColor,
            textStyle = MaterialTheme.typography.labelLarge
        ) {
            Row(
                Modifier.defaultMinSize(
                        minWidth = ButtonDefaults.MinWidth,
                        minHeight = ButtonDefaults.MinHeight
                    )
                    .padding(contentPadding),
                horizontalArrangement = Arrangement.Center,
                verticalAlignment = Alignment.CenterVertically,
                content = content
            )
        }
    }
}

1. Surface 的作用(关键)

Surface 是 Compose 中的通用容器,负责:

  • 提供背景颜色(来自 ButtonColors)
  • 提供 elevation(阴影)
  • 提供点击行为(通过 onClick)
  • 提供 shape(圆角、裁剪等)
  • 提供 ripple 效果(内部自动通过 indication 使用 rememberRipple())
  • 使用 Modifier.clickable 实现交互响应

注意:几乎所有 Material 组件都会使用 Surface 来包裹内容,统一管理视觉表现。

2. InteractionSource

  • InteractionSource 是 Compose 中管理用户交互状态的机制(如 pressedhoveredfocused
  • Button 将其传入 Surface,用于响应和处理 ripple 动画等
  • MutableInteractionSource 配合,可以观察组件的状态变化

3. ButtonDefaults

ButtonDefaults 是提供默认值的工具类,包含:

  • elevation():返回 ButtonElevation 对象,用于设置不同状态下的阴影高度
  • buttonColors():返回 ButtonColors 对象,用于设置正常 / 禁用状态下的背景与文字颜色
  • ContentPadding:内容的默认内边距 4. Content Slot(RowScope.() -> Unit

4. Content Slot(RowScope.() -> Unit)

Buttoncontent 是一个 RowScope 的 lambda,允许你自由组合子组件,如:

Button(onClick = { }) {
    Icon(imageVector = Icons.Default.Add, contentDescription = null)
    Spacer(modifier = Modifier.width(4.dp))
    Text("添加")
}

因为是在 RowScope 中,所以能用 Spacer 等布局函数在水平排列子项。

关键点说明
Surface提供背景、阴影、圆角、点击、ripple 效果的统一封装
InteractionSource用于收集用户交互状态(点击、悬停等)
ButtonDefaults提供默认颜色、阴影、Padding 等参数
Row + Text内容布局,允许图标 + 文本灵活组合
Modifier控制尺寸、形状、边距、点击响应等

如果想自定义 Button 的样式,也可以直接使用 Surface + Row 自己实现一个“按钮”,只需照着官方的做法组装即可。

@Suppress("DEPRECATION_ERROR")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Button(
    onClick: () -> Unit = {},
    onLongPress: () -> Unit = {},
    onPressed: () -> Unit = {},
    onReleased: () -> Unit = {},
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    shape: Shape = ButtonDefaults.shape,
    colors: ButtonColors = ButtonDefaults.buttonColors(),
    border: BorderStroke? = null,
    shadowElevation: Dp = 0.dp,
    contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
    content: @Composable RowScope.() -> Unit = { Text("LongButton") }
) {
    val containerColor = colors.containerColor
    val contentColor = colors.contentColor
    Surface(
        modifier = modifier
            .minimumInteractiveComponentSize()
            .pointerInput(enabled) {
                detectTapGestures(
                    onPress = { offset ->
                        onPressed()
                        tryAwaitRelease()
                        onReleased()
                    },
                    onTap = { onClick() },
                    onLongPress = { onLongPress() }
                )
            }
            .semantics { role = Role.Button },
        shape = shape,
        color = containerColor,
        contentColor = contentColor,
        shadowElevation = shadowElevation,
        border = border,
    ) {
        CompositionLocalProvider(
            LocalContentColor provides contentColor,
            LocalTextStyle provides LocalTextStyle.current.merge(MaterialTheme.typography.labelLarge),
        ) {
            Row(
                Modifier
                    .defaultMinSize(ButtonDefaults.MinWidth, ButtonDefaults.MinHeight)
                    .padding(contentPadding),
                horizontalArrangement = Arrangement.Center,
                verticalAlignment = Alignment.CenterVertically,
                content = content
            )
        }
    }
}

Button 的动画实现

为了让按钮在按下时提供自然的视觉反馈,Compose 通常会使用状态驱动的动画。最常见的方式是通过 animateColorAsState 来实现颜色的平滑过渡,比如按钮被按下时背景色或文字颜色稍微变暗,松开时再恢复。

这个动画实现的关键点在于:

  • 交互状态:比如是否按下、是否禁用,可以通过 InteractionSource 结合 collectIsPressedAsState() 实时监听当前状态。
  • 根据状态决定目标颜色:当状态变化时(如按下 -> 松开),我们会设置新的目标颜色。
  • 使用动画驱动状态变化:通过 animateColorAsState() 把颜色变化变成带过渡效果的状态变化,而不是突变。

这种方式符合 Compose 的声明式编程模型,不需要手动写动画过程,而是让状态驱动 UI 动画。

下面是按钮颜色动画部分的代码片段,只展示相关的状态监听和动画逻辑,具体如何应用在 UI 中将在后续实现:

@Composable
fun AnimatedButtonColors(
    enabled: Boolean,
    interactionSource: InteractionSource,
    defaultContainerColor: Color,
    pressedContainerColor: Color,
    disabledContainerColor: Color
): State<Color> {
    val isPressed by interactionSource.collectIsPressedAsState()
    val targetColor = when {
        !enabled -> disabledContainerColor
        isPressed -> pressedContainerColor
        else -> defaultContainerColor
    }
    // 返回一个状态驱动的动画颜色
    val animatedColor by animateColorAsState(targetColor, label = "containerColorAnimation")
    return rememberUpdatedState(animatedColor)
}

值得一提的是,Button 使用的动画类型为 ripple (涟漪效果)

这段代码仅负责计算当前的按钮背景色,并通过动画使其平滑过渡。它不会直接控制按钮的点击或布局逻辑,而是为最终的 UI 提供一个可动画的颜色状态。

后续可以将这个 animatedColor 应用于 Surface 或背景 Modifier 上,完成整体的按钮外观动画。

完整动画代码

// 1. 确保 interactionSource 不为空
val interaction = interactionSource ?: remember { MutableInteractionSource() }
// 2. 监听按下状态
val isPressed by interaction.collectIsPressedAsState()
// 4. 按状态选 target 值
val defaultContainerColor = colors.containerColor
val disabledContainerColor = colors.disabledContainerColor
val defaultContentColor = colors.contentColor
val disabledContentColor = colors.disabledContentColor
val targetContainerColor = when {
    !enabled -> disabledContainerColor
    isPressed -> defaultContainerColor.copy(alpha = 0.85f)
    else -> defaultContainerColor
}
val targetContentColor = when {
    !enabled -> disabledContentColor
    isPressed -> defaultContentColor.copy(alpha = 0.9f)
    else -> defaultContentColor
}
// 5. 动画
val containerColorAni by animateColorAsState(targetContainerColor, label = "containerColor")
val contentColorAni by animateColorAsState(targetContentColor, label = "contentColor")
// 涟漪效果
// 根据当前环境选择是否使用新版 Material3 的 ripple(),还是退回到老版的 rememberRipple() 实现
val ripple = if (LocalUseFallbackRippleImplementation.current) {
    rememberRipple(true, Dp.Unspecified, Color.Unspecified)
} else {
    ripple(true, Dp.Unspecified, Color.Unspecified)
}
// 6. Surface + 手动发 PressInteraction
Surface(
    modifier = modifier
        .minimumInteractiveComponentSize()
        .pointerInput(enabled) {
            detectTapGestures(
                onPress = { offset ->
                    // 发起 PressInteraction,供 collectIsPressedAsState 监听
                    val press = PressInteraction.Press(offset)
                    val scope = CoroutineScope(coroutineContext)
                    scope.launch {
                        interaction.emit(press)
                    }
                    // 用户 onPressed
                    onPressed()
                    // 等待手指抬起或取消
                    tryAwaitRelease()
                    // 发 ReleaseInteraction
                    scope.launch {
                        interaction.emit(PressInteraction.Release(press))
                    }
                    // 用户 onReleased
                    onReleased()
                },
                onTap = { onClick() },
                onLongPress = { onLongPress() }
            )
        }
        .indication(interaction, ripple)
        .semantics { role = Role.Button },
    shape = shape,
    color = containerColorAni,
    contentColor = contentColorAni,
    shadowElevation = shadowElevation,
    border = border,
) {...}

这个 Button 的动画部分主要体现在按下状态下的颜色过渡。它通过 animateColorAsState 来实现背景色和文字颜色的动态变化。

当按钮被按下时,会使用 interaction.collectIsPressedAsState() 实时监听是否处于 Pressed 状态,进而动态计算目标颜色(targetContainerColortargetContentColor)。按下状态下颜色会降低透明度(背景 alpha = 0.85,文字 alpha = 0.9),形成按压视觉反馈。

颜色的渐变不是突变的,而是带有过渡动画,由 animateColorAsState 自动驱动。它会在目标颜色发生变化时,通过内部的动画插值器平滑过渡到目标值,用户无需手动控制动画过程。

使用 by animateColorAsState(...) 得到的是 State<Color> 类型的值,它会在颜色变化时自动重组,使整个按钮在交互中呈现更自然的过渡效果。

这种方式相比传统手动实现动画更简洁、声明性更强,也更容易和 Compose 的状态系统集成。

完整代码

// androidx.compose.material3: 1.3.0
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.indication
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.riChina编程pple.rememberRipple
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.LocalUseFallbackRippleImplementation
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphpphics.Shape
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlin.coroutines.coroutineContext
@Suppress("DEPRECATION_ERROR")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Button(
    onClick: () -> Unit = {},
    onLongPress: () -> Unit = {},
    onPressed: () -> Unit = {},
    onReleased: () -> Unit = {},
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    shape: Shape = ButtonDefaults.shape,
    colpythonors: ButtonColors = ButtonDefaults.buttonColors(),
    border: BorderStroke? = null,
    shadowElevation: Dp = 0.dp,
    contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
    interactionSource: MutableInteractionSource? = null,
    content: @Composable RowScope.() -> Unit = { Text("LongButton") }
) {
    // 1. 确保 interactionSource 不为空
    val interaction = interactionSource ?: remember { MutableInteractionSource() }
    // 2. 监听按下状态
    val isPressed by interaction.collectIsPressedAsState()
    // 4. 按状态选 target 值
    val defaultContainerColor = colors.containerColor
    val disabledContainerColor = colors.disabledContainerColor
    val defaultContentColor = colors.contentColor
    val disabledContentColor = colors.disabledContentColor
    val targetContainerColor = when {
        !enabled -> disabledContainerColor
        isPressed -> defaultContainerColor.copy(alpha = 0.85f)
        else -> defaultContainerColor
    }
    val targetContentColor = when {
        !enabled -> disabledContentColor
        isPressed -> defaultContentColor.copy(alpha = 0.9f)
        else -> defaultContentColor
    }
    // 5. 动画
    val containerColorAni by animateColorAsState(targetContainerColor, label = "containerColor")
    val contentColorAni by animateColorAsState(targetContentColor, label = "contentColor")
    // 涟漪效果
    // 根据当前环境选择是否使用新版 Material3 的 ripple(),还是退回到老版的 rememberRipple() 实现
    val ripple = if (LocalUseFallbackRippleImplementation.current) {
        rememberRipple(true, Dp.Unspecified, Color.Unspecified)
    } else {
        ripple(true, Dp.Unspecified, Color.Unspecified)
    }
    // 6. Surface + 手动发 PressInteraction
    Surface(
        modifier = modifier
            .minimumInteractiveComponentSize()
            .pointerInput(enabled) {
                detectTapGestures(
                    onPress = { offset ->
                        // 发起 PressInteraction,供 collectIsPressedAsState 监听
                        val press = PressInteraction.Press(offset)
                        val scope = CoroutineScope(coroutineContext)
                        scope.launch {
                            interaction.emit(press)
                        }
                        // 用户 onPressed
                        onPressed()
                        // 等待手指抬起或取消
                        tryAwaitRelease()
                        // 发 ReleaseInteraction
                        scope.launch {
                            interaction.emit(PressInteraction.Release(press))
                        }
                        // 用户 onReleased
                        onReleased()
                    },
                    onTap = { onClick() },
                    onLongPress = { onLongPress() }
                )
            }
            .indication(interaction, ripple)
            .semantics { role = Role.Button },
        shape = shape,
        color = containerColorAni,
        contentColor = contentColorAni,
        shadowElevation = shadowElevation,
        border = border,
    ) {
        CompositionLocalProvider(
            LocalContentColor provides contentColorAni,
            LocalTextStyle provides LocalTextStyle.current.merge(MaterialTheme.typography.labelLarge),
        ) {
            Row(编程
                Modifier
                    .defaultMinSize(ButtonDefaults.MinWidth, ButtonDefaults.MinHeight)
                    .padding(contentPadding),
                horizontalArrangement = Arrangement.Center,
                verticalAlignment = Alignment.CenterVertically,
                content = content
            )
        }
    }
}

到此这篇关于Kotlin Compose Button 实现长按监听并实现动画效果的文章就介绍到这了,更多相关Kotlin Compose Button长按监听内容请搜索China编程(www.chinasem.cn)以前的文章或继续浏览下面的相关文章希望大家以后多多支持China编程(www.chinasem.cn)!

这篇关于Kotlin Compose Button 实现长按监听并实现动画效果(完整代码)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1154757

相关文章

Linux下删除乱码文件和目录的实现方式

《Linux下删除乱码文件和目录的实现方式》:本文主要介绍Linux下删除乱码文件和目录的实现方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录linux下删除乱码文件和目录方法1方法2总结Linux下删除乱码文件和目录方法1使用ls -i命令找到文件或目录

SpringBoot+EasyExcel实现自定义复杂样式导入导出

《SpringBoot+EasyExcel实现自定义复杂样式导入导出》这篇文章主要为大家详细介绍了SpringBoot如何结果EasyExcel实现自定义复杂样式导入导出功能,文中的示例代码讲解详细,... 目录安装处理自定义导出复杂场景1、列不固定,动态列2、动态下拉3、自定义锁定行/列,添加密码4、合并

mybatis执行insert返回id实现详解

《mybatis执行insert返回id实现详解》MyBatis插入操作默认返回受影响行数,需通过useGeneratedKeys+keyProperty或selectKey获取主键ID,确保主键为自... 目录 两种方式获取自增 ID:1. ​​useGeneratedKeys+keyProperty(推

Spring Boot集成Druid实现数据源管理与监控的详细步骤

《SpringBoot集成Druid实现数据源管理与监控的详细步骤》本文介绍如何在SpringBoot项目中集成Druid数据库连接池,包括环境搭建、Maven依赖配置、SpringBoot配置文件... 目录1. 引言1.1 环境准备1.2 Druid介绍2. 配置Druid连接池3. 查看Druid监控

Linux在线解压jar包的实现方式

《Linux在线解压jar包的实现方式》:本文主要介绍Linux在线解压jar包的实现方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录linux在线解压jar包解压 jar包的步骤总结Linux在线解压jar包在 Centos 中解压 jar 包可以使用 u

创建Java keystore文件的完整指南及详细步骤

《创建Javakeystore文件的完整指南及详细步骤》本文详解Java中keystore的创建与配置,涵盖私钥管理、自签名与CA证书生成、SSL/TLS应用,强调安全存储及验证机制,确保通信加密和... 目录1. 秘密键(私钥)的理解与管理私钥的定义与重要性私钥的管理策略私钥的生成与存储2. 证书的创建与

Android kotlin中 Channel 和 Flow 的区别和选择使用场景分析

《Androidkotlin中Channel和Flow的区别和选择使用场景分析》Kotlin协程中,Flow是冷数据流,按需触发,适合响应式数据处理;Channel是热数据流,持续发送,支持... 目录一、基本概念界定FlowChannel二、核心特性对比数据生产触发条件生产与消费的关系背压处理机制生命周期

c++ 类成员变量默认初始值的实现

《c++类成员变量默认初始值的实现》本文主要介绍了c++类成员变量默认初始值,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录C++类成员变量初始化c++类的变量的初始化在C++中,如果使用类成员变量时未给定其初始值,那么它将被

Qt使用QSqlDatabase连接MySQL实现增删改查功能

《Qt使用QSqlDatabase连接MySQL实现增删改查功能》这篇文章主要为大家详细介绍了Qt如何使用QSqlDatabase连接MySQL实现增删改查功能,文中的示例代码讲解详细,感兴趣的小伙伴... 目录一、创建数据表二、连接mysql数据库三、封装成一个完整的轻量级 ORM 风格类3.1 表结构

基于Python实现一个图片拆分工具

《基于Python实现一个图片拆分工具》这篇文章主要为大家详细介绍了如何基于Python实现一个图片拆分工具,可以根据需要的行数和列数进行拆分,感兴趣的小伙伴可以跟随小编一起学习一下... 简单介绍先自己选择输入的图片,默认是输出到项目文件夹中,可以自己选择其他的文件夹,选择需要拆分的行数和列数,可以通过