การผสานและการล้าง

เมื่อบริการการช่วยเหลือพิเศษไปยังส่วนต่างๆ ขององค์ประกอบบนหน้าจอ องค์ประกอบเหล่านี้ต้องจัดกลุ่ม แยก หรือแม้แต่ซ่อนไว้ในระดับความละเอียดที่เหมาะสม เมื่อคอมโพสิชันระดับล่างทุกรายการในหน้าจอไฮไลต์แยกกัน ผู้ใช้จะต้องโต้ตอบหลายครั้งเพื่อไปยังส่วนต่างๆ ของหน้าจอ หากผสานองค์ประกอบเข้าด้วยกันมากเกินไป ผู้ใช้อาจไม่เข้าใจว่าองค์ประกอบใดควรอยู่ด้วยกันตามหลักเหตุผล หากมีองค์ประกอบบนหน้าจอที่ตกแต่งเพื่อความสวยงามเท่านั้น บริการเพื่อการเข้าถึงอาจซ่อนองค์ประกอบเหล่านี้ไว้ ในกรณีเหล่านี้ คุณสามารถใช้ Compose API เพื่อผสาน ล้าง และซ่อนความหมายได้

ความหมายแบบผสาน

เมื่อคุณใช้ตัวแก้ไข clickable กับคอมโพสิชันหลัก คอมโพสิชันจะผสานองค์ประกอบย่อยทั้งหมดที่อยู่ภายใต้คอมโพสิชันนั้นโดยอัตโนมัติ หากต้องการทำความเข้าใจวิธีที่คอมโพเนนต์ Compose Material และ Foundation แบบอินเทอร์แอกทีฟใช้กลยุทธ์การผสานโดยค่าเริ่มต้น โปรดดูที่ส่วนองค์ประกอบแบบอินเทอร์แอกทีฟ

คอมโพเนนต์หนึ่งๆ อาจมีคอมโพสิเบิลหลายรายการ คอมโพสิเบิลเหล่านี้อาจสร้างกลุ่มตามตรรกะและแต่ละรายการอาจมีข้อมูลสำคัญ แต่คุณอาจยังต้องการให้บริการการช่วยเหลือพิเศษแสดงคอมโพสิเบิลเหล่านี้เป็นองค์ประกอบเดียว

ตัวอย่างเช่น ลองนึกถึงคอมโพสิเบิลที่แสดงรูปโปรไฟล์ ชื่อ และข้อมูลเพิ่มเติมของผู้ใช้

กลุ่มองค์ประกอบ UI ที่มีชื่อของผู้ใช้ ระบบจะเลือกชื่อ
รูปที่ 1 กลุ่มองค์ประกอบ UI ที่มีชื่อของผู้ใช้ เลือกชื่อแล้ว

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

@Composable
private fun PostMetadata(metadata: Metadata) {
    // Merge elements below for accessibility purposes
    Row(modifier = Modifier.semantics(mergeDescendants = true) {}) {
        Image(
            imageVector = Icons.Filled.AccountCircle,
            contentDescription = null // decorative
        )
        Column {
            Text(metadata.author.name)
            Text("${metadata.date}${metadata.readTimeMinutes} min read")
        }
    }
}

ตอนนี้บริการการช่วยเหลือพิเศษจะมุ่งเน้นที่ทั้งคอนเทนเนอร์พร้อมกันและผสานเนื้อหาของคอนเทนเนอร์ ดังนี้

กลุ่มองค์ประกอบ UI ที่มีชื่อของผู้ใช้ เลือกองค์ประกอบทั้งหมดพร้อมกัน
รูปที่ 2 กลุ่มองค์ประกอบ UI ที่มีชื่อของผู้ใช้ เลือกองค์ประกอบทั้งหมดพร้อมกัน

