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")
    }
}
相关推荐
踩着两条虫10 分钟前
从设计稿到代码:VTJ.PRO 的 AI 集成系统架构解析
前端·vue.js·人工智能
恋猫de小郭2 小时前
Flutter 发布官方 Skills ,Flutter 在 AI 领域再添一助力
android·前端·flutter
codingWhat2 小时前
如何实现一个「万能」的通用打印组件?
前端·javascript·vue.js
徐小夕4 小时前
pxcharts Ultra V2.3更新:多维表一键导出 PDF,渲染兼容性拉满!
vue.js·算法·github
前端Hardy5 小时前
别再无脑用 `JSON.parse()` 了!这个安全漏洞你可能每天都在触发
前端·javascript·vue.js
前端Hardy5 小时前
别再让 `console.log` 上线了!它正在悄悄拖垮你的生产系统
前端·javascript·vue.js
青青家的小灰灰5 小时前
从入门到精通:Vue3 ref vs reactive 最佳实践与底层原理
前端·vue.js·面试
OpenTiny社区5 小时前
我的新同事是个AI:支持skill后,它用TinyVue搭项目还挺溜!
前端·vue.js·ai编程
李剑一5 小时前
大屏天气展示太普通?视觉升级!用 Canvas 做动态天气遮罩,雷阵雨效果直接封神
前端·vue.js·canvas
本末倒置1836 小时前
面向 Vue 开发者的 Next.js 快速入门指南
前端·vue.js