變更焦點行為

有時候,您必須覆寫畫面上元素的預設焦點行為。例如,您可能想要將可組合項分組避免聚焦特定可組合項、明確要求焦點位於某個可組合項、擷取或釋放焦點,或是在進入或離開時重新導向焦點。本節說明如何在預設值不符合需求時變更焦點行為。

透過焦點群組提供連貫的導覽體驗

有時 Jetpack Compose 不會立即猜測分頁導覽的下一個項目,尤其是當分頁和清單等複雜的父項 Composables 上線時。

雖然焦點搜尋通常會遵循 Composables 的宣告順序,但在某些情況下不會這麼做,例如階層中的其中一個 Composables 是無法完全顯示的水平捲動項目。如以下範例所示。

Jetpack Compose 可能會決定將下一個項目聚焦在最靠近畫面的開頭處 (如下所示),而不是繼續前往您預期的單向導覽路徑:

應用程式的動畫,顯示頂端水平導覽項目,且包含下列項目清單。
圖 1. 應用程式的動畫,顯示頂端水平導覽項目和下方的項目清單

在這個例子中,開發人員明顯不想把重點從「Chocolates」(巧克力) 分頁移到下方第一張圖片,然後再返回「Pastries」(糕點) 分頁。反之,他們希望聚焦於分頁繼續停留到最後一個分頁,然後將焦點放在內部內容:

應用程式的動畫,顯示頂端水平導覽項目,且包含下列項目清單。
圖 2. 應用程式的動畫,顯示頂端水平導覽項目和下方的項目清單

當系統必須依序取得一組可組合項 (例如上例中的分頁列) 時,您必須將 Composable 納入含有 focusGroup() 修飾符的父項中:

LazyVerticalGrid(columns = GridCells.Fixed(4)) {
    item(span = { GridItemSpan(maxLineSpan) }) {
        Row(modifier = Modifier.focusGroup()) {
            FilterChipA()
            FilterChipB()
            FilterChipC()
        }
    }
    items(chocolates) {
        SweetsCard(sweets = it)
    }
}

雙向導覽會尋找指定方向最接近的可組合項,如果其他群組中的元素距離目前群組中無法完全顯示的項目距離更近,導覽功能就會挑選最接近的項目。如要避免這種行為,可以套用 focusGroup() 修飾符。

FocusGroup 可讓整個群組在焦點上看起來像單一實體,但群組本身不會取得焦點,而會改為聚焦距離最近的子項。如此一來,導覽就知道在離開群組前,會前往無法看見的項目。

在此情況下,即使使用者已完全能看到 SweetsCards,且部分 FilterChip 可能隱藏,系統仍會將焦點放在 SweetsCard 項目之前的三個 FilterChip 例項。這是因為 focusGroup 修飾符會指示焦點管理員調整聚焦項目的順序,讓導覽更容易且與使用者介面更一致。

如果沒有 focusGroup 修飾符,則不顯示 FilterChipC 時,焦點導覽功能最後會回到該修飾符。不過,新增這類修飾詞不僅可偵測出來,還能像使用者預期一樣在 FilterChipB 後立即取得焦點。

將可組合項設為可聚焦

部分可組合項的設計可聚焦,例如按鈕或附加 clickable 修飾符的可組合項。如果您想特別將可聚焦行為新增至可組合項,請使用 focusable 修飾符:

var color by remember { mutableStateOf(Green) }
Box(
    Modifier
        .background(color)
        .onFocusChanged { color = if (it.isFocused) Blue else Green }
        .focusable()
) {
    Text("Focusable 1")
}

將可組合項變成無法聚焦

在某些情況下,部分元素不應參與焦點。在這種極少數的情況下,您可以使用 canFocus property 從無法聚焦的 Composable 中排除。

var checked by remember { mutableStateOf(false) }

Switch(
    checked = checked,
    onCheckedChange = { checked = it },
    // Prevent component from being focused
    modifier = Modifier
        .focusProperties { canFocus = false }
)