พร็อพเพอร์ตี้เชิงความหมายแต่ละรายการมีกลยุทธ์การผสานที่กําหนดไว้ เช่น พร็อพเพอร์ตี้ ContentDescription จะเพิ่มค่า ContentDescription ทั้งหมดที่สืบทอดมาลงในรายการ คุณสามารถตรวจสอบกลยุทธ์การผสานของพร็อพเพอร์ตี้เชิงอรรถศาสตร์ได้โดยตรวจสอบmergePolicyการใช้งานใน SemanticsProperties.kt พร็อพเพอร์ตี้อาจใช้ค่าหลักหรือค่าย่อย ผสานค่าเป็นลิสต์หรือสตริง ไม่อนุญาตให้ผสานเลยและแสดงข้อยกเว้นแทน หรือใช้กลยุทธ์การผสานที่กำหนดเองอื่นๆ

ยังมีสถานการณ์อื่นๆ ที่คุณคาดหวังว่าระบบจะผสานความหมายของคำย่อยเข้ากับคำหลัก แต่กลับไม่เกิดขึ้น ในตัวอย่างต่อไปนี้ เรามีรายการระดับบนสุด clickable ที่มีองค์ประกอบย่อย และเราอาจคาดหวังว่ารายการระดับบนสุดจะผสานองค์ประกอบย่อยทั้งหมด

รายการในลิสต์ที่มีรูปภาพ ข้อความบางส่วน และไอคอนบุ๊กมาร์ก
รูปที่ 3 รายการที่มีรูปภาพ ข้อความบางส่วน และไอคอนบุ๊กมาร์ก

@Composable
private fun ArticleListItem(
    openArticle: () -> Unit,
    addToBookmarks: () -> Unit,
) {

    Row(modifier = Modifier.clickable { openArticle() }) {
        // Merges with parent clickable:
        Icon(
            painter = painterResource(R.drawable.ic_logo),
            contentDescription = "Article thumbnail"
        )
        ArticleDetails()

        // Defies the merge due to its own clickable:
        BookmarkButton(onClick = addToBookmarks)
    }
}

เมื่อผู้ใช้กดclickable รายการ Row ระบบจะเปิดบทความ มีการฝัง BookmarkButton ไว้ด้านในเพื่อบุ๊กมาร์กบทความ ปุ่มที่ฝังอยู่นี้จะปรากฏขึ้นแบบไม่ได้ผสาน ขณะที่เนื้อหาย่อยที่เหลือในแถวจะผสานกัน

ต้นไม้ที่ผสานจะมีข้อความหลายรายการในรายการภายในโหนดแถว ต้นไม้ที่ยังไม่ได้ผสานจะมีโหนดแยกกันสำหรับคอมโพสิชันข้อความแต่ละรายการ
รูปที่ 4 ต้นไม้ที่ผสานจะมีข้อความหลายรายการในรายการภายในโหนด Row ต้นไม้ที่ยังไม่ได้ผสานจะมีโหนดแยกกันสำหรับTextคอมโพสิเบิล
แต่ละรายการ

คอมโพสิเบิลบางรายการจะไม่ผสานภายใต้รายการหลักโดยอัตโนมัติตามการออกแบบ องค์ประกอบหลักจะผสานองค์ประกอบย่อยไม่ได้เมื่อองค์ประกอบย่อยกำลังผสานอยู่ด้วย ไม่ว่าจะเป็นจากการตั้งค่า mergeDescendants = true อย่างชัดแจ้ง หรือเป็นคอมโพเนนต์ที่ผสานตัวเอง เช่น ปุ่มหรือองค์ประกอบที่คลิกได้ การทราบว่า API บางรายการผสานหรือปฏิเสธการผสานอย่างไรจะช่วยให้คุณแก้ไขข้อบกพร่องของลักษณะการทำงานบางอย่างที่อาจไม่คาดคิดได้

ใช้การผสานเมื่อองค์ประกอบย่อยประกอบกันเป็นกลุ่มที่สมเหตุสมผลและตรรกะภายใต้องค์ประกอบหลัก แต่หากรายการย่อยที่ฝังอยู่ต้องมีการปรับหรือนําความหมายของรายการย่อยออกด้วยตนเอง API อื่นๆ อาจเหมาะกับความต้องการของคุณมากกว่า (เช่น clearAndSetSemantics)

