使用者介面元件可透過回應使用者互動的方式,向裝置使用者提供回饋。每個元件都有專屬的互動回應方式,可協助使用者瞭解互動情形。舉例來說,如果使用者在裝置的觸控螢幕上輕觸某個按鈕,該按鈕可能會以某種方式改變,例如加上醒目顯示顏色。這項變化可讓使用者知道自己已輕觸該按鈕。如果使用者並不是想執行這項操作,就會知道要先將手指從按鈕拖曳到其他地方再放開,否則會啟用該按鈕。
![](https://developer.android.google.cn/static/develop/ui/compose/images/touchinput/button-always-enabled.gif?authuser=3&hl=zh-tw)
![](https://developer.android.google.cn/static/develop/ui/compose/images/touchinput/button-reflects-state.gif?authuser=3&hl=zh-tw)
Compose 的手勢說明文件的內容涵蓋 Compose 元件如何處理低層級的指標事件,例如指標移動和點擊事件。Compose 預設會將這些低層級事件簡化為高層級互動,例如一系列指標事件可能會組成按下並放開按鈕的過程。瞭解這些較高層級的簡化作業後,您就可以自訂使用者介面回應使用者的方式。例如,您可以自訂使用者與元件互動時的元件外觀變化,或者保留使用者動作記錄。本文件將提供所需資訊,協助您修改標準 UI 元素或自行設計 UI 元素。
互動
在許多情況下,您並不需要瞭解 Compose 元件是如何解讀使用者互動。舉例來說,Button
要使用 Modifier.clickable
才能判斷使用者是否點選了按鈕。如要在應用程式中加入一般按鈕,您可以定義按鈕的 onClick
程式碼,而 Modifier.clickable
會視情況執行該程式碼。也就是說,您不必瞭解使用者是否輕觸螢幕或透過鍵盤選取按鈕。Modifier.clickable
會知道使用者執行了點選動作,並執行 onClick
程式碼以做出回應。
不過,如果您要自訂使用者介面元件對使用者行為的回應方式,可能得進一步瞭解背後的運作原理。本節將提供這方面的說明。
當使用者與使用者介面元件互動時,系統會產生多個 Interaction
事件來表示其行為。舉例來說,如果使用者輕觸某個按鈕,該按鈕就會產生 PressInteraction.Press
。如果使用者抬起手指在按鈕內,系統就會產生 PressInteraction.Release
,讓按鈕知道點擊已完成。另一方面,如果使用者將手指拖曳到按鈕外才放開,按鈕就會產生 PressInteraction.Cancel
,表示按下按鈕的動作已取消而並未完成。
這類互動無預設立場。也就是說,這些低層級互動事件不會解讀使用者動作的意義或順序,也不會解讀這類動作的優先順序。
這類互動通常成對,分別為起始互動和結尾互動。第二個互動包含第一個互動的參照。舉例來說,如果使用者輕觸某個按鈕後放開手指,輕觸動作就會產生 PressInteraction.Press
互動,而版本會產生 PressInteraction.Release
;Release
具有識別初始 PressInteraction.Press
的 press
屬性。
您可以觀察特定元件的 InteractionSource
以瞭解其互動。InteractionSource
是根據 Kotlin 流程建構而成,因此您可以像使用任何其他流程一樣,透過 Kotlin 流程收集互動。如要進一步瞭解這項設計決策,請參閱「實作互動」網誌文章。
互動狀態
您也可以自行追蹤互動,藉此擴充元件的內建功能,比如讓按鈕在受到點選時變色。如要追蹤互動,最簡單的方法就是觀察相應的互動「狀態」。InteractionSource
提供多種顯示各種互動狀態的方法。舉例來說,如要瞭解使用者是否按下了特定按鈕,您可以呼叫其 InteractionSource.collectIsPressedAsState()
方法:
val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() Button( onClick = { /* do something */ }, interactionSource = interactionSource ) { Text(if (isPressed) "Pressed!" else "Not pressed") }
除了 collectIsPressedAsState()
以外,Compose 還提供 collectIsFocusedAsState()
、collectIsDraggedAsState()
和 collectIsHoveredAsState()
。這些方法實際上是根據較低層級 InteractionSource
API 建構而成的簡便方法。在某些情況下,建議您直接使用這些較低層級的函式。
舉例來說,假設您必須瞭解使用者是否按下了按鈕,以及是否「也」拖曳了手指。如果您同時使用 collectIsPressedAsState()
和 collectIsDraggedAsState()
,Compose 會執行許多重複的工作,而且您接收到的互動順序不一定正確。在這類情況下,建議您直接使用 InteractionSource
。如要進一步瞭解如何透過 InteractionSource
追蹤自己的互動情形,請參閱「使用 InteractionSource
」。
下節說明如何分別使用 InteractionSource
和 MutableInteractionSource
及發出互動。
取用並發出 Interaction
InteractionSource
表示 Interactions
的唯讀串流;您無法將 Interaction
發送至 InteractionSource
。如要發出 Interaction
,您需要使用由 InteractionSource
擴充的 MutableInteractionSource
。
修飾符和元件可以使用、發出或取用及發出 Interactions
。以下各節將說明如何使用及發出修飾符和元件的互動。
使用修飾符範例
針對針對聚焦狀態繪製邊框的修飾符,只需觀察 Interactions
,即可接受 InteractionSource
:
fun Modifier.focusBorder(interactionSource: InteractionSource): Modifier { // ... }
從函式簽章中明確看出,這個修飾符是「消費端」— 可以使用 Interaction
,但無法發出這類符號。
產生修飾符範例
針對處理懸停事件 (例如 Modifier.hoverable
) 的修飾符,您必須發出 Interactions
,並改為接受 MutableInteractionSource
做為參數:
fun Modifier.hover(interactionSource: MutableInteractionSource, enabled: Boolean): Modifier { // ... }
這個修飾符屬於「生產端」,可以使用提供的 MutableInteractionSource
在滑鼠遊標懸停或懸停時發出 HoverInteractions
。
建構使用和生成的元件
Material Button
等高階元件會同時擔任生產者和消費者。可處理輸入和焦點事件,並依據這些事件變更外觀,例如顯示漣漪效果或動畫高度。因此,這些參數會直接將 MutableInteractionSource
公開為參數,以便您提供自己記住的執行個體:
@Composable fun Button( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, // exposes MutableInteractionSource as a parameter interactionSource: MutableInteractionSource? = null, elevation: ButtonElevation? = ButtonDefaults.elevatedButtonElevation(), shape: Shape = MaterialTheme.shapes.small, border: BorderStroke? = null, colors: ButtonColors = ButtonDefaults.buttonColors(), contentPadding: PaddingValues = ButtonDefaults.ContentPadding, content: @Composable RowScope.() -> Unit ) { /* content() */ }
這樣就能將 MutableInteractionSource
提升從元件中,並觀察元件產生的所有 Interaction
。您可以利用這項動作控制元件或 UI 中任何其他元件的外觀。
如要自行建構互動式高階元件,建議您透過此方式將 MutableInteractionSource
公開為參數。除了遵循狀態提升最佳做法以外,這也可讓您輕鬆讀取及控制元件的視覺狀態,就像任何其他狀態 (例如啟用狀態) 可以讀取和控制的狀態 (例如啟用狀態) 一樣。
Compose 遵循分層架構方法,因此高階 Material Design 元件是以基礎建構區塊為基礎建構而成,並產生用於控制漣漪效果和其他視覺效果所需的 Interaction
。基礎程式庫提供高階互動修飾符,例如 Modifier.hoverable
、Modifier.focusable
和 Modifier.draggable
。
如要建構元件來回應懸停事件,只要使用 Modifier.hoverable
,並傳遞 MutableInteractionSource
做為參數即可。每當使用者將滑鼠遊標懸停在元件上時,元件就會發出 HoverInteraction
,而您可以使用此設定變更元件的顯示方式。
// This InteractionSource will emit hover interactions val interactionSource = remember { MutableInteractionSource() } Box( Modifier .size(100.dp) .hoverable(interactionSource = interactionSource), contentAlignment = Alignment.Center ) { Text("Hello!") }
如要將此元件設為可聚焦,您可以新增 Modifier.focusable
,並將「相同的」 MutableInteractionSource
做為參數傳遞。現在,HoverInteraction.Enter/Exit
和 FocusInteraction.Focus/Unfocus
都會透過相同的 MutableInteractionSource
發出,您可以在相同位置自訂這兩種互動的外觀:
// This InteractionSource will emit hover and focus interactions val interactionSource = remember { MutableInteractionSource() } Box( Modifier .size(100.dp) .hoverable(interactionSource = interactionSource) .focusable(interactionSource = interactionSource), contentAlignment = Alignment.Center ) { Text("Hello!") }
與 hoverable
和 focusable
相比,Modifier.clickable
的抽象層更為高。如果想讓元件可供點選,是以隱含方式可懸停元素,而可供點選的元件也應可聚焦。您可以使用 Modifier.clickable
建立元件,用來處理懸停、聚焦和按下互動,而無需合併較低層級的 API。如果想讓元件也可供點選,可將 hoverable
和 focusable
替換為 clickable
:
// This InteractionSource will emit hover, focus, and press interactions val interactionSource = remember { MutableInteractionSource() } Box( Modifier .size(100.dp) .clickable( onClick = {}, interactionSource = interactionSource, // Also show a ripple effect indication = ripple() ), contentAlignment = Alignment.Center ) { Text("Hello!") }
與「InteractionSource
」合作
如果您需要低層級的元件互動資訊,可以為該元件的 InteractionSource
使用標準流程 API。舉例來說,假設您要保有 InteractionSource
的按下和拖曳互動清單。以下程式碼會執行一半的工作,在發生新的按下互動時立即將其加入清單中:
val interactionSource = remember { MutableInteractionSource() } val interactions = remember { mutableStateListOf<Interaction>() } LaunchedEffect(interactionSource) { interactionSource.interactions.collect { interaction -> when (interaction) { is PressInteraction.Press -> { interactions.add(interaction) } is DragInteraction.Start -> { interactions.add(interaction) } } } }
不過,除了新增互動以外,您也必須在互動結束後移除互動,例如在使用者將手指從元件上放開後。這很簡單,因為結尾互動一律包含相關聯起始互動的參照。以下程式碼顯示如何移除已結束的互動:
val interactionSource = remember { MutableInteractionSource() } val interactions = remember { mutableStateListOf<Interaction>() } LaunchedEffect(interactionSource) { interactionSource.interactions.collect { interaction -> when (interaction) { is PressInteraction.Press -> { interactions.add(interaction) } is PressInteraction.Release -> { interactions.remove(interaction.press) } is PressInteraction.Cancel -> { interactions.remove(interaction.press) } is DragInteraction.Start -> { interactions.add(interaction) } is DragInteraction.Stop -> { interactions.remove(interaction.start) } is DragInteraction.Cancel -> { interactions.remove(interaction.start) } } } }
現在,如要瞭解元件目前是否處於按下或拖曳狀態,只要查看 interactions
是否為空白即可:
val isPressedOrDragged = interactions.isNotEmpty()
如要瞭解最近一次的互動為何,只要查看清單中的最後一個項目即可。舉例來說,以下程式碼顯示 Compose 漣漪實作項目如何模擬適合用於最近互動的適當狀態重疊:
val lastInteraction = when (interactions.lastOrNull()) { is DragInteraction.Start -> "Dragged" is PressInteraction.Press -> "Pressed" else -> "No state" }
由於所有 Interaction
都遵循相同的結構,因此使用不同類型的使用者互動時,程式碼上不會有太大差異,因為所有模式都相同。
請注意,本節中的上述範例代表使用 State
的互動 Flow
,可讓您輕鬆觀察更新後的值,因為讀取狀態值會自動造成重組。不過,組成是以預先影格「批次」進行。這表示如果狀態有所變更,然後在同一個畫面中變更,觀測狀態的元件便不會看到變更。
這點十分重要,因為互動可能會在同一影格中定期開始和結束。舉例來說,將上一個範例搭配 Button
使用:
val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() Button(onClick = { /* do something */ }, interactionSource = interactionSource) { Text(if (isPressed) "Pressed!" else "Not pressed") }
如果按下開始和結束在同一影格中,文字一律不會顯示「已按下!」。在多數情況下,這並不是一個問題。如果持續顯示一小段時間的視覺效果,會導致畫面閃爍,而且使用者很容易察覺。在某些情況下,例如顯示漣漪特效或類似的動畫,您可能需要顯示至少一段時間的效果,而不是在按下按鈕時立即停止。為此,您可以直接從收集 lambda 內部啟動及停止動畫,而不必寫入狀態。如需此模式的範例,請參閱「建構使用動畫邊框的進階 Indication
」一節。
範例:透過自訂互動處理功能建構元件
想瞭解如何建構包含自訂輸入回應的元件,請參考以下這個修改過的按鈕範例。在本範例中,假設您想在使用者按下按鈕時變更按鈕外觀:
![動畫:使用者點選按鈕時,系統在按鈕中動態加入「雜貨購物車」圖示](https://developer.android.google.cn/static/develop/ui/compose/images/handling-interactions-iconbutton.gif?authuser=3&hl=zh-tw)
為此,請根據 Button
建構自訂可組合項,並讓該可組合項利用額外的 icon
參數繪製圖示 (在這個範例中為購物車圖示)。您要呼叫 collectIsPressedAsState()
追蹤使用者是否將滑鼠遊標懸停在按鈕上。如果是的話,就加入圖示。程式碼如下所示:
@Composable fun PressIconButton( onClick: () -> Unit, icon: @Composable () -> Unit, text: @Composable () -> Unit, modifier: Modifier = Modifier, interactionSource: MutableInteractionSource? = null ) { val isPressed = interactionSource?.collectIsPressedAsState()?.value ?: false Button( onClick = onClick, modifier = modifier, interactionSource = interactionSource ) { AnimatedVisibility(visible = isPressed) { if (isPressed) { Row { icon() Spacer(Modifier.size(ButtonDefaults.IconSpacing)) } } } text() } }
使用上述新可組合項後,程式碼將如下所示:
PressIconButton( onClick = {}, icon = { Icon(Icons.Filled.ShoppingCart, contentDescription = null) }, text = { Text("Add to cart") } )
這個新的 PressIconButton
是以現有質感設計 Button
為基礎,因此會照常回應使用者互動。使用者按下按鈕後,按鈕的不透明度會稍微改變,就像一般的質感設計 Button
一樣。
使用 Indication
建立及套用可重複使用的自訂效果
在之前的章節中,您已瞭解如何變更元件的某些部分以回應不同的 Interaction
,例如按下按鈕時顯示圖示。您可以利用相同的方法變更提供給元件的參數值,或變更元件內顯示的內容,但這僅適用於每個元件。應用程式或設計系統通常會針對有狀態視覺效果提供通用系統,這種效果應能以一致的方式套用至所有元件。
如要建構這種設計系統,請自訂一個元件,並針對其他元件重複使用這項自訂,原因如下:
- 設計系統中的每個元件都需要相同的樣板
- 要將這個效果套用至新建構的元件和自訂可點擊的元件,很容易忘記
- 難以結合自訂效果與其他效果
如要避免這類問題,並在系統中輕鬆調整自訂元件的資源配置,可以使用 Indication
。Indication
代表可重複使用的視覺效果,可套用至應用程式或設計系統中的各元件。Indication
分成兩個部分:
IndicationNodeFactory
:建立Modifier.Node
例項,為元件算繪視覺效果的工廠。針對不會跨元件變更的簡易實作方式,這可以是單例模式 (物件),並在整個應用程式中重複使用。這些執行個體可以是有狀態或無狀態的執行個體。由於它們是為每個元件建立,因此可以像使用任何其他
Modifier.Node
一樣,從CompositionLocal
擷取值以變更這些值在特定元件內的顯示方式或行為。Modifier.indication
:此修飾符可繪製元件的Indication
。Modifier.clickable
和其他高階互動修飾符直接接受指標參數,因此不僅會輸出Interaction
,還能為所發出的Interaction
繪製視覺效果。因此,在簡單的情況下,您可以直接使用Modifier.clickable
,而不需要使用Modifier.indication
。
以 Indication
取代效果
本節說明如何取代套用至特定按鈕的手動縮放效果,並指示其可在多個元件中重複使用。
下列程式碼會建立按鈕,可在按下時縮小:
val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() val scale by animateFloatAsState(targetValue = if (isPressed) 0.9f else 1f, label = "scale") Button( modifier = Modifier.scale(scale), onClick = { }, interactionSource = interactionSource ) { Text(if (isPressed) "Pressed!" else "Not pressed") }
如要將上方程式碼片段中的縮放效果轉換為 Indication
,請按照下列步驟操作:
建立負責套用縮放效果的
Modifier.Node
。連接後,節點會觀察互動來源,與先前的範例類似。唯一的差別在於,它會直接啟動動畫,而不是將連入的互動轉換為狀態。節點必須實作
DrawModifierNode
,才能覆寫ContentDrawScope#draw()
,並使用與 Compose 中任何其他圖形 API 相同的繪圖指令,算繪縮放效果。呼叫
ContentDrawScope
接收器提供的drawContent()
會繪製要套用Indication
的實際元件,因此您只要在縮放轉換中呼叫此函式即可。請確保Indication
實作在某個時間點一律呼叫drawContent()
,否則系統不會繪製您要套用Indication
的元件。private class ScaleNode(private val interactionSource: InteractionSource) : Modifier.Node(), DrawModifierNode { var currentPressPosition: Offset = Offset.Zero val animatedScalePercent = Animatable(1f) private suspend fun animateToPressed(pressPosition: Offset) { currentPressPosition = pressPosition animatedScalePercent.animateTo(0.9f, spring()) } private suspend fun animateToResting() { animatedScalePercent.animateTo(1f, spring()) } override fun onAttach() { coroutineScope.launch { interactionSource.interactions.collectLatest { interaction -> when (interaction) { is PressInteraction.Press -> animateToPressed(interaction.pressPosition) is PressInteraction.Release -> animateToResting() is PressInteraction.Cancel -> animateToResting() } } } } override fun ContentDrawScope.draw() { scale( scale = animatedScalePercent.value, pivot = currentPressPosition ) { this@draw.drawContent() } } }
建立
IndicationNodeFactory
。使用者只需要為提供的互動來源建立新的節點執行個體。由於沒有可設定指標的參數,因此工廠可能是物件:object ScaleIndication : IndicationNodeFactory { override fun create(interactionSource: InteractionSource): DelegatableNode { return ScaleNode(interactionSource) } override fun equals(other: Any?): Boolean = other === ScaleIndication override fun hashCode() = 100 }
Modifier.clickable
會在內部使用Modifier.indication
,因此如要使用ScaleIndication
建立可點選元件,您只需要提供Indication
做為clickable
的參數即可:Box( modifier = Modifier .size(100.dp) .clickable( onClick = {}, indication = ScaleIndication, interactionSource = null ) .background(Color.Blue), contentAlignment = Alignment.Center ) { Text("Hello!", color = Color.White) }
這也可讓您使用自訂
Indication
輕鬆建構可重複使用的高階元件,按鈕如下所示:@Composable fun ScaleButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, interactionSource: MutableInteractionSource? = null, shape: Shape = CircleShape, content: @Composable RowScope.() -> Unit ) { Row( modifier = modifier .defaultMinSize(minWidth = 76.dp, minHeight = 48.dp) .clickable( enabled = enabled, indication = ScaleIndication, interactionSource = interactionSource, onClick = onClick ) .border(width = 2.dp, color = Color.Blue, shape = shape) .padding(horizontal = 16.dp, vertical = 8.dp), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, content = content ) }
接著,您可以透過下列方式使用按鈕:
ScaleButton(onClick = {}) { Icon(Icons.Filled.ShoppingCart, "") Spacer(Modifier.padding(10.dp)) Text(text = "Add to cart!") }
![含有雜貨購物車圖示的按鈕動畫,按下後會縮小](https://developer.android.google.cn/static/develop/ui/compose/images/touchinput/button-custom-indication.gif?authuser=3&hl=zh-tw)
Indication
建構的按鈕。建立具有動畫邊框的進階 Indication
Indication
不只會影響轉換效果,例如縮放元件。IndicationNodeFactory
會傳回 Modifier.Node
,因此您可以在內容上方或下方繪製任何類型的效果,就像使用其他繪圖 API 一樣。舉例來說,您可以在元件周圍繪製動畫邊框,然後在元件上疊加動畫:
![酷炫彩虹按下效果的按鈕](https://developer.android.google.cn/static/develop/ui/compose/images/touchinput/rainbow-effect.gif?authuser=3&hl=zh-tw)
Indication
繪製的動畫邊框效果。此處的 Indication
實作與上一個範例類似,只會建立含有一些參數的節點。由於動畫邊框取決於形狀和 Indication
所用元件的邊框,因此 Indication
實作時也必須提供形狀和框線寬度做為參數:
data class NeonIndication(private val shape: Shape, private val borderWidth: Dp) : IndicationNodeFactory { override fun create(interactionSource: InteractionSource): DelegatableNode { return NeonNode( shape, // Double the border size for a stronger press effect borderWidth * 2, interactionSource ) } }
Modifier.Node
實作在概念上也相同,即使繪製程式碼比較複雜也一樣。和先前一樣,系統會在附加時觀察 InteractionSource
、啟動動畫,並實作 DrawModifierNode
,以在內容上方繪製效果:
private class NeonNode( private val shape: Shape, private val borderWidth: Dp, private val interactionSource: InteractionSource ) : Modifier.Node(), DrawModifierNode { var currentPressPosition: Offset = Offset.Zero val animatedProgress = Animatable(0f) val animatedPressAlpha = Animatable(1f) var pressedAnimation: Job? = null var restingAnimation: Job? = null private suspend fun animateToPressed(pressPosition: Offset) { // Finish any existing animations, in case of a new press while we are still showing // an animation for a previous one restingAnimation?.cancel() pressedAnimation?.cancel() pressedAnimation = coroutineScope.launch { currentPressPosition = pressPosition animatedPressAlpha.snapTo(1f) animatedProgress.snapTo(0f) animatedProgress.animateTo(1f, tween(450)) } } private fun animateToResting() { restingAnimation = coroutineScope.launch { // Wait for the existing press animation to finish if it is still ongoing pressedAnimation?.join() animatedPressAlpha.animateTo(0f, tween(250)) animatedProgress.snapTo(0f) } } override fun onAttach() { coroutineScope.launch { interactionSource.interactions.collect { interaction -> when (interaction) { is PressInteraction.Press -> animateToPressed(interaction.pressPosition) is PressInteraction.Release -> animateToResting() is PressInteraction.Cancel -> animateToResting() } } } } override fun ContentDrawScope.draw() { val (startPosition, endPosition) = calculateGradientStartAndEndFromPressPosition( currentPressPosition, size ) val brush = animateBrush( startPosition = startPosition, endPosition = endPosition, progress = animatedProgress.value ) val alpha = animatedPressAlpha.value drawContent() val outline = shape.createOutline(size, layoutDirection, this) // Draw overlay on top of content drawOutline( outline = outline, brush = brush, alpha = alpha * 0.1f ) // Draw border on top of overlay drawOutline( outline = outline, brush = brush, alpha = alpha, style = Stroke(width = borderWidth.toPx()) ) } /** * Calculates a gradient start / end where start is the point on the bounding rectangle of * size [size] that intercepts with the line drawn from the center to [pressPosition], * and end is the intercept on the opposite end of that line. */ private fun calculateGradientStartAndEndFromPressPosition( pressPosition: Offset, size: Size ): Pair<Offset, Offset> { // Convert to offset from the center val offset = pressPosition - size.center // y = mx + c, c is 0, so just test for x and y to see where the intercept is val gradient = offset.y / offset.x // We are starting from the center, so halve the width and height - convert the sign // to match the offset val width = (size.width / 2f) * sign(offset.x) val height = (size.height / 2f) * sign(offset.y) val x = height / gradient val y = gradient * width // Figure out which intercept lies within bounds val intercept = if (abs(y) <= abs(height)) { Offset(width, y) } else { Offset(x, height) } // Convert back to offsets from 0,0 val start = intercept + size.center val end = Offset(size.width - start.x, size.height - start.y) return start to end } private fun animateBrush( startPosition: Offset, endPosition: Offset, progress: Float ): Brush { if (progress == 0f) return TransparentBrush // This is *expensive* - we are doing a lot of allocations on each animation frame. To // recreate a similar effect in a performant way, it would be better to create one large // gradient and translate it on each frame, instead of creating a whole new gradient // and shader. The current approach will be janky! val colorStops = buildList { when { progress < 1 / 6f -> { val adjustedProgress = progress * 6f add(0f to Blue) add(adjustedProgress to Color.Transparent) } progress < 2 / 6f -> { val adjustedProgress = (progress - 1 / 6f) * 6f add(0f to Purple) add(adjustedProgress * MaxBlueStop to Blue) add(adjustedProgress to Blue) add(1f to Color.Transparent) } progress < 3 / 6f -> { val adjustedProgress = (progress - 2 / 6f) * 6f add(0f to Pink) add(adjustedProgress * MaxPurpleStop to Purple) add(MaxBlueStop to Blue) add(1f to Blue) } progress < 4 / 6f -> { val adjustedProgress = (progress - 3 / 6f) * 6f add(0f to Orange) add(adjustedProgress * MaxPinkStop to Pink) add(MaxPurpleStop to Purple) add(MaxBlueStop to Blue) add(1f to Blue) } progress < 5 / 6f -> { val adjustedProgress = (progress - 4 / 6f) * 6f add(0f to Yellow) add(adjustedProgress * MaxOrangeStop to Orange) add(MaxPinkStop to Pink) add(MaxPurpleStop to Purple) add(MaxBlueStop to Blue) add(1f to Blue) } else -> { val adjustedProgress = (progress - 5 / 6f) * 6f add(0f to Yellow) add(adjustedProgress * MaxYellowStop to Yellow) add(MaxOrangeStop to Orange) add(MaxPinkStop to Pink) add(MaxPurpleStop to Purple) add(MaxBlueStop to Blue) add(1f to Blue) } } } return linearGradient( colorStops = colorStops.toTypedArray(), start = startPosition, end = endPosition ) } companion object { val TransparentBrush = SolidColor(Color.Transparent) val Blue = Color(0xFF30C0D8) val Purple = Color(0xFF7848A8) val Pink = Color(0xFFF03078) val Orange = Color(0xFFF07800) val Yellow = Color(0xFFF0D800) const val MaxYellowStop = 0.16f const val MaxOrangeStop = 0.33f const val MaxPinkStop = 0.5f const val MaxPurpleStop = 0.67f const val MaxBlueStop = 0.83f } }
主要差異在於現在使用 animateToResting()
函式的動畫設有最短持續時間,因此即使按下動作立即釋放,按下動畫仍會繼續播放。您也可以在 animateToPressed
開始時處理多次快速按下事件,如果在現有的按下或靜止動畫期間按下動作,系統會取消上一個動畫,並由按下動畫從頭開始播放。如要支援多種並行效果 (例如將新的分享關係圖繪製在其他漣漪效果動畫之上),您可以在清單中追蹤動畫,而不必取消現有的動畫及啟動新動畫。
為您推薦
- 注意:系統會在 JavaScript 關閉時顯示連結文字
- 瞭解手勢
- 適用於 Jetpack Compose 的 Kotlin
- Material Design 元件和版面配置