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")
    }
}
相关推荐
NiceCloud喜云7 小时前
Opus 4.8 的 Effort Control 怎么选:Low 到 Max 五档策略
android·java·大数据·前端·c++·python·spring
日光明媚11 小时前
一步生成视频!One-Forcing:DMD + 零成本 GAN,训练 200 步超越多步 SOTA
android·开发语言·kotlin
帅次12 小时前
Android 17 开发者实战:核心更新与应用场景落地指南
android·java·ios·android studio·iphone·android jetpack·webview
大鹏说大话12 小时前
SQL 排序与分组实战:解决“分组后取最新数据“
android·java·数据库
xiaohua0708day13 小时前
Lodash库
前端·javascript·vue.js
万物皆对象66613 小时前
切换路由时页面空白问题(vue3)
前端·vue.js·typescript
李剑一15 小时前
小红书前端架构面试问的挺深入啊!面试官:Vue中组合式API与选项式API的设计权衡
vue.js·面试
搜狐技术产品小编202315 小时前
破局与重构:纯端侧 Android 自动化引擎的尝试与未来推演
android·运维·重构·自动化
码云骑士15 小时前
Android SystemServer启动过程
android·systemserver
一 乐15 小时前
汽车租赁|基于SprinBoot+vue的汽车租赁管理系统(源码+数据库+文档)
数据库·vue.js·spring boot·汽车·论文·毕设·汽车租赁管理系统