最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Kotlin Compose Button 實(shí)現(xiàn)長按監(jiān)聽并實(shí)現(xiàn)動(dòng)畫效果(完整代碼)

 更新時(shí)間:2025年05月23日 15:06:25   作者:zimoyin  
想要實(shí)現(xiàn)長按按鈕開始錄音,松開發(fā)送的功能,因此為了實(shí)現(xiàn)這些功能就需要自己寫一個(gè) Button 來解決問題,下面小編給大家分享Kotlin Compose Button 實(shí)現(xiàn)長按監(jiān)聽并實(shí)現(xiàn)動(dòng)畫效果(完整代碼),感興趣的朋友一起看看吧

想要實(shí)現(xiàn)長按按鈕開始錄音,松開發(fā)送的功能。發(fā)現(xiàn) Button 這個(gè)控件如果去監(jiān)聽這些按下,松開,長按等事件,發(fā)現(xiàn)是不會(huì)觸發(fā)的,究其原因是 Button 已經(jīng)提前消耗了這些事件所以導(dǎo)致,這些監(jiān)聽無法被觸發(fā)。因此為了實(shí)現(xiàn)這些功能就需要自己寫一個(gè) Button 來解決問題。

Button 實(shí)現(xiàn)原理

在 Jetpack Compose 中,Button 是一個(gè)高度封裝的可組合函數(shù)(Composable),其底層是由多個(gè) UI 組件組合而成,關(guān)鍵組成包括:Surface、Text、Row、InteractionSource 等

源碼

@Composable
fun Button(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    shape: Shape = ButtonDefaults.shape,
    colors: ButtonColors = ButtonDefaults.buttonColors(),
    elevation: ButtonElevation? = ButtonDefaults.buttonElevation(),
    border: BorderStroke? = null,
    contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
    interactionSource: MutableInteractionSource? = null,
    content: @Composable RowScope.() -> Unit
) {
    @Suppress("NAME_SHADOWING")
    val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
    val containerColor = colors.containerColor(enabled)
    val contentColor = colors.contentColor(enabled)
    val shadowElevation = elevation?.shadowElevation(enabled, interactionSource)?.value ?: 0.dp
    Surface(
        onClick = onClick,
        modifier = modifier.semantics { role = Role.Button },
        enabled = enabled,
        shape = shape,
        color = containerColor,
        contentColor = contentColor,
        shadowElevation = shadowElevation,
        border = border,
        interactionSource = interactionSource
    ) {
        ProvideContentColorTextStyle(
            contentColor = contentColor,
            textStyle = MaterialTheme.typography.labelLarge
        ) {
            Row(
                Modifier.defaultMinSize(
                        minWidth = ButtonDefaults.MinWidth,
                        minHeight = ButtonDefaults.MinHeight
                    )
                    .padding(contentPadding),
                horizontalArrangement = Arrangement.Center,
                verticalAlignment = Alignment.CenterVertically,
                content = content
            )
        }
    }
}

1. Surface 的作用(關(guān)鍵)

Surface 是 Compose 中的通用容器,負(fù)責(zé):

  • 提供背景顏色(來自 ButtonColors)
  • 提供 elevation(陰影)
  • 提供點(diǎn)擊行為(通過 onClick)
  • 提供 shape(圓角、裁剪等)
  • 提供 ripple 效果(內(nèi)部自動(dòng)通過 indication 使用 rememberRipple())
  • 使用 Modifier.clickable 實(shí)現(xiàn)交互響應(yīng)

注意:幾乎所有 Material 組件都會(huì)使用 Surface 來包裹內(nèi)容,統(tǒng)一管理視覺表現(xiàn)。