ล้างและตั้งค่าความหมาย

หากจำเป็นต้องล้างหรือเขียนทับข้อมูลเชิงความหมายทั้งหมด ให้ใช้ clearAndSetSemantics ซึ่งเป็น API ที่มีประสิทธิภาพ

เมื่อคอมโพเนนต์ต้องการล้างความหมายของคอมโพเนนต์เองและความหมายของคอมโพเนนต์ที่สืบทอดมา ให้ใช้ API นี้กับ Lambda ว่าง เมื่อต้องเขียนทับความหมาย ให้ใส่เนื้อหาใหม่ไว้ใน Lambda

โปรดทราบว่าเมื่อล้างด้วย Lambda ว่าง ระบบจะไม่ส่งความหมายที่ล้างแล้วไปยังผู้บริโภคที่ใช้ข้อมูลนี้ เช่น การช่วยเหลือพิเศษ การป้อนข้อความอัตโนมัติ หรือการทดสอบ เมื่อเขียนทับเนื้อหาด้วย clearAndSetSemantics{/*semantic information*/} ความหมายใหม่จะแทนที่ความหมายก่อนหน้าทั้งหมดขององค์ประกอบและองค์ประกอบย่อย

ต่อไปนี้คือตัวอย่างคอมโพเนนต์เปิด/ปิดที่กําหนดเอง ซึ่งแสดงด้วยแถวที่โต้ตอบได้โดยมีไอคอนและข้อความ

// Developer might intend this to be a toggleable.
// Using `clearAndSetSemantics`, on the Row, a clickable modifier is applied,
// a custom description is set, and a Role is applied.

@Composable
fun FavoriteToggle() {
    val checked = remember { mutableStateOf(true) }
    Row(
        modifier = Modifier
            .toggleable(
                value = checked.value,
                onValueChange = { checked.value = it }
            )
            .clearAndSetSemantics {
                stateDescription = if (checked.value) "Favorited" else "Not favorited"
                toggleableState = ToggleableState(checked.value)
                role = Role.Switch
            },
    ) {
        Icon(
            imageVector = Icons.Default.Favorite,
            contentDescription = null // not needed here

        )
        Text("Favorite?")
    }
}

แม้ว่าไอคอนและข้อความจะมีข้อมูลเชิงความหมายบางอย่าง แต่ก็ไม่ได้บ่งชี้ว่าคอมโพเนนต์นี้เปิด/ปิดได้ การผสานไม่เพียงพอเนื่องจากคุณต้องให้ข้อมูลเพิ่มเติมเกี่ยวกับคอมโพเนนต์

เนื่องจากข้อมูลโค้ดด้านบนสร้างคอมโพเนนต์เปิด/ปิดที่กําหนดเอง คุณจึงต้องเพิ่มความสามารถในการเปิด/ปิด รวมถึงความหมายของ stateDescription, toggleableState และ role วิธีนี้จะช่วยให้คอมโพเนนต์มีสถานะและการดําเนินการที่เกี่ยวข้อง เช่น TalkBack จะประกาศว่า "แตะสองครั้งเพื่อเปิด/ปิด" แทนที่จะเป็น "แตะสองครั้งเพื่อเปิดใช้งาน"

การล้างความหมายเดิมและตั้งค่าความหมายใหม่ซึ่งสื่อความหมายได้ชัดเจนยิ่งขึ้นจะช่วยให้บริการการช่วยเหลือพิเศษเห็นว่านี่คือคอมโพเนนต์ที่เปิด/ปิดได้ซึ่งสลับสถานะได้

