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