เปลี่ยนลักษณะการทำงานของการโฟกัส

บางครั้งอาจจำเป็นต้องลบล้างลักษณะการทำงานของการโฟกัสเริ่มต้นขององค์ประกอบ บนหน้าจอ ตัวอย่างเช่น คุณอาจต้องการจัดกลุ่ม Composable ป้องกัน โฟกัสไปที่คำสั่ง Composable ขอโฟกัสที่ชัดเจนกับ 1 จับภาพหรือปล่อยโฟกัส หรือเปลี่ยนเส้นทางโฟกัสเมื่อเข้าหรือออก ช่วงเวลานี้ อธิบายวิธีเปลี่ยนลักษณะการโฟกัสเมื่อค่าเริ่มต้นไม่ใช่สิ่งที่คุณต้องการ ความต้องการ

ให้การนำทางที่สอดคล้องกับการสนทนากลุ่ม

บางครั้ง Jetpack Compose ไม่ได้คาดเดารายการถัดไปที่ถูกต้องโดยทันที การนำทางแบบแท็บ โดยเฉพาะเมื่อ Composables ระดับบนสุดที่ซับซ้อน เช่น แท็บและ รายการที่เข้าร่วม

แม้ว่าการค้นหาโฟกัสมักจะเป็นไปตามลำดับการประกาศของComposables ซึ่งเป็นไปไม่ได้ในบางกรณี เช่นเมื่อ Composables ใน ลำดับชั้นเป็นการเลื่อนในแนวนอนได้และมองไม่เห็นบางส่วน ซึ่งจะแสดงใน ตัวอย่างด้านล่าง

Jetpack Compose อาจตัดสินใจโฟกัสรายการถัดไปที่ใกล้กับจุดเริ่มต้นของ ดังที่แสดงด้านล่าง แทนการดำเนินการต่อตามเส้นทางที่คุณคาดหวังไว้ การนำทางแบบทิศทางเดียว:

วันที่ ภาพเคลื่อนไหวของแอปแสดงการนำทางแนวนอนด้านบนและรายการสินค้าด้านล่าง
รูปที่ 1 ภาพเคลื่อนไหวของแอปแสดงการนำทางแนวนอนด้านบนและรายการสินค้าด้านล่าง

ในตัวอย่างนี้แสดงให้เห็นอย่างชัดเจนว่านักพัฒนาแอปไม่ได้มีเจตนาที่จะมุ่งเน้นที่ ข้ามจากแท็บช็อกโกแลตไปยังรูปภาพแรกด้านล่าง แล้วกลับไปที่ แท็บขนมอบ แต่ต้องการให้โฟกัสที่แท็บนั้นต่อไปจนกว่า แท็บสุดท้าย แล้วเน้นไปที่เนื้อหาด้านใน

วันที่ ภาพเคลื่อนไหวของแอปแสดงการนำทางแนวนอนด้านบนและรายการสินค้าด้านล่าง
รูปที่ 2 ภาพเคลื่อนไหวของแอปแสดงการนำทางแนวนอนด้านบนและรายการสินค้าด้านล่าง

ในสถานการณ์ที่กลุ่มของ Composable มีความสำคัญ ตามลำดับ เช่น ในแถวแท็บจากตัวอย่างก่อนหน้านี้ คุณจะต้องตัดข้อความ Composable ในกลุ่มหลักที่มีตัวปรับแต่ง focusGroup():

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

การนำทางแบบ 2 ทิศทางจะค้นหา Composable ที่ใกล้เคียงที่สุดสำหรับ ทิศทาง—หากองค์ประกอบจากกลุ่มอื่นอยู่ใกล้กว่าที่มองไม่เห็นทั้งหมด รายการในกลุ่มปัจจุบัน การนำทางจะเลือกรายการที่ใกล้เคียงที่สุด เพื่อหลีกเลี่ยงปัญหานี้ คุณสามารถใช้แป้นกดร่วม focusGroup()

FocusGroup ทำให้ทั้งกลุ่มดูเหมือนบุคคลเดียวในแง่ของการมุ่งเน้น แต่กลุ่มเองก็จะไม่เป็นจุดสนใจ แต่เด็กที่สนิทที่สุดจะ เพื่อให้ได้โฟกัสแทน วิธีนี้ทำให้การนำทางรู้ว่าต้องไปยัง URL ที่มองไม่เห็นทั้งหมด ก่อนออกจากกลุ่ม

