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")
    }
}
相关推荐
镜宇秋霖丶8 小时前
2026.5.6@霖宇博客制作中遇见的问题
前端·javascript·vue.js
计算机专业码农一枚8 小时前
微信小程序 uniapp+vue高校社团管理
vue.js·微信小程序·uni-app
小李子呢02119 小时前
前端八股Vue---Vue-router路由管理器
前端·javascript·vue.js
荣月灵的小梅花13 小时前
在Android 9上修改build.fingerprint
android
帅次14 小时前
Compose 入门:@Composable、组合与重组
android·kotlin·gradle·android jetpack·compose·composable
洞见前行14 小时前
APK Signing Block V2 多渠道分包技术原理
android
Momo__14 小时前
Vue 3.6 Vapor Mode:跳过虚拟 DOM,性能极致优化
前端·vue.js
DandelionR14 小时前
Android SDK安装
android
雪铃儿15 小时前
Flutter Android 热更新:我为什么没用 Shorebird 而是自己造了一个🚀
android·开源
walking95715 小时前
重新学习前端之JavaScript
前端·vue.js·面试