Android中仿照View selector自定义Compose Button

一、背景

在使用Compose进行UI开发的过程中,UI设计师要求按钮具有按下效果,当手指触摸到按钮就改变背景颜色。Compose中Button自带的效果为涟漪效果,可以改动涟漪颜色和范围,但都打不到UI老师要求的传统View selector效果。也尝试用使用拦截indication的方式collectIsPressedAsState收集按钮按下效果,结果只有按下停顿的情况下会触发,快速按下抬起手指并不能触发效果,也是达不到预期。后来想到使用pointerInput进行手势的监听,处理onPress事件,果然解决了这个问题。后来经过网络上各种资料,重写了HButton组件以供后续其他项目使用。

二、自定义

自定义HButton

复制代码
package cn.aihongbo.dev.demo_tablayout.ui.widget

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.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle
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.graphics.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

/**
 * Created by ZhaoHongBo on 2026/1/9.
 * Copyright (c) 2026 ZHB. All rights reserved.
 */
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HButton(
    onClick: () -> Unit = {},
    onLongPress: () -> Unit = {},
    onPressed: () -> Unit = {},
    onReleased: () -> Unit = {},
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    enableRipple: Boolean = false,
    shape: Shape = ButtonDefaults.shape,
    colors: HButtonColors = HButtonColors.defaultColors(),
    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 pressedContainerColor = colors.pressedContainerColor
    val defaultContentColor = colors.contentColor
    val disabledContentColor = colors.disabledContentColor
    val pressedContentColor = colors.disabledContentColor

    val targetContainerColor = when {
        !enabled -> disabledContainerColor
        isPressed -> pressedContainerColor
        else -> defaultContainerColor
    }
    val targetContentColor = when {
        !enabled -> disabledContentColor
        isPressed -> pressedContentColor
        else -> defaultContentColor
    }

    // 5. 动画
    val containerColorAni by animateColorAsState(targetContainerColor, label = "containerColor")
    val contentColorAni by animateColorAsState(targetContentColor, label = "contentColor")

    // 涟漪效果
    val ripple = 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, if (enableRipple) ripple else null)
            .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
            )
        }
    }
}

class HButtonColors(
    val containerColor: Color,
    val contentColor: Color,
    val disabledContainerColor: Color,
    val disabledContentColor: Color,
    val pressedContainerColor: Color,
    val pressedContentColor: Color,
) {
    companion object {
        fun defaultColors(
            containerColor: Color = Color(0xFFFF6435),
            contentColor: Color = Color.White,
            disabledContainerColor: Color = Color.LightGray,
            disabledContentColor: Color = disabledContainerColor.copy(alpha = 0.85f),
            pressedContainerColor: Color = containerColor.copy(alpha = 0.7f),
            pressedContentColor: Color = Color.Gray
        ) = HButtonColors(
            containerColor = containerColor,
            contentColor = contentColor,
            disabledContainerColor = disabledContainerColor,
            disabledContentColor = disabledContentColor,
            pressedContainerColor = pressedContainerColor,
            pressedContentColor = pressedContentColor
        )
    }
}

使用自定义HButtton

复制代码
@Composable
fun MainFourContent() {
    HButton(
        colors = HButtonColors.defaultColors(), onClick = {
            Log.d("====>", "点击${System.currentTimeMillis()}")
        }) {
        Text("Click Me")
    }
}
相关推荐
大纽耶耶1 小时前
我把 YOLO 训练做成了网页版:需求设计与技术选型
vue.js
大纽耶耶1 小时前
开发 YOLO 训练管理平台的 23 个实践教训
vue.js
Maynor在掘金1 小时前
1.9 万 Star 的 Hypit:让 AI Agent 帮你“一键复刻”爆款视频,保姆级上手教程
vue.js·github
ym hyd 1111 小时前
门诊挂号系统源码 Java+SpringBoot+Vue3 前后分离
java·vue.js·spring boot·毕设
斯内普吖2 小时前
(开源)宠物饲养系统实战指南 基于 Java + SSM + Vue + MySQL
java·vue.js·mysql·开源·宠物
传奇开心果编程2 小时前
【Compose Multiplatform 跨端开发学与练】第8课 资源管理与主题
android·windows·学习·ios·kotlin·web·composer
可乐鸡翅yeah_3 小时前
新手梳理:HLS 线上问题,哪些该提给 CDN,哪些该找后端切片服务
开发语言·前端·javascript·vue.js·网络协议·http·m3u8在线
Q26433650233 小时前
【有源码】基于SpringBoot+Vue的博物馆展览与服务一体化平台-java博物馆数字化展品展示与交互平台设计
java·vue.js·spring boot·spring·毕业设计·javaweb·课程设计
传奇开心果编程3 小时前
【Compose Multiplatform 跨端开发学与练】第9课 测试与调试
android·学习·macos·ios·kotlin·web·composer
ym hyd 1114 小时前
汽车零部件缺陷管理系统源码 Java+SpringBoot+Vue3 前后分离
java·vue.js·spring boot·汽车·毕设