處理使用者互動

使用者介面元件可透過以下方式向裝置使用者提供意見回饋 回應使用者互動每個元件都有專屬的回應 互動 - 幫助使用者瞭解使用者互動行為。適用對象 舉例來說,如果使用者在裝置的觸控螢幕上輕觸按鈕, 可能以某種程度改變 例如加上醒目顯示顏色這項異動 讓使用者知道他們已輕觸按鈕。如果使用者不願意 這樣他們就會知道 否則這個按鈕就會啟動

圖 1. 一律顯示啟用的按鈕,不會按下漣漪效果。
圖 2. 附有漣漪效果的按鈕,可反映其啟用狀態。

Compose 手勢說明文件 會說明 Compose 元件會處理低階指標事件,例如指標移動和 點擊。Compose 預設會將這些低層級事件 互動程度較高的活動,舉例來說,一系列指標事件可能會 按下並放開按鈕。瞭解這些較高層級的抽象層 可協助您自訂 UI 回應使用者的方式。舉例來說 自訂元件外觀,決定使用者與使用者互動時的元件 或者只是想保留這類使用者動作的記錄這個 文件提供了修改標準 UI 元素所需的資訊 也可以自行設計

互動

在許多情況下,您不必瞭解 Compose 元件 或是解讀使用者互動方式舉例來說,Button 依賴 Modifier.clickable 瞭解使用者是否點選了按鈕。如果要加入的是 的按鈕,您可以定義該按鈕的 onClick 程式碼。 Modifier.clickable 會視情況執行該程式碼。也就是說 便能得知使用者是否輕觸螢幕 鍵盤;Modifier.clickable 會發現使用者已完成點擊,而且 就會回應您的 onClick 程式碼。

不過,如果您要自訂 UI 元件對使用者行為的回應, 可能還需要更瞭解背後的運作原理本節提供了 提供一些資訊

當使用者與 UI 元件互動時,系統會表示其行為 方法是 Interaction敬上 事件。舉例來說,如果使用者輕觸按鈕,這個按鈕就會產生 PressInteraction.Press。 當使用者在按鈕內舉起手指時, PressInteraction.Release、 通知按鈕點擊已經完成另一方面 使用者將手指拖曳到按鈕外,然後舉起手指, 產生 PressInteraction.Cancel、 表示按下按鈕已取消,未完成。

這類互動無預設立場。也就是說 事件並非解讀使用者動作的意義 序列也不會解讀這類動作的優先順序 或執行其他動作

這類互動通常會成對,具有開始和結束。第二個 次互動包含第一個事件的參照。例如,如果使用者 輕觸按鈕並放開手指,觸控就會產生 PressInteraction.Press敬上 而發布版本會產生 PressInteraction.ReleaseRelease 具有 press 屬性,可識別 PressInteraction.Press

您可以觀察特定元件的互動情形, InteractionSourceInteractionSource 是以 Kotlin 為基礎 資料流,因此您可以透過相同的方式收集互動資料 您需要執行其他流程如要進一步瞭解這項設計決策 請參閱「Illuminating Interactions」這篇網誌文章。

互動狀態

您可能會想擴充元件的內建功能 自行追蹤互動情形舉例來說,假設您希望某個按鈕 按下後會變更顏色。如要追蹤互動情形,最簡單的方法是: 觀察適當的互動狀態。「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」一文。

下一節將說明如何使用及發出互動 InteractionSourceMutableInteractionSource

消耗並發出 Interaction

InteractionSource 代表 Interactions 的唯讀串流,而非 向 InteractionSource 發出 Interaction。發出 Interaction,您需要使用從MutableInteractionSource InteractionSource

修飾符和元件可以使用、發出或消耗及發出 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 元件是以基礎建築物為基礎 產生 Interaction 所需的區塊,而這些區塊需要控制漣漪效果和其他 視覺效果基礎程式庫提供高階互動修飾符 例如 Modifier.hoverableModifier.focusableModifier.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/ExitFocusInteraction.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!")
}

Modifier.clickable提升了 其層級抽象化機制必須大於 hoverablefocusable, 這是可點擊的元件,也是隱含的,而且可點擊的元件 也能聚焦您可以使用 Modifier.clickable 建立元件 可處理懸停、聚焦和按下互動,不必合併 API 級別您也可以將元件設為可點擊 將 hoverablefocusable 替換為 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 都採用相同的結構,因此不會有太多 和不同類型的使用者互動 整體模式均相同

請注意,本節中的範例代表 Flow 使用 State 的互動 — 可讓您輕鬆觀測最新的值 因為讀取狀態值會自動引起重組。不過 組合是批次的前置影格。也就是說,如果狀態變更 然後在同一影格中變更回原來的位置,觀測狀態的元件就不會 相關變更

