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

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

รวมความหมาย

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

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

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

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

คุณสามารถเปิดใช้ Compose เพื่อผสานองค์ประกอบเหล่านี้ได้โดยใช้พารามิเตอร์ 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 เพื่อบุ๊กมาร์กบทความ ปุ่มที่ฝังนี้จะแสดงเป็นแบบไม่ผสานรวม ในขณะที่เนื้อหาที่ฝังอื่นๆ ภายในแถวจะผสานรวมกัน

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

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

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

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

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

เมื่อคอมโพเนนต์ต้องการล้างความหมายของตัวเองและองค์ประกอบสืบทอด ให้ใช้ 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 Agent และบริการที่คล้ายกันอาจใช้ข้อมูลความหมายเพื่อทำความเข้าใจหน้าจอ ดังนั้นจึงควรล้างข้อมูลเมื่อจำเป็นเท่านั้น
  • คุณอาจตั้งค่าความหมายที่กำหนดเองภายใน Lambda ของ API ได้
  • ลำดับของตัวแก้ไขมีความสำคัญ โดย 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 ที่นี่ช่วยให้มั่นใจได้ว่าลายน้ำและการตกแต่งจะถูกซ่อนจากบริการการช่วยเหลือพิเศษ แต่ยังคงรักษาความหมายไว้สำหรับกรณีการใช้งานอื่นๆ เช่น การทดสอบ

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

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

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