使用 FocusRequester 要求鍵盤焦點

在某些情況下,您可能會想要明確要求焦點,以回應使用者互動。舉例來說,您可以詢問使用者是否要重新在表單中填入資料,並詢問使用者「是」時,想重新聚焦該表單的第一個欄位。

首先,請將 FocusRequester 物件與您要將鍵盤焦點移至哪個可組合項建立關聯。在以下程式碼片段中,系統會設定名為 Modifier.focusRequester 的修飾符,將 FocusRequester 物件與 TextField 建立關聯:

val focusRequester = remember { FocusRequester() }
var text by remember { mutableStateOf("") }

TextField(
    value = text,
    onValueChange = { text = it },
    modifier = Modifier.focusRequester(focusRequester)
)

您可以呼叫 FocusRequester 的 requestFocus 方法,以便傳送實際焦點要求。您應在 Composable 內容以外的地方叫用這個方法 (否則,系統會在每次重組時重新執行此方法)。下列程式碼片段示範如何要求系統在使用者點選按鈕時移動鍵盤焦點:

val focusRequester = remember { FocusRequester() }
var text by remember { mutableStateOf("") }

TextField(
    value = text,
    onValueChange = { text = it },
    modifier = Modifier.focusRequester(focusRequester)
)

Button(onClick = { focusRequester.requestFocus() }) {
    Text("Request focus on TextField")
}

擷取並放開焦點

您可以運用重點引導使用者提供應用程式執行工作所需的適當資料,例如取得有效的電子郵件地址或電話號碼。雖然錯誤狀態會讓使用者瞭解實際情況,但您可能需要含有錯誤資訊的欄位,直到問題修正為止。

如要擷取焦點,您可以叫用 captureFocus() 方法,然後改用 freeFocus() 方法發布該方法,如以下範例所示:

val textField = FocusRequester()

TextField(
    value = text,
    onValueChange = {
        text = it

        if (it.length > 3) {
            textField.captureFocus()
        } else {
            textField.freeFocus()
        }
    },
    modifier = Modifier.focusRequester(textField)
)

優先使用焦點修飾符

Modifiers 可以視為只有一個子項的元素,因此在將其排入佇列時,左側 (或頂端) 的每個 Modifier 都會納入右側 (或下方) 後面的 Modifier。這表示第二個 Modifier 包含在第一個函式中,因此在宣告兩個 focusProperties 時,只有最頂層的一個有效,因為下列項目包含在最頂層。

如要進一步釐清概念,請參閱下列程式碼:

Modifier
    .focusProperties { right = item1 }
    .focusProperties { right = item2 }
    .focusable()

在這種情況下,系統不會使用指出 item2 做為正確焦點的 focusProperties,因為上述內容已包含在上述標記中;因此,系統會使用 item1

使用這個方法時,父項也可以使用 FocusRequester.Default 將行為重設為預設值:

Modifier
    .focusProperties { right = Default }
    .focusProperties { right = item1 }
    .focusProperties { right = item2 }
    .focusable()

父項不一定要屬於同一個修飾符鏈結。父項可組合項可以覆寫子項可組合項的焦點屬性。舉例來說,請考慮使用此 FancyButton,導致按鈕無法聚焦:

@Composable
fun FancyButton(modifier: Modifier = Modifier) {
    Row(modifier.focusProperties { canFocus = false }) {
        Text("Click me")
        Button(onClick = { }) { Text("OK") }
    }
}

使用者只要將 canFocus 設為 true,即可讓此按鈕再次成為焦點:

FancyButton(Modifier.focusProperties { canFocus = true })

和每個 Modifier 一樣,焦點相關項目的行為取決於您宣告的順序。舉例來說,如下所示的程式碼可讓 Box 成為可聚焦,但 FocusRequester 會在可聚焦後宣告,因此不會與此可聚焦項目建立關聯。

Box(
    Modifier
        .focusable()
        .focusRequester(Default)
        .onFocusChanged {}
)