ในกรณีนี้ FilterChip ทั้ง 3 อินสแตนซ์จะโฟกัสก่อน SweetsCard รายการ แม้ว่า SweetsCards จะมองเห็นได้ทั้งหมดกับ ผู้ใช้และ FilterChip บางส่วนอาจซ่อนอยู่ ซึ่งเกิดขึ้นเนื่องจาก ตัวแก้ไข focusGroup จะบอกตัวจัดการโฟกัสให้ปรับลำดับของรายการ เพื่อให้ไปยังส่วนต่างๆ ได้ง่ายและสอดคล้องกับ UI มากขึ้น

เมื่อไม่มีตัวปรับ focusGroup หาก FilterChipC ไม่ปรากฏ ให้โฟกัส จะมาเป็นคนเลือกเป็นลำดับสุดท้าย อย่างไรก็ตาม การเพิ่มคีย์ตัวปรับแต่งดังกล่าวจะทำให้ ค้นพบได้เท่านั้น แต่จะได้รับโฟกัสทันทีหลัง FilterChipB ด้วย ที่ผู้ใช้คาดหวัง

การทำให้โฟกัสที่ประกอบกันได้

Composable บางประเภทสามารถโฟกัสได้ด้วยการออกแบบ เช่น Button หรือ Composable ที่มี แป้นกดร่วม clickable ที่แนบมาด้วย ถ้าคุณต้องการเพิ่ม ลักษณะการทำงานที่โฟกัสได้ไปยัง Composable คุณจะใช้ตัวแก้ไข focusable

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

การทำให้ Compos ไม่สามารถโฟกัสได้

อาจมีบางสถานการณ์ที่องค์ประกอบบางอย่างของคุณไม่ควรมีส่วนร่วม เราโฟกัสในเรื่องนี้ ในกรณีที่ไม่ค่อยเกิดขึ้นเหล่านี้ คุณสามารถใช้ 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 เข้ากับ Composable ที่คุณต้องการย้ายโฟกัสของแป้นพิมพ์ไป ในโค้ดต่อไปนี้ ข้อมูลโค้ด ออบเจ็กต์ FocusRequester จะเชื่อมโยงกับ TextField โดยการตั้งค่า แป้นกดร่วมที่ชื่อ Modifier.focusRequester:

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

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

คุณสามารถเรียกใช้เมธอด requestFocus ของ FocusRequester เพื่อส่งคำขอโฟกัสจริงได้ คุณควรเรียกใช้เมธอดนี้นอกบริบท 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 เครื่องที่ 2 อยู่ใน รายการแรก ดังนั้นเมื่อประกาศ focusProperties สองรายการ ให้แสดงเฉพาะระดับบนสุด รายการแรกใช้งานได้ โดยมีชิ้นงานต่อไปนี้อยู่บนสุด

หากต้องการอธิบายแนวคิดให้ชัดเจนมากขึ้น โปรดดูโค้ดต่อไปนี้

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

ในกรณีนี้ focusProperties ที่ระบุว่า item2 เป็นโฟกัสที่ถูกต้องจะ ไม่มีการใช้ เนื่องจากมีอยู่ในข้อความก่อนหน้านี้ ดังนั้น item1 จะเป็นค่า ใช้ไป 1 รูป

หากใช้วิธีนี้ ผู้ปกครองสามารถรีเซ็ตลักษณะการทำงานให้เป็นค่าเริ่มต้นได้ โดยใช้ FocusRequester.Default:

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

ระดับบนสุดไม่จำเป็นต้องเป็นส่วนหนึ่งของกลุ่มตัวปรับแต่งเดียวกัน ผู้ปกครอง Composable สามารถเขียนทับคุณสมบัติโฟกัสของ Composable ย่อยได้ ตัวอย่างเช่น ให้ลองพิจารณา 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()) คุณจะไปยังส่วนต่างๆ ได้โดยใช้การนำทางแบบ 2 ทิศทาง

อีกตัวอย่างหนึ่งคือตัวอย่างต่อไปนี้ onFocusChanged() ตัวปรับหมายถึงองค์ประกอบแรกที่โฟกัสได้ที่ปรากฏหลังจาก แป้นกดร่วม focusable() หรือ focusTarget()

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

