병합 및 삭제

접근성 서비스가 화면의 요소를 탐색할 때 이러한 요소가 적절한 세부사항으로 그룹화, 분리 또는 숨겨져야 합니다. 화면에서 낮은 수준의 모든 단일 컴포저블이 독립적으로 강조 표시되면 사용자가 화면 전체에서 이동하기 위해 상호작용을 많이 해야 합니다. 요소가 지나치게 병합된 경우 어떤 요소가 논리적으로 서로 결합되어 있는지 사용자가 알지 못할 수도 있습니다. 화면에 장식용 요소가 있는 경우 접근성 서비스에서 숨길 수 있습니다. 이 경우 Compose API를 사용하여 시맨틱을 병합, 지우기, 숨길 수 있습니다.

병합 시맨틱스

상위 컴포저블에 clickable 수정자를 적용하면 Compose는 그 아래의 모든 하위 요소를 자동으로 병합합니다. 대화형 Compose Material 및 Foundation 구성요소가 기본적으로 병합 전략을 사용하는 방법을 알아보려면 대화형 요소 섹션을 참고하세요.

구성요소는 일반적으로 여러 컴포저블로 구성됩니다. 이러한 컴포저블은 논리적 그룹을 형성할 수 있으며 각각 중요한 정보를 포함할 수 있지만 접근성 서비스에서 계속 컴포저블을 하나의 요소로 간주하도록 할 수 있습니다.

예를 들어 사용자의 아바타, 이름, 추가 정보를 보여주는 컴포저블을 생각해 보세요.

사용자 이름이 포함된 UI 요소의 그룹. 이름이 선택되어 있습니다.
그림 1. 사용자 이름이 포함된 UI 요소의 그룹. 이름이 선택되어 있습니다.

시맨틱 수정자의 mergeDescendants 매개변수를 사용하여 이러한 요소를 병합하도록 Compose를 사용 설정할 수 있습니다. 이렇게 하면 접근성 서비스가 구성요소를 하나의 항목으로 취급하고 하위 요소의 모든 시맨틱 속성이 병합됩니다.

@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 값을 목록에 추가합니다. SemanticsProperties.kt에서 mergePolicy 구현을 확인하여 시맨틱 속성의 병합 전략을 확인할 수 있습니다. 속성은 상위 값이나 하위 값을 가져오거나 값을 목록이나 문자열에 병합하거나 병합을 아예 허용하지 않고 대신 예외를 발생시키거나 다른 맞춤 병합 전략을 사용할 수 있습니다.

하위 시맨틱이 상위 시맨틱으로 병합될 것으로 예상되지만 실제로는 병합되지 않는 다른 시나리오도 있습니다. 다음 예에서는 하위 요소가 있는 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)가 더 적합할 수 있습니다.

시맨틱 지우기 및 설정

시맨틱 정보를 완전히 삭제하거나 덮어써야 하는 경우 사용할 수 있는 강력한 API는 clearAndSetSemantics입니다.

구성요소의 자체 시맨틱스와 하위 요소 시맨틱스를 지워야 하는 경우 빈 람다와 함께 이 API를 사용합니다. 시맨틱을 재정의해야 하는 경우 람다 내에 새 콘텐츠를 포함합니다.

빈 람다로 지우는 경우 접근성, 자동 완성, 테스트와 같이 이 정보를 사용하는 소비자에게 지워진 시맨틱이 전송되지 않습니다. 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에서는 'Double tap to activate' 대신 'Double tap to toggle'이라고 안내합니다.

이제 원래 시맨틱을 지우고 더 설명적인 새 시맨틱을 설정하면 접근성 서비스에서 이 요소가 상태를 전환할 수 있는 전환 가능한 구성요소임을 알 수 있습니다.

clearAndSetSemantics를 사용할 때는 다음 사항을 고려하세요.

  • 이 API가 설정되면 서비스는 정보를 수신하지 않으므로 가급적 사용하지 않는 것이 좋습니다.
    • 시맨틱 정보는 AI 상담사 및 유사한 서비스에서 화면을 이해하는 데 사용할 수 있으므로 필요한 경우에만 삭제해야 합니다.
  • 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{}를 사용하세요.
    • 구성요소의 시맨틱을 수동으로 설정해야 하는 경우 람다 내의 콘텐츠와 함께 clearAndSetSemantics{/*content*/}를 사용합니다.
  • 콘텐츠가 하나의 항목으로 취급되어야 하고 모든 하위 항목의 정보가 완전해야 하는 경우:
    • 시맨틱 하위 요소 병합을 사용합니다.
차별화된 API 사용 사례가 포함된 표
그림 5. 차별화된 API 사용 사례가 있는 표