請務必記得,focusRequester 與階層中下方第一個可聚焦的項目相關聯,因此這個 focusRequester 會指向第一個可聚焦的子項。如果沒有可用的資料,則不會指向任何項目。 不過,由於 Box 可聚焦 (感謝 focusable() 修飾符),因此您可以使用雙向導覽功能前往該修飾符。

再舉一個例子,由於 onFocusChanged() 修飾符參照 focusable()focusTarget() 修飾符之後的第一個可聚焦元素,因此可以使用下列指令。

Box(
    Modifier
        .onFocusChanged {}
        .focusRequester(Default)
        .focusable()
)
Box(
    Modifier
        .focusRequester(Default)
        .onFocusChanged {}
        .focusable()
)

進入或離開時將焦點重新導向

有時您需要提供非常具體的導覽類型,例如下方動畫中的導覽:

動畫:顯示兩欄並排的按鈕,動畫焦點從一欄另一欄開始。
圖 3. 動畫畫面顯示兩欄並排放置的按鈕,並以動畫方式將焦點從一欄移至另一欄

深入探討如何建立這項功能之前,請務必先瞭解焦點搜尋的預設行為。在未經任何修改的情況下,當焦點的搜尋到達 Clickable 3 項目時,在 D-Pad 上按下 DOWN (或對等的方向鍵) 會將焦點移至 Column 下方顯示的內容,離開群組並忽略右側的群組。如果沒有可聚焦的項目,則焦點不會移動,而是會留在 Clickable 3 上。

如要修改此行為並提供預期的導覽,您可以利用 focusProperties 修飾符,管理焦點搜尋進入或離開 Composable 時會發生什麼事:

val otherComposable = remember { FocusRequester() }

Modifier.focusProperties {
    exit = { focusDirection ->
        when (focusDirection) {
            Right -> Cancel
            Down -> otherComposable
            else -> Default
        }
    }
}

每當焦點進入或離開階層的某個部分時,可以將焦點導向特定 Composable。例如,當 UI 有兩個資料欄,而您想確保系統每次處理第一個資料欄時,請將焦點切換到第二個:

動畫:顯示兩欄並排的按鈕,動畫焦點從一欄另一欄開始。
圖 4. 動畫畫面顯示兩欄並排放置的按鈕,並以動畫方式將焦點從一欄移至另一欄

在這張 GIF 中,當焦點到達 Column 1 中的 Clickable 3 Composable 時,下一個聚焦項目就是另一個 Column 中的 Clickable 4。透過結合 focusDirectionfocusProperties 修飾符中的 enterexit 值,即可達成此行為。兩者都需要一個 lambda,該參數會將焦點的來源方向做為參數,並傳回 FocusRequester。此 lambda 的行為有三種不同:傳回 FocusRequester.Cancel 會讓焦點停止繼續,但 FocusRequester.Default 不會變更其行為。而改為提供附加至其他 ComposableFocusRequester,可將焦點跳至該特定 Composable

變更焦點前進方向

如要將焦點移至下一個項目或朝著精確方向移動,您可以利用 onPreviewKey 修飾符並直接使用 LocalFocusManager 來透過 moveFocus 修飾符引導焦點。

以下範例顯示焦點機制的預設行為:偵測到 tab 鍵時,焦點會前進至焦點清單中的下一個元素。雖然這並非需要設定的項目,但您必須瞭解系統內部運作,才能變更預設行為。

val focusManager = LocalFocusManager.current
var text by remember { mutableStateOf("") }

TextField(
    value = text,
    onValueChange = { text = it },
    modifier = Modifier.onPreviewKeyEvent {
        when {
            KeyEventType.KeyUp == it.type && Key.Tab == it.key -> {
                focusManager.moveFocus(FocusDirection.Next)
                true
            }

            else -> false
        }
    }
)

在此範例中,focusManager.moveFocus() 函式會將焦點移到指定項目,或函式參數中隱含的方向。