2. InteractionSource

  • InteractionSource 是 Compose 中管理用戶交互狀態(tài)的機(jī)制(如 pressed、hovered、focused
  • Button 將其傳入 Surface,用于響應(yīng)和處理 ripple 動(dòng)畫等
  • MutableInteractionSource 配合,可以觀察組件的狀態(tài)變化

3. ButtonDefaults

ButtonDefaults 是提供默認(rèn)值的工具類,包含:

  • elevation():返回 ButtonElevation 對(duì)象,用于設(shè)置不同狀態(tài)下的陰影高度
  • buttonColors():返回 ButtonColors 對(duì)象,用于設(shè)置正常 / 禁用狀態(tài)下的背景與文字顏色
  • ContentPadding:內(nèi)容的默認(rèn)內(nèi)邊距 4. Content Slot(RowScope.() -> Unit

4. Content Slot(RowScope.() -> Unit)

Buttoncontent 是一個(gè) RowScope 的 lambda,允許你自由組合子組件,如:

Button(onClick = { }) {
    Icon(imageVector = Icons.Default.Add, contentDescription = null)
    Spacer(modifier = Modifier.width(4.dp))
    Text("添加")
}

因?yàn)槭窃?RowScope 中,所以能用 Spacer 等布局函數(shù)在水平排列子項(xiàng)。

關(guān)鍵點(diǎn)說明
Surface提供背景、陰影、圓角、點(diǎn)擊、ripple 效果的統(tǒng)一封裝
InteractionSource用于收集用戶交互狀態(tài)(點(diǎn)擊、懸停等)
ButtonDefaults提供默認(rèn)顏色、陰影、Padding 等參數(shù)
Row + Text內(nèi)容布局,允許圖標(biāo) + 文本靈活組合
Modifier控制尺寸、形狀、邊距、點(diǎn)擊響應(yīng)等

如果想自定義 Button 的樣式,也可以直接使用 Surface + Row 自己實(shí)現(xiàn)一個(gè)“按鈕”,只需照著官方的做法組裝即可。

@Suppress("DEPRECATION_ERROR")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Button(
    onClick: () -> Unit = {},
    onLongPress: () -> Unit = {},
    onPressed: () -> Unit = {},
    onReleased: () -> Unit = {},
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    shape: Shape = ButtonDefaults.shape,
    colors: ButtonColors = ButtonDefaults.buttonColors(),
    border: BorderStroke? = null,
    shadowElevation: Dp = 0.dp,
    contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
    content: @Composable RowScope.() -> Unit = { Text("LongButton") }
) {
    val containerColor = colors.containerColor
    val contentColor = colors.contentColor
    Surface(
        modifier = modifier
            .minimumInteractiveComponentSize()
            .pointerInput(enabled) {
                detectTapGestures(
                    onPress = { offset ->
                        onPressed()
                        tryAwaitRelease()
                        onReleased()
                    },
                    onTap = { onClick() },
                    onLongPress = { onLongPress() }
                )
            }
            .semantics { role = Role.Button },
        shape = shape,
        color = containerColor,
        contentColor = contentColor,
        shadowElevation = shadowElevation,
        border = border,
    ) {
        CompositionLocalProvider(
            LocalContentColor provides contentColor,
            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
            )
        }
    }
}

Button 的動(dòng)畫實(shí)現(xiàn)

為了讓按鈕在按下時(shí)提供自然的視覺反饋,Compose 通常會(huì)使用狀態(tài)驅(qū)動(dòng)的動(dòng)畫。最常見的方式是通過 animateColorAsState 來實(shí)現(xiàn)顏色的平滑過渡,比如按鈕被按下時(shí)背景色或文字顏色稍微變暗,松開時(shí)再恢復(fù)。

這個(gè)動(dòng)畫實(shí)現(xiàn)的關(guān)鍵點(diǎn)在于:

  • 交互狀態(tài):比如是否按下、是否禁用,可以通過 InteractionSource 結(jié)合 collectIsPressedAsState() 實(shí)時(shí)監(jiān)聽當(dāng)前狀態(tài)。
  • 根據(jù)狀態(tài)決定目標(biāo)顏色:當(dāng)狀態(tài)變化時(shí)(如按下 -> 松開),我們會(huì)設(shè)置新的目標(biāo)顏色。
  • 使用動(dòng)畫驅(qū)動(dòng)狀態(tài)變化:通過 animateColorAsState() 把顏色變化變成帶過渡效果的狀態(tài)變化,而不是突變。

這種方式符合 Compose 的聲明式編程模型,不需要手動(dòng)寫動(dòng)畫過程,而是讓狀態(tài)驅(qū)動(dòng) UI 動(dòng)畫。

下面是按鈕顏色動(dòng)畫部分的代碼片段,只展示相關(guān)的狀態(tài)監(jiān)聽和動(dòng)畫邏輯,具體如何應(yīng)用在 UI 中將在后續(xù)實(shí)現(xiàn):

