Thay đổi chế độ lấy nét

Đôi khi, bạn cần phải ghi đè hành vi lấy nét mặc định của các phần tử trên màn hình. Ví dụ: có thể bạn muốn nhóm các thành phần kết hợp, ngăn chặn tiêu điểm vào một thành phần kết hợp nhất định, yêu cầu lấy tiêu điểm một cách rõ ràng, chụp hoặc huỷ tiêu điểm hoặc chuyển hướng tiêu điểm khi vào hoặc thoát. Phần này mô tả cách thay đổi hành vi của tiêu điểm khi các giá trị mặc định không phải là điều bạn cần.

Mang đến khả năng điều hướng nhất quán thông qua các nhóm tâm điểm

Đôi khi, Jetpack Compose không đoán ngay mục tiếp theo chính xác cho các thao tác bằng thẻ, đặc biệt là khi Composables mẹ phức tạp như các thẻ và danh sách xuất hiện.

Mặc dù tính năng tìm kiếm tâm điểm thường tuân theo thứ tự khai báo của Composables, nhưng điều này không thể thực hiện được trong một số trường hợp, chẳng hạn như khi một trong các Composables trong hệ phân cấp là một thanh cuộn ngang không hiển thị đầy đủ. Điều này được thể hiện trong ví dụ bên dưới.

Jetpack Compose có thể quyết định tập trung vào mục tiếp theo gần điểm bắt đầu màn hình nhất, như minh hoạ dưới đây, thay vì tiếp tục trên đường dẫn mà bạn mong đợi đối với thao tác điều hướng một chiều:

Ảnh động về một ứng dụng cho thấy thanh điều hướng ngang ở trên cùng và danh sách các mục bên dưới.
Hình 1. Ảnh động về một ứng dụng cho thấy thanh điều hướng ngang ở trên cùng và danh sách các mục bên dưới

Trong ví dụ này, rõ ràng là các nhà phát triển không có ý định chuyển tiêu điểm từ thẻ Sô-cô-la sang hình ảnh đầu tiên bên dưới, sau đó quay lại thẻ Bánh ngọt. Thay vào đó, họ muốn tiêu điểm tiếp tục ở trên các thẻ cho đến thẻ cuối cùng, rồi tập trung vào nội dung bên trong:

Ảnh động về một ứng dụng cho thấy thanh điều hướng ngang ở trên cùng và danh sách các mục bên dưới.
Hình 2. Ảnh động về một ứng dụng cho thấy thanh điều hướng ngang ở trên cùng và danh sách các mục bên dưới

Trong những trường hợp quan trọng là một nhóm thành phần kết hợp phải được lấy tiêu điểm theo tuần tự, như trong hàng Thẻ của ví dụ trước, bạn cần gói Composable trong một thành phần mẹ có đối tượng sửa đổi focusGroup():

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

Điều hướng hai chiều sẽ tìm thành phần kết hợp gần nhất theo hướng nhất định – nếu một phần tử từ một nhóm khác gần hơn một mục không hiển thị đầy đủ trong nhóm hiện tại, thì thành phần điều hướng sẽ chọn mục gần nhất. Để tránh hành vi này, bạn có thể áp dụng đối tượng sửa đổi focusGroup().

FocusGroup làm cho cả nhóm trông giống như một thực thể duy nhất về mặt tiêu điểm, nhưng bản thân nhóm đó sẽ không nhận được tiêu điểm. Thay vào đó, thành phần con gần nhất sẽ được lấy tiêu điểm. Bằng cách này, tính năng điều hướng sẽ biết chuyển đến mục không hiển thị đầy đủ trước khi rời khỏi nhóm.

Trong trường hợp này, 3 thực thể của FilterChip sẽ được lấy làm tâm điểm trước các mục SweetsCard, ngay cả khi người dùng hoàn toàn nhìn thấy SweetsCards và một số FilterChip có thể bị ẩn. Điều này xảy ra vì đối tượng sửa đổi focusGroup yêu cầu trình quản lý tiêu điểm điều chỉnh thứ tự các mục được lấy tiêu điểm để thao tác dễ dàng và nhất quán hơn với giao diện người dùng.

Nếu không có đối tượng sửa đổi focusGroup, nếu FilterChipC không hiển thị, thì tính năng điều hướng tâm điểm sẽ chọn đối tượng đó sau cùng. Tuy nhiên, việc thêm một đối tượng sửa đổi như vậy làm cho đối tượng sửa đổi này không chỉ ở chế độ có thể tìm thấy mà còn lấy được tiêu điểm ngay sau FilterChipB, như người dùng mong đợi.

Làm cho thành phần kết hợp có thể làm tâm điểm

Một số thành phần kết hợp có thể làm tâm điểm theo thiết kế, chẳng hạn như Nút hoặc thành phần kết hợp có đối tượng sửa đổi clickable đi kèm. Nếu muốn thêm cụ thể hành vi có thể tập trung vào một thành phần kết hợp, bạn phải sử dụng đối tượng sửa đổi focusable:

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

