Kotlin Compose Button 實現(xiàn)長按監(jiān)聽并實現(xiàn)動畫效果(完整代碼)
想要實現(xiàn)長按按鈕開始錄音,松開發(fā)送的功能。發(fā)現(xiàn) Button 這個控件如果去監(jiān)聽這些按下,松開,長按等事件,發(fā)現(xiàn)是不會觸發(fā)的,究其原因是 Button 已經(jīng)提前消耗了這些事件所以導致,這些監(jiān)聽無法被觸發(fā)。因此為了實現(xiàn)這些功能就需要自己寫一個 Button 來解決問題。
Button 實現(xiàn)原理
在 Jetpack Compose 中,Button 是一個高度封裝的可組合函數(shù)(Composable),其底層是由多個 UI 組件組合而成,關鍵組成包括: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 的作用(關鍵)
Surface 是 Compose 中的通用容器,負責:
- 提供背景顏色(來自 ButtonColors)
- 提供 elevation(陰影)
- 提供點擊行為(通過 onClick)
- 提供 shape(圓角、裁剪等)
- 提供 ripple 效果(內部自動通過 indication 使用 rememberRipple())
- 使用 Modifier.clickable 實現(xiàn)交互響應
注意:幾乎所有 Material 組件都會使用 Surface 來包裹內容,統(tǒng)一管理視覺表現(xiàn)。
2. InteractionSource
InteractionSource是 Compose 中管理用戶交互狀態(tài)的機制(如pressed、hovered、focused)Button將其傳入Surface,用于響應和處理 ripple 動畫等- 與
MutableInteractionSource配合,可以觀察組件的狀態(tài)變化
3. ButtonDefaults
ButtonDefaults 是提供默認值的工具類,包含:
elevation():返回ButtonElevation對象,用于設置不同狀態(tài)下的陰影高度buttonColors():返回ButtonColors對象,用于設置正常 / 禁用狀態(tài)下的背景與文字顏色ContentPadding:內容的默認內邊距 4. Content Slot(RowScope.() -> Unit)
4. Content Slot(RowScope.() -> Unit)
Button 的 content 是一個 RowScope 的 lambda,允許你自由組合子組件,如:
Button(onClick = { }) {
Icon(imageVector = Icons.Default.Add, contentDescription = null)
Spacer(modifier = Modifier.width(4.dp))
Text("添加")
}因為是在 RowScope 中,所以能用 Spacer 等布局函數(shù)在水平排列子項。
| 關鍵點 | 說明 |
|---|---|
Surface | 提供背景、陰影、圓角、點擊、ripple 效果的統(tǒng)一封裝 |
InteractionSource | 用于收集用戶交互狀態(tài)(點擊、懸停等) |
ButtonDefaults | 提供默認顏色、陰影、Padding 等參數(shù) |
Row + Text | 內容布局,允許圖標 + 文本靈活組合 |
Modifier | 控制尺寸、形狀、邊距、點擊響應等 |
如果想自定義 Button 的樣式,也可以直接使用 Surface + Row 自己實現(xiàn)一個“按鈕”,只需照著官方的做法組裝即可。
@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 的動畫實現(xiàn)
為了讓按鈕在按下時提供自然的視覺反饋,Compose 通常會使用狀態(tài)驅動的動畫。最常見的方式是通過 animateColorAsState 來實現(xiàn)顏色的平滑過渡,比如按鈕被按下時背景色或文字顏色稍微變暗,松開時再恢復。
這個動畫實現(xiàn)的關鍵點在于:
- 交互狀態(tài):比如是否按下、是否禁用,可以通過
InteractionSource結合collectIsPressedAsState()實時監(jiān)聽當前狀態(tài)。 - 根據(jù)狀態(tài)決定目標顏色:當狀態(tài)變化時(如按下 -> 松開),我們會設置新的目標顏色。
- 使用動畫驅動狀態(tài)變化:通過
animateColorAsState()把顏色變化變成帶過渡效果的狀態(tài)變化,而不是突變。
這種方式符合 Compose 的聲明式編程模型,不需要手動寫動畫過程,而是讓狀態(tài)驅動 UI 動畫。
下面是按鈕顏色動畫部分的代碼片段,只展示相關的狀態(tài)監(jiān)聽和動畫邏輯,具體如何應用在 UI 中將在后續(xù)實現(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
}
// 返回一個狀態(tài)驅動的動畫顏色
val animatedColor by animateColorAsState(targetColor, label = "containerColorAnimation")
return rememberUpdatedState(animatedColor)
}值得一提的是,Button 使用的動畫類型為 ripple (漣漪效果)
這段代碼僅負責計算當前的按鈕背景色,并通過動畫使其平滑過渡。它不會直接控制按鈕的點擊或布局邏輯,而是為最終的 UI 提供一個可動畫的顏色狀態(tài)。
后續(xù)可以將這個 animatedColor 應用于 Surface 或背景 Modifier 上,完成整體的按鈕外觀動畫。
完整動畫代碼
// 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. 動畫
val containerColorAni by animateColorAsState(targetContainerColor, label = "containerColor")
val contentColorAni by animateColorAsState(targetContentColor, label = "contentColor")
// 漣漪效果
// 根據(jù)當前環(huán)境選擇是否使用新版 Material3 的 ripple(),還是退回到老版的 rememberRipple() 實現(xiàn)
val ripple = if (LocalUseFallbackRippleImplementation.current) {
rememberRipple(true, Dp.Unspecified, Color.Unspecified)
} else {
ripple(true, Dp.Unspecified, Color.Unspecified)
}
// 6. Surface + 手動發(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,
) {...}這個 Button 的動畫部分主要體現(xiàn)在按下狀態(tài)下的顏色過渡。它通過 animateColorAsState 來實現(xiàn)背景色和文字顏色的動態(tài)變化。
當按鈕被按下時,會使用 interaction.collectIsPressedAsState() 實時監(jiān)聽是否處于 Pressed 狀態(tài),進而動態(tài)計算目標顏色(targetContainerColor 和 targetContentColor)。按下狀態(tài)下顏色會降低透明度(背景 alpha = 0.85,文字 alpha = 0.9),形成按壓視覺反饋。
顏色的漸變不是突變的,而是帶有過渡動畫,由 animateColorAsState 自動驅動。它會在目標顏色發(fā)生變化時,通過內部的動畫插值器平滑過渡到目標值,用戶無需手動控制動畫過程。
使用 by animateColorAsState(...) 得到的是 State<Color> 類型的值,它會在顏色變化時自動重組,使整個按鈕在交互中呈現(xiàn)更自然的過渡效果。
這種方式相比傳統(tǒng)手動實現(xiàn)動畫更簡潔、聲明性更強,也更容易和 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. 動畫
val containerColorAni by animateColorAsState(targetContainerColor, label = "containerColor")
val contentColorAni by animateColorAsState(targetContentColor, label = "contentColor")
// 漣漪效果
// 根據(jù)當前環(huán)境選擇是否使用新版 Material3 的 ripple(),還是退回到老版的 rememberRipple() 實現(xiàn)
val ripple = if (LocalUseFallbackRippleImplementation.current) {
rememberRipple(true, Dp.Unspecified, Color.Unspecified)
} else {
ripple(true, Dp.Unspecified, Color.Unspecified)
}
// 6. Surface + 手動發(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
)
}
}
}到此這篇關于Kotlin Compose Button 實現(xiàn)長按監(jiān)聽并實現(xiàn)動畫效果的文章就介紹到這了,更多相關Kotlin Compose Button長按監(jiān)聽內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Android自定義EditText實現(xiàn)登錄界面
這篇文章主要為大家詳細介紹了Android自定義EditText實現(xiàn)登錄界面,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-12-12
DCloud的native.js調用系統(tǒng)分享實例Android版代碼
本文為大家分享了DCloud的native.js如何調用系統(tǒng)分享功能Android版的實例代碼,直接拿來就用2018-09-09
EditText監(jiān)聽方法,實時的判斷輸入多少字符
在EditText提供了一個方法addTextChangedListener實現(xiàn)對輸入文本的監(jiān)控。本文分享了EditText監(jiān)聽方法案例,需要的朋友一起來看下吧2016-12-12