這對於互動很重要,因為互動可能會定期開始和結束 在同一畫面中例如,將前一個範例與 Button 搭配使用:

val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()

Button(onClick = { /* do something */ }, interactionSource = interactionSource) {
    Text(if (isPressed) "Pressed!" else "Not pressed")
}

如果按下按鍵開始及結束於相同畫面,則文字一律不會 「已按下!」。在大多數情況下,這並非問題,而是 這只會導致閃爍 第一,更容易注意到針對某些情況,例如顯示漣漪效果, 建議您至少以最低標準 而不是在按下按鈕時立即停止。目的地: 可以直接在集合中開始和停止動畫 lambda,而非寫入狀態。這個模式的範例在 「Build a Advanced with an anim 生效 Ball」(建構具有動畫邊框的進階 Indication) 區段。

範例:使用自訂互動處理建構元件

如要瞭解如何使用自訂輸入回應建構元件,請參閱 修改後按鈕範例。在這種情況下,假設您想新增一個 透過變更外觀來回應按下

動畫:使用者點選時,動態加入雜貨購物車圖示
圖 3. 使用者點選時動態加入圖示的按鈕。

為此,請根據 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 是以現有 Material 為基礎 Button 的應用程式,會以所有一般方式回應使用者互動。當使用者 按下按鈕後,透明度會稍微改變,就像一般情況一樣 材質:Button

使用 Indication 建立及套用可重複使用的自訂特效

在先前的章節中,您已學會如何在回應中變更元件的部分內容 不同 Interaction,例如在按下時顯示圖示。相同 這種方法可用來變更你提供給 或變更元件內顯示的內容,但這是 但僅適用於個別元件通常是應用程式或設計系統 將擁有適用於有狀態視覺效果的通用系統, 以一致的方式套用至所有元件

如要建構這種設計系統,請自訂單一元件 但如果您在其他元件中重複使用這個自訂設定, 。

  • 設計系統中的每個元件都需要相同的樣板
  • 容易忘記將這項效果套用至新建構的元件和自訂元件 可點擊的元件
  • 想結合自訂特效和其他效果,可能並不容易

為避免發生這類問題,並在系統中輕鬆調整自訂元件的資源配置, 可以使用 IndicationIndication 代表可重複使用的視覺效果 應用程式或設計系統中的元件Indication分成兩部分 部分:

  • IndicationNodeFactory:建立具有 Modifier.Node 例項的工廠函式 算繪元件的視覺效果。如果有比較簡單的導入作業 因為這可以是一個單例模式 (物件),並在不同元件中重複使用 整個應用程式

    這些執行個體可以是有狀態或無狀態。由於這些是依據個別建立 就可以從 CompositionLocal 擷取值,變更 它們在特定元件中的外觀或行為,和其他類似 Modifier.Node

  • Modifier.indication: 此修飾符繪製 Indication, 元件。Modifier.clickable 和其他高階互動修飾符 直接接受指標參數,因此這些參數不僅會發出 Interaction,但也可以為其 Interaction 繪製視覺效果 發出因此,如果是簡單的情況,您可以直接使用 Modifier.clickable,無需等待 需要 Modifier.indication

將特效更換為 Indication

本節說明如何替換套用至 1 的手動縮放效果 特定按鈕,標有可重複用於多個項目的 元件。

以下程式碼會建立一個按鈕,按下時可向下縮小:

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,請按照 步驟如下:

  1. 建立負責套用縮放效果的 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()
            }
        }
    }

  2. 建立 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
    }

  3. 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!")
}

動畫按鈕,當中的雜貨購物車圖示會在按下時縮小
圖 4. 內建自訂 Indication 的按鈕。
,瞭解如何調查及移除這項存取權。

建立加上動畫邊框的進階 Indication

Indication 不僅限於轉換效果,例如資源調度 元件。IndicationNodeFactory 會傳回 Modifier.Node,因此您可以繪製 與其他繪圖 API 一樣,在內容上方或下方顯示任何形式的效果。適用對象 例如,您可以在元件周圍繪製動畫框線 按下元件時:

點擊時帶有精美彩虹效果的按鈕
圖 5. 使用 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 開始時多次快速按下 (如果按壓按鈕) 會在現有的按下或靜息動畫播放期間發生 取消後,按下動畫就會從頭開始播放。支援多種 兩個並行效果 (例如透過漣漪效果,新的分享關係會 除了其他漣漪效果外,您也可以追蹤清單中的動畫,不必使用 取消現有動畫並啟動新動畫。