@Composable
fun AnimatedButtonColors(
    enabled: Boolean,
    interactionSource: InteractionSource,
    defaultContainerColor: Color,
    pressedContainerColor: Color,
    disabledContainerColor: Color
): State<Color> {
    val isPressed by interactionSource.collectIsPressedAsState()
    val targetColor = when {
        !enabled -> disabledContainerColor
        isPressed -> pressedContainerColor
        else -> defaultContainerColor
    }
    // 返回一個(gè)狀態(tài)驅(qū)動(dòng)的動(dòng)畫顏色
    val animatedColor by animateColorAsState(targetColor, label = "containerColorAnimation")
    return rememberUpdatedState(animatedColor)
}

值得一提的是,Button 使用的動(dòng)畫類型為 ripple (漣漪效果)

這段代碼僅負(fù)責(zé)計(jì)算當(dāng)前的按鈕背景色,并通過動(dòng)畫使其平滑過渡。它不會(huì)直接控制按鈕的點(diǎn)擊或布局邏輯,而是為最終的 UI 提供一個(gè)可動(dòng)畫的顏色狀態(tài)。

后續(xù)可以將這個(gè) animatedColor 應(yīng)用于 Surface 或背景 Modifier 上,完成整體的按鈕外觀動(dòng)畫。

完整動(dòng)畫代碼

// 1. 確保 interactionSource 不為空
val interaction = interactionSource ?: remember { MutableInteractionSource() }
// 2. 監(jiān)聽按下狀態(tài)
val isPressed by interaction.collectIsPressedAsState()
// 4. 按狀態(tài)選 target 值
val defaultContainerColor = colors.containerColor
val disabledContainerColor = colors.disabledContainerColor
val defaultContentColor = colors.contentColor
val disabledContentColor = colors.disabledContentColor
val targetContainerColor = when {
    !enabled -> disabledContainerColor
    isPressed -> defaultContainerColor.copy(alpha = 0.85f)
    else -> defaultContainerColor
}
val targetContentColor = when {
    !enabled -> disabledContentColor
    isPressed -> defaultContentColor.copy(alpha = 0.9f)
    else -> defaultContentColor
}
// 5. 動(dòng)畫
val containerColorAni by animateColorAsState(targetContainerColor, label = "containerColor")
val contentColorAni by animateColorAsState(targetContentColor, label = "contentColor")
// 漣漪效果
// 根據(jù)當(dāng)前環(huán)境選擇是否使用新版 Material3 的 ripple(),還是退回到老版的 rememberRipple() 實(shí)現(xiàn)
val ripple = if (LocalUseFallbackRippleImplementation.current) {
    rememberRipple(true, Dp.Unspecified, Color.Unspecified)
} else {
    ripple(true, Dp.Unspecified, Color.Unspecified)
}
// 6. Surface + 手動(dòng)發(fā) PressInteraction
Surface(
    modifier = modifier
        .minimumInteractiveComponentSize()
        .pointerInput(enabled) {
            detectTapGestures(
                onPress = { offset ->
                    // 發(fā)起 PressInteraction,供 collectIsPressedAsState 監(jiān)聽
                    val press = PressInteraction.Press(offset)
                    val scope = CoroutineScope(coroutineContext)
                    scope.launch {
                        interaction.emit(press)
                    }
                    // 用戶 onPressed
                    onPressed()
                    // 等待手指抬起或取消
                    tryAwaitRelease()
                    // 發(fā) ReleaseInteraction
                    scope.launch {
                        interaction.emit(PressInteraction.Release(press))
                    }
                    // 用戶 onReleased
                    onReleased()
                },
                onTap = { onClick() },
                onLongPress = { onLongPress() }
            )
        }
        .indication(interaction, ripple)
        .semantics { role = Role.Button },
    shape = shape,
    color = containerColorAni,
    contentColor = contentColorAni,
    shadowElevation = shadowElevation,
    border = border,
) {...}

這個(gè) Button 的動(dòng)畫部分主要體現(xiàn)在按下狀態(tài)下的顏色過渡。它通過 animateColorAsState 來實(shí)現(xiàn)背景色和文字顏色的動(dòng)態(tài)變化。

當(dāng)按鈕被按下時(shí),會(huì)使用 interaction.collectIsPressedAsState() 實(shí)時(shí)監(jiān)聽是否處于 Pressed 狀態(tài),進(jìn)而動(dòng)態(tài)計(jì)算目標(biāo)顏色(targetContainerColortargetContentColor)。按下狀態(tài)下顏色會(huì)降低透明度(背景 alpha = 0.85,文字 alpha = 0.9),形成按壓視覺反饋。