เมื่อใช้ clearAndSetSemantics ให้พิจารณาสิ่งต่อไปนี้

  • เนื่องจากบริการไม่ได้รับข้อมูลเมื่อตั้งค่า API นี้ คุณจึงควรใช้ API นี้อย่างจำกัด
    • ตัวแทน AI และบริการที่คล้ายกันอาจใช้ข้อมูลเชิงอรรถศาสตร์เพื่อทำความเข้าใจหน้าจอ ดังนั้นคุณจึงควรล้างข้อมูลดังกล่าวเมื่อจำเป็นเท่านั้น
  • คุณอาจตั้งค่าความหมายที่กำหนดเองภายใน API Lambda ได้
  • ลําดับของตัวแก้ไขมีความสำคัญ เนื่องจาก API นี้จะล้างความหมายทั้งหมดที่อยู่หลังตําแหน่งที่ใช้ โดยไม่คำนึงถึงกลยุทธ์การผสานอื่นๆ

ซ่อนความหมาย

ในบางสถานการณ์ คุณไม่จำเป็นต้องส่งองค์ประกอบไปยังบริการการช่วยเหลือพิเศษ เนื่องจากข้อมูลเพิ่มเติมขององค์ประกอบนั้นอาจไม่จำเป็นต่อการช่วยเหลือพิเศษ หรือเป็นเพียงการตกแต่งภาพเท่านั้นและไม่มีการทำงานแบบอินเทอร์แอกทีฟ ในกรณีเหล่านี้ คุณสามารถซ่อนองค์ประกอบด้วย hideFromAccessibility API

ตัวอย่างต่อไปนี้คือคอมโพเนนต์ที่อาจต้องซ่อน ได้แก่ ลายน้ำที่ซ้ำซ้อนซึ่งครอบคลุมคอมโพเนนต์ และตัวอักษรที่ใช้แยกข้อมูลเพื่อตกแต่ง

@Composable
fun WatermarkExample(
    watermarkText: String,
    content: @Composable () -> Unit,
) {
    Box {
        WatermarkedContent()
        // Mark the watermark as hidden to accessibility services.
        WatermarkText(
            text = watermarkText,
            color = Color.Gray.copy(alpha = 0.5f),
            modifier = Modifier
                .align(Alignment.BottomEnd)
                .semantics { hideFromAccessibility() }
        )
    }
}

@Composable
fun DecorativeExample() {
    Text(
        modifier =
        Modifier.semantics {
            hideFromAccessibility()
        },
        text = "A dot character that is used to decoratively separate information, like •"
    )
}

การใช้ hideFromAccessibility ที่นี่จะทำให้ระบบซ่อนลายน้ำและการตกแต่งจากบริการการช่วยเหลือพิเศษ แต่ยังคงเก็บความหมายขององค์ประกอบไว้สำหรับกรณีการใช้งานอื่นๆ เช่น การทดสอบ

รายละเอียดกรณีการใช้งาน

ต่อไปนี้เป็นสรุป Use Case เพื่อทำความเข้าใจวิธีแยกความแตกต่างระหว่าง API เวอร์ชันเก่าอย่างชัดเจน

  • กรณีที่เนื้อหาไม่ได้มีไว้เพื่อให้บริการการช่วยเหลือพิเศษนำไปใช้
    • ใช้ hideFromAccessibility เมื่อเนื้อหาอาจเป็นการตกแต่งหรือซ้ำซ้อน แต่ยังคงต้องทดสอบ
    • ใช้ clearAndSetSemantics{} กับ Lambda ว่างเมื่อต้องล้างความหมายขององค์ประกอบหลักและองค์ประกอบย่อยสำหรับบริการทั้งหมด
    • ใช้ clearAndSetSemantics{/*content*/} กับเนื้อหาภายใน Lambda เมื่อต้องตั้งค่าความหมายของคอมโพเนนต์ด้วยตนเอง
  • กรณีที่ควรถือว่าเนื้อหาเป็นเอนทิตีเดียวและข้อมูลของเนื้อหาย่อยทั้งหมดต้องสมบูรณ์
    • ใช้รายการที่สืบทอดตามความหมายแบบผสาน
ตารางที่มีกรณีการใช้งาน API ที่ต่างกัน
รูปที่ 5 ตารางที่มีกรณีการใช้งาน API ที่ต่างกัน