Làm cho thành phần kết hợp không thể làm tiêu điểm

Có thể có những trường hợp mà một số phần tử của bạn không tham gia vào tiêu điểm. Trong những trường hợp hiếm hoi này, bạn có thể tận dụng canFocus property để loại trừ Composable khỏi tiêu điểm.

var checked by remember { mutableStateOf(false) }

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

Yêu cầu lấy tiêu điểm bàn phím bằng FocusRequester

Trong một số trường hợp, bạn có thể muốn yêu cầu rõ ràng lấy tiêu điểm làm phản hồi cho một tương tác của người dùng. Ví dụ: bạn có thể hỏi người dùng xem họ có muốn bắt đầu lại điền vào biểu mẫu hay không và nếu họ nhấn "có", thì bạn muốn lấy nét lại trường đầu tiên của biểu mẫu đó.

Điều đầu tiên cần làm là liên kết một đối tượng FocusRequester với thành phần kết hợp mà bạn muốn di chuyển tiêu điểm bàn phím đến. Trong đoạn mã sau, đối tượng FocusRequester được liên kết với TextField bằng cách đặt một đối tượng sửa đổi có tên là Modifier.focusRequester:

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

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

Bạn có thể gọi phương thức requestFocus của FocusRequester để gửi các yêu cầu lấy tiêu điểm thực tế. Bạn phải gọi phương thức này bên ngoài ngữ cảnh Composable (nếu không, phương thức này sẽ được thực thi lại ở mỗi lần kết hợp lại). Đoạn mã sau đây cho biết cách yêu cầu hệ thống di chuyển tiêu điểm bàn phím khi bạn nhấp vào nút:

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")
}

Chụp và thả tiêu điểm

Bạn có thể tận dụng tính năng tập trung để hướng dẫn người dùng cung cấp dữ liệu phù hợp mà ứng dụng của bạn cần để thực hiện nhiệm vụ, chẳng hạn như thu thập địa chỉ email hoặc số điện thoại hợp lệ. Mặc dù trạng thái lỗi sẽ thông báo cho người dùng về điều đang diễn ra, nhưng có thể bạn cần phải giữ trường có thông tin không chính xác cho trường này cho đến khi được khắc phục.

Để lấy tiêu điểm, bạn có thể gọi phương thức captureFocus() và giải phóng phương thức đó sau đó bằng phương thức freeFocus(), như trong ví dụ sau:

val textField = FocusRequester()

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

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

Mức độ ưu tiên của đối tượng sửa đổi tiêu điểm

Modifiers có thể được xem là các phần tử chỉ có một phần tử con. Vì vậy, khi bạn đưa các phần tử đó vào hàng đợi, mỗi Modifier ở bên trái (hoặc trên cùng) sẽ gói Modifier ở bên phải (hoặc bên dưới). Điều này có nghĩa là Modifier thứ hai nằm trong phần tử đầu tiên, để khi khai báo hai focusProperties, chỉ một cái trên cùng hoạt động, vì các phần tử sau đây được chứa ở trên cùng.

Để làm rõ hơn khái niệm này, hãy xem đoạn mã sau:

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

Trong trường hợp này, focusProperties cho biết item2 là tiêu điểm phù hợp sẽ không được sử dụng vì đã có trong tiêu điểm trước đó; do đó, item1 sẽ là tiêu điểm được sử dụng.

Khi sử dụng phương pháp này, cha mẹ cũng có thể đặt lại hành vi về mặc định bằng cách dùng FocusRequester.Default:

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

Thành phần mẹ không nhất thiết phải thuộc cùng một chuỗi đối tượng sửa đổi. Thành phần kết hợp mẹ có thể ghi đè thuộc tính tiêu điểm của thành phần kết hợp con. Chẳng hạn hãy xem xét FancyButton này khiến nút không thể làm tâm điểm:

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

Người dùng có thể đặt lại nút này làm tiêu điểm bằng cách đặt canFocus thành true:

FancyButton(Modifier.focusProperties { canFocus = true })

Giống như mọi Modifier, các sự kiện liên quan đến tâm điểm sẽ hoạt động theo cách khác nhau dựa trên thứ tự bạn khai báo các sự kiện đó. Chẳng hạn, mã như sau giúp Box có thể làm tâm điểm, nhưng FocusRequester không liên kết với thành phần có thể làm tâm điểm này vì nó được khai báo sau thành phần có thể làm tâm điểm.

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

