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")
    }
}
相关推荐
AFinalStone3 小时前
Android 7系统网络(四)Native层(下)—netd Controller详解
android·网络
爱笑鱼3 小时前
Handler(一):post 之后,Runnable 到底在哪个线程执行?
android
你挚爱的强哥3 小时前
Vue2 实现 1.5s 十连击监听(连续点击若干次),封装通用可复用点击检测工具,不用 data!Vue 封装通用连击监听方法,支持自定义时长与点击次数
前端·javascript·vue.js
木木子224 小时前
[特殊字符] 音乐播放器——状态驱动的多媒体控制
android·开发语言·华为·php·harmonyos
雨白4 小时前
C 语言基础:结构体、联合体与枚举
android
卓怡学长5 小时前
w266基于spring boot + vue 圣地延安美食乐享系统
java·数据库·vue.js·spring boot·spring·intellij-idea
我有满天星辰5 小时前
Vue项目脚手架之争:create-vue与Vue CLI全面对比
前端·javascript·vue.js
阿巴斯甜6 小时前
Android 代码混淆
android
Random_index7 小时前
#Vue3篇: Vue 项目发版后如何提示用户刷新?基于 package.json 版本号的前端更新检测方案
前端·vue.js·json
apihz8 小时前
台风实时与历史详情查询免费 API 接口完整教程
android·开发语言·tcp/ip·dubbo·台风·天气预报