เปลี่ยนเส้นทางโฟกัสเมื่อเข้าหรือออก

ในบางครั้ง คุณอาจต้องระบุการนำทางที่เฉพาะเจาะจงมาก ที่แสดงในภาพเคลื่อนไหวด้านล่าง

วันที่ ภาพเคลื่อนไหวของหน้าจอแสดงปุ่ม 2 คอลัมน์ที่วางอยู่ข้างกันและย้ายโฟกัสจากคอลัมน์หนึ่งไปยังอีกคอลัมน์หนึ่ง
รูปที่ 3 ภาพเคลื่อนไหวของหน้าจอแสดงปุ่ม 2 คอลัมน์ที่วางอยู่ข้างกันและย้ายโฟกัสจากคอลัมน์หนึ่งไปยังอีกคอลัมน์หนึ่ง

ก่อนที่เราจะเจาะลึกถึงวิธีสร้างโฆษณานี้ คุณควรเข้าใจค่าเริ่มต้น พฤติกรรมของการค้นหาโฟกัส โดยไม่ต้องแก้ไขใดๆ เมื่อการค้นหาโฟกัส ไปถึงรายการ Clickable 3 การกด DOWN บน D-pad (หรือจำนวนที่เทียบเท่า แป้นลูกศร) จะเลื่อนโฟกัสไปยังสิ่งที่แสดงใต้ Column ออกจากกลุ่มและไม่สนใจกลุ่มทางขวา หากไม่มี มีรายการที่สามารถโฟกัสได้ โฟกัสจะไม่ย้ายไปที่อื่น แต่ยังคงอยู่ Clickable 3

หากต้องการเปลี่ยนลักษณะการทำงานนี้และให้การนำทางตามที่ต้องการ คุณสามารถใช้ประโยชน์จาก แป้นกดร่วม focusProperties ซึ่งจะช่วยให้คุณจัดการสิ่งที่จะเกิดขึ้นเมื่อโฟกัส การค้นหาจะเข้าหรือออกจาก Composable:

val otherComposable = remember { FocusRequester() }

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

คุณสามารถส่งโฟกัสไปที่ Composable ที่เฉพาะเจาะจงเมื่อใดก็ตามที่ หรือออกจากบางส่วนของลำดับชั้น เช่น เมื่อ UI มี และคุณต้องการแน่ใจว่าเมื่อใดก็ตามที่คอลัมน์แรกได้รับการประมวลผล โฟกัสจะสลับไปที่ตัวที่สอง:

วันที่ ภาพเคลื่อนไหวของหน้าจอแสดงปุ่ม 2 คอลัมน์ที่วางอยู่ข้างกันและย้ายโฟกัสจากคอลัมน์หนึ่งไปยังอีกคอลัมน์หนึ่ง
รูปที่ 4 ภาพเคลื่อนไหวของหน้าจอแสดงปุ่ม 2 คอลัมน์ที่วางอยู่ข้างกันและย้ายโฟกัสจากคอลัมน์หนึ่งไปยังอีกคอลัมน์หนึ่ง

ใน GIF นี้ เมื่อโฟกัสอยู่ที่ Clickable 3 Composable ใน Column 1 รายการถัดไปที่โฟกัสอยู่คือ Clickable 4 ใน Column อีกรายการ ลักษณะการทำงานนี้ ด้วยการรวม focusDirection เข้ากับ enter และ exit ภายในตัวปรับแต่ง focusProperties ทั้งคู่ต้องการแลมบ์ดาที่ใช้เวลา เป็นพารามิเตอร์ว่าจุดโฟกัสมาจากไหนและส่งคืน FocusRequester แลมบ์ดานี้มีลักษณะ 3 ประการ ได้แก่ การย้อนกลับ FocusRequester.Cancel จะหยุดโฟกัสไม่ให้ใช้งานต่อ FocusRequester.Default จะไม่เปลี่ยนลักษณะการทำงาน การป้อนพารามิเตอร์ FocusRequester ที่แนบมากับ Composable อีกรายการทำให้โฟกัสข้ามไปที่นั่น 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() จะย้ายโฟกัสไปที่ รายการที่ระบุ หรือทิศทางโดยนัยในพารามิเตอร์ฟังก์ชัน