Bạn cần nhớ là focusRequester liên kết với thành phần con có thể làm tâm điểm đầu tiên bên dưới nó trong hệ phân cấp. Vì vậy, focusRequester này sẽ trỏ đến thành phần con có thể làm tâm điểm đầu tiên. Trong trường hợp không có giá trị nào, Analytics sẽ không trỏ đến bất kỳ giá trị nào. Tuy nhiên, vì Box có thể làm tâm điểm (nhờ đối tượng sửa đổi focusable()) nên bạn có thể chuyển đến lớp này bằng cách điều hướng hai chiều.

Một ví dụ khác là cả hai cách sau đây đều hoạt động hiệu quả, vì đối tượng sửa đổi onFocusChanged() đề cập đến phần tử có thể làm tâm điểm đầu tiên xuất hiện sau đối tượng sửa đổi focusable() hoặc focusTarget().

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

Chuyển hướng tiêu điểm khi vào hoặc thoát

Đôi khi, bạn cần cung cấp một loại điều hướng rất cụ thể, chẳng hạn như loại hiển thị trong ảnh động dưới đây:

Ảnh động một màn hình cho thấy 2 cột nút được đặt cạnh nhau và tạo ảnh động tiêu điểm từ cột này sang cột kia.
Hình 3. Ảnh động của một màn hình cho thấy 2 cột nút được đặt cạnh nhau và tạo ảnh động tiêu điểm từ cột này sang cột khác

Trước khi tìm hiểu cách tạo tính năng này, bạn cần hiểu hành vi mặc định của tính năng tìm kiếm tâm điểm. Nếu không sửa đổi, thì một khi tính năng tìm kiếm tâm điểm chuyển đến mục Clickable 3, thao tác nhấn DOWN trên D-Pad (hoặc phím mũi tên tương đương) sẽ di chuyển tiêu điểm đến bất kỳ mục nào hiển thị bên dưới Column, thoát khỏi nhóm đó và bỏ qua mục ở bên phải. Nếu không có mục có thể làm tâm điểm, tiêu điểm sẽ không di chuyển đến bất cứ đâu mà vẫn nằm trên Clickable 3.

Để thay đổi hành vi này và cung cấp thành phần điều hướng như mong muốn, bạn có thể tận dụng đối tượng sửa đổi focusProperties. Công cụ này giúp bạn quản lý những gì sẽ xảy ra khi tính năng tìm kiếm tâm điểm chuyển vào hoặc thoát khỏi Composable:

val otherComposable = remember { FocusRequester() }

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

Bạn có thể hướng tiêu điểm đến một Composable cụ thể bất cứ khi nào nó vào hoặc thoát khỏi một phần nhất định của hệ phân cấp – ví dụ: khi giao diện người dùng của bạn có hai cột và bạn muốn đảm bảo rằng bất cứ khi nào cột đầu tiên được xử lý, tiêu điểm sẽ chuyển sang cột thứ hai:

Ảnh động một màn hình cho thấy 2 cột nút được đặt cạnh nhau và tạo ảnh động tiêu điểm từ cột này sang cột kia.
Hình 4. Ảnh động của một màn hình cho thấy 2 cột nút được đặt cạnh nhau và tạo ảnh động tiêu điểm từ cột này sang cột khác

Trong ảnh gif này, sau khi tiêu điểm đến Clickable 3 Composable trong Column 1, mục tiếp theo được lấy làm tâm điểm là Clickable 4 trong một Column khác. Bạn có thể thực hiện hành vi này bằng cách kết hợp focusDirection với các giá trị enterexit bên trong đối tượng sửa đổi focusProperties. Cả hai đều cần một lambda lấy tham số làm tham số hướng đến từ tiêu điểm và trả về FocusRequester. Hàm lambda này có thể hoạt động theo 3 cách: việc trả về FocusRequester.Cancel sẽ ngăn tiêu điểm tiếp tục, trong khi FocusRequester.Default không thay đổi hành vi của nó. Thay vào đó, việc cung cấp FocusRequester được đính kèm vào một Composable khác sẽ khiến tiêu điểm chuyển đến Composable cụ thể đó.

Thay đổi hướng tiến triển của trọng tâm

Để chuyển tiêu điểm đến mục tiếp theo hoặc hướng tới một hướng chính xác, bạn có thể sử dụng đối tượng sửa đổi onPreviewKey và ngụ ý LocalFocusManager để chuyển sang tiêu điểm bằng Đối tượng sửa đổi moveFocus.

Ví dụ sau đây cho thấy hành vi mặc định của cơ chế lấy nét: khi phát hiện thao tác nhấn phím tab, tiêu điểm sẽ chuyển đến phần tử tiếp theo trong danh sách tâm điểm. Mặc dù đây không phải là việc bạn thường cần định cấu hình, nhưng điều quan trọng là bạn phải biết hoạt động bên trong của hệ thống để có thể thay đổi hành vi mặc định.

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
        }
    }
)

Trong mẫu này, hàm focusManager.moveFocus() chuyển tiêu điểm đến mục được chỉ định hoặc đến hướng ngụ ý trong tham số hàm.