顏色的漸變不是突變的,而是帶有過渡動(dòng)畫,由 animateColorAsState 自動(dòng)驅(qū)動(dòng)。它會(huì)在目標(biāo)顏色發(fā)生變化時(shí),通過內(nèi)部的動(dòng)畫插值器平滑過渡到目標(biāo)值,用戶無需手動(dòng)控制動(dòng)畫過程。

使用 by animateColorAsState(...) 得到的是 State<Color> 類型的值,它會(huì)在顏色變化時(shí)自動(dòng)重組,使整個(gè)按鈕在交互中呈現(xiàn)更自然的過渡效果。

這種方式相比傳統(tǒng)手動(dòng)實(shí)現(xiàn)動(dòng)畫更簡潔、聲明性更強(qiáng),也更容易和 Compose 的狀態(tài)系統(tǒng)集成。

完整代碼

// androidx.compose.material3: 1.3.0
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.material.ripple.rememberRipple
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.LocalUseFallbackRippleImplementation
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
@Suppress("DEPRECATION_ERROR")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Button(
    onClick: () -> Unit = {},
    onLongPress: () -> Unit = {},
    onPressed: () -> Unit = {},
    onReleased: () -> Unit = {},
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    shape: Shape = ButtonDefaults.shape,
    colors: ButtonColors = ButtonDefaults.buttonColors(),
    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. 監(jiān)聽按下狀態(tài)
    val isPressed by interaction.collectIsPressedAsState()
    // 4. 按狀態(tài)選 target 值
    val defaultContainerColor = colors.containerColor
    val disabledContainerColor = colors.disabledContainerColor
    val defaultContentColor = colors.contentColor
    val disabledContentColor = colors.disabledContentColor
    val targetContainerColor = when {
        !enabled -> disabledContainerColor
        isPressed -> defaultContainerColor.copy(alpha = 0.85f)
        else -> defaultContainerColor
    }
    val targetContentColor = when {
        !enabled -> disabledContentColor
        isPressed -> defaultContentColor.copy(alpha = 0.9f)
        else -> defaultContentColor
    }
    // 5. 動(dòng)畫
    val containerColorAni by animateColorAsState(targetContainerColor, label = "containerColor")
    val contentColorAni by animateColorAsState(targetContentColor, label = "contentColor")
    // 漣漪效果
    // 根據(jù)當(dāng)前環(huán)境選擇是否使用新版 Material3 的 ripple(),還是退回到老版的 rememberRipple() 實(shí)現(xiàn)
    val ripple = if (LocalUseFallbackRippleImplementation.current) {
        rememberRipple(true, Dp.Unspecified, Color.Unspecified)
    } else {
        ripple(true, Dp.Unspecified, Color.Unspecified)
    }
    // 6. Surface + 手動(dòng)發(fā) PressInteraction
    Surface(
        modifier = modifier
            .minimumInteractiveComponentSize()
            .pointerInput(enabled) {
                detectTapGestures(
                    onPress = { offset ->
                        // 發(fā)起 PressInteraction,供 collectIsPressedAsState 監(jiān)聽
                        val press = PressInteraction.Press(offset)
                        val scope = CoroutineScope(coroutineContext)
                        scope.launch {
                            interaction.emit(press)
                        }
                        // 用戶 onPressed
                        onPressed()
                        // 等待手指抬起或取消
                        tryAwaitRelease()
                        // 發(fā) ReleaseInteraction
                        scope.launch {
                            interaction.emit(PressInteraction.Release(press))
                        }
                        // 用戶 onReleased
                        onReleased()
                    },
                    onTap = { onClick() },
                    onLongPress = { onLongPress() }
                )
            }
            .indication(interaction, ripple)
            .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
            )
        }
    }
}

到此這篇關(guān)于Kotlin Compose Button 實(shí)現(xiàn)長按監(jiān)聽并實(shí)現(xiàn)動(dòng)畫效果的文章就介紹到這了,更多相關(guān)Kotlin Compose Button長按監(jiān)聽內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評(píng)論

宁夏| 安福县| 武宁县| 三穗县| 沙坪坝区| 石嘴山市| 澜沧| 商丘市| 庐江县| 改则县| 资阳市| 高邮市| 景东| 安远县| 虎林市| 崇阳县| 禄丰县| 涞水县| 中宁县| 虎林市| 泰宁县| 老河口市| 白沙| 建始县| 花垣县| 乐至县| 玛多县| 闽清县| 耒阳市| 隆回县| 平果县| 新乐市| 常熟市| 遂昌县| 武义县| 兴文县| 武穴市| 于都县| 伊春市| 冀州市| 金塔县|