處理使用者互動

使用者介面元件可透過回應使用者互動的方式,向裝置使用者提供回饋。每個元件都有專屬的互動回應方式,可協助使用者瞭解互動情形。舉例來說,如果使用者在裝置的觸控螢幕上輕觸某個按鈕,該按鈕可能會以某種方式改變,例如加上醒目顯示顏色。這項變化可讓使用者知道自己已輕觸該按鈕。如果使用者並不是想執行這項操作,就會知道要先將手指從按鈕拖曳到其他地方再放開,否則會啟用該按鈕。

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.ReleaseRelease 具有用於識別初始 PressInteraction.Presspress 屬性。

您可以觀察特定元件的 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 使用標準流程 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 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"
}

範例說明

想瞭解如何建構包含自訂輸入回應的元件,請參考以下這個修改過的按鈕範例。在本範例中,假設您想在使用者按下按鈕時變更按鈕外觀:

動畫:使用者點選按鈕時,系統在按鈕中動態加入圖示

為此,請根據 Button 建構自訂可組合項,並讓該可組合項利用額外的 icon 參數繪製圖示 (在這個範例中為購物車圖示)。您要呼叫 collectIsPressedAsState() 追蹤使用者是否將滑鼠游標懸停在按鈕上。如果是的話,就加入圖示。程式碼如下所示:

@Composable
fun PressIconButton(
    onClick: () -> Unit,
    icon: @Composable () -> Unit,
    text: @Composable () -> Unit,
    modifier: Modifier = Modifier,
    interactionSource: MutableInteractionSource =
        remember { MutableInteractionSource() },
) {
    val isPressed by interactionSource.collectIsPressedAsState()
    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 一樣。此外,使用新程式碼後,HoverIconButton 會新增圖示以動態回應游標懸停動作。