Compose의 접근성

Compose로 작성된 앱은 요구사항이 서로 다른 여러 사용자의 접근성을 지원해야 합니다. 접근성 서비스는 화면에 표시된 내용을 특정한 요구사항이 있는 사용자에게 보다 적합한 형식으로 변환하는 데 사용됩니다. 접근성 서비스를 지원하기 위해 앱은 Android 프레임워크의 API를 사용하여 UI 요소에 관한 시맨틱 정보를 노출합니다. 그런 다음 Android 프레임워크에서 이 시맨틱 정보를 접근성 서비스에 전달합니다. 각 접근성 서비스에서 사용자에게 앱을 가장 잘 설명하는 방법을 선택할 수 있습니다. Android는 TalkBack, 스위치 제어 등 여러 접근성 서비스를 제공합니다.

시맨틱

Compose는 시맨틱 속성을 사용하여 접근성 서비스에 정보를 전달합니다. 시맨틱 속성은 사용자에게 표시되는 UI 요소에 관한 정보를 제공합니다. TextButton과 같은 대부분의 내장 컴포저블은 이 시맨틱 속성을 컴포저블 및 컴포저블의 하위 요소로부터 추론된 정보로 채웁니다. toggleableclickable과 같은 일부 수정자는 특정 시맨틱 속성도 설정합니다. 하지만 프레임워크에서 사용자에게 UI 요소를 설명하는 방법을 이해하기 위해 더 많은 정보가 필요한 경우도 있습니다.

이 문서에서는 Android 프레임워크에 올바로 설명할 수 있도록 컴포저블에 추가 정보를 명시적으로 추가해야 하는 여러 가지 상황을 설명합니다. 또한 특정 컴포저블의 시맨틱 정보를 완전히 바꾸는 방법을 설명합니다. 여기서는 Android의 접근성을 기본적으로 이해하고 있다고 가정합니다.

일반적인 사용 사례

접근성 기능이 필요한 사용자가 앱을 성공적으로 사용할 수 있도록 지원하려면 앱이 이 페이지에 설명된 권장사항을 따라야 합니다.

터치 영역 최소 크기 고려

사용자가 클릭, 터치 등의 방법으로 상호작용할 수 있는 화면상의 요소는 안정적으로 상호작용할 수 있도록 충분히 커야 합니다. 이러한 요소의 크기를 조절할 때 Material Design 접근성 가이드라인을 정확히 준수하도록 최소 크기를 48dp로 설정해야 합니다.

Checkbox, RadioButton, Switch, Slider, Surface와 같은 Material 구성요소는 이 최소 크기를 내부적으로 설정합니다. 단, 구성요소가 사용자 작업을 수신할 수 있는 경우에 한합니다. 예를 들어 CheckboxonCheckedChange 매개변수가 null이 아닌 값으로 설정된 경우 너비와 높이가 최소 48dp인 패딩이 포함됩니다.

@Composable
private fun CheckableCheckbox() {
    Checkbox(checked = true, onCheckedChange = {})
}

onCheckedChange 매개변수가 null로 설정된 경우 구성요소와 직접 상호작용할 수 없으므로 패딩이 포함되지 않습니다.

@Composable
private fun NonClickableCheckbox() {
    Checkbox(checked = true, onCheckedChange = null)
}

Switch, RadioButton, Checkbox와 같은 선택 설정을 구현할 경우 일반적으로 클릭 가능한 동작을 상위 컨테이너로 올리고, 컴포저블에 관한 클릭 콜백을 null로 설정하고, toggleable 또는 selectable 수정자를 상위 컴포저블에 추가합니다.

@Composable
private fun CheckableRow() {
    MaterialTheme {
        var checked by remember { mutableStateOf(false) }
        Row(
            Modifier
                .toggleable(
                    value = checked,
                    role = Role.Checkbox,
                    onValueChange = { checked = !checked }
                )
                .padding(16.dp)
                .fillMaxWidth()
        ) {
            Text("Option", Modifier.weight(1f))
            Checkbox(checked = checked, onCheckedChange = null)
        }
    }
}

클릭 가능한 컴포저블의 크기가 터치 영역 최소 크기보다 작은 경우 Compose는 여전히 터치 영역 크기를 늘립니다. 이 작업은 컴포저블의 경계 밖으로 터치 영역 크기를 확장하여 이뤄집니다.

다음 예에서는 클릭 가능한 아주 작은 Box를 만듭니다. 터치 영역 부분은 Box의 경계 밖으로 자동으로 확장되므로 Box 옆을 탭하면 여전히 클릭 이벤트가 트리거됩니다.

@Composable
private fun SmallBox() {
    var clicked by remember { mutableStateOf(false) }
    Box(
        Modifier
            .size(100.dp)
            .background(if (clicked) Color.DarkGray else Color.LightGray)
    ) {
        Box(
            Modifier
                .align(Alignment.Center)
                .clickable { clicked = !clicked }
                .background(Color.Black)
                .size(1.dp)
        )
    }
}

서로 다른 컴포저블의 터치 영역이 서로 겹치지 않도록 항상 컴포저블에 충분히 큰 최소 크기를 사용해야 합니다. 이 예에서는 sizeIn 수정자를 사용하여 내부 상자의 최소 크기를 설정하는 것을 의미합니다.

@Composable
private fun LargeBox() {
    var clicked by remember { mutableStateOf(false) }
    Box(
        Modifier
            .size(100.dp)
            .background(if (clicked) Color.DarkGray else Color.LightGray)
    ) {
        Box(
            Modifier
                .align(Alignment.Center)
                .clickable { clicked = !clicked }
                .background(Color.Black)
                .sizeIn(minWidth = 48.dp, minHeight = 48.dp)
        )
    }
}

클릭 라벨 추가

클릭 라벨을 사용하여 컴포저블의 클릭 동작에 시맨틱 의미를 추가할 수 있습니다. 클릭 라벨은 사용자가 컴포저블과 상호작용할 때 발생하는 결과를 설명합니다. 접근성 서비스는 클릭 라벨을 사용하여 특정 요구사항이 있는 사용자에게 앱을 설명할 수 있습니다.

clickable 수정자에 매개변수를 전달하여 클릭 라벨을 설정합니다.

@Composable
private fun ArticleListItem(openArticle: () -> Unit) {
    Row(
        Modifier.clickable(
            // R.string.action_read_article = "read article"
            onClickLabel = stringResource(R.string.action_read_article),
            onClick = openArticle
        )
    ) {
        // ..
    }
}

또는 클릭 가능한 수정자에 액세스할 수 없는 경우 시맨틱 수정자에 클릭 라벨을 설정할 수 있습니다.

@Composable
private fun LowLevelClickLabel(openArticle: () -> Boolean) {
    // R.string.action_read_article = "read article"
    val readArticleLabel = stringResource(R.string.action_read_article)
    Canvas(
        Modifier.semantics {
            onClick(label = readArticleLabel, action = openArticle)
        }
    ) {
        // ..
    }
}

시각적 요소 설명

Image 또는 Icon 컴포저블을 정의하는 경우 Android 프레임워크에서 표시되는 내용을 자동으로 파악할 수 없습니다. 시각적 요소의 텍스트 설명을 전달해야 합니다.

사용자가 현재 페이지를 친구와 공유할 수 있는 화면이 있다고 가정해 보겠습니다. 이 화면에는 클릭 가능한 공유 아이콘이 포함되어 있습니다.

클릭 가능한 아이콘 모음,

Android 프레임워크는 아이콘만 가지고는 시각 장애를 가진 사용자에게 아이콘에 대해 설명할 수 없습니다. Android 프레임워크에는 아이콘의 추가 텍스트 설명이 필요합니다.

contentDescription 매개변수는 시각적 요소를 설명하는 데 사용됩니다. 설명은 사용자에게 전달되므로 현지화된 문자열을 사용해야 합니다.

@Composable
private fun ShareButton(onClick: () -> Unit) {
    IconButton(onClick = onClick) {
        Icon(
            imageVector = Icons.Filled.Share,
            contentDescription = stringResource(R.string.label_share)
        )
    }
}

일부 시각적 요소는 완전히 장식용이므로 사용자에게 전달하지 않아도 됩니다. contentDescription 매개변수를 null로 설정하면 Android 프레임워크에 이 요소에 연결된 작업 또는 상태가 없음을 나타냅니다.

@Composable
private fun PostImage(post: Post, modifier: Modifier = Modifier) {
    val image = post.imageThumb ?: painterResource(R.drawable.placeholder_1_1)

    Image(
        painter = image,
        // Specify that this image has no semantic meaning
        contentDescription = null,
        modifier = modifier
            .size(40.dp, 40.dp)
            .clip(MaterialTheme.shapes.small)
    )
}

특정 시각적 요소에 contentDescription이 필요한지 여부는 개발자가 결정합니다. 해당 요소에서 사용자가 작업을 처리하는 데 필요한 정보를 전달하는지 자문해 보세요. 그렇지 않으면 설명을 사용하지 않는 것이 좋습니다.

요소 병합

TalkBack 및 스위치 제어와 같은 접근성 서비스를 사용하면 사용자가 화면의 요소 간에 포커스를 이동할 수 있습니다. 요소의 올바른 세부사항에 포커스를 맞춰야 합니다. 화면에서 낮은 수준의 모든 단일 컴포저블에 독립적으로 포커스를 맞추는 경우 사용자가 화면 전체에서 이동하기 위해 상호작용을 많이 해야 합니다. 요소가 지나치게 병합된 경우 어떤 요소가 서로 결합되어 있는지 사용자가 알지 못할 수도 있습니다.

컴포저블에 clickable 수정자를 적용하면 Compose는 자신이 포함된 모든 요소를 자동으로 병합합니다. ListItem의 경우에도 마찬가지입니다. 목록 항목 내 요소가 병합되며 접근성 서비스에서 하나의 요소로 간주합니다.

컴포저블의 집합이 논리적 그룹을 구성할 수 있지만 이 그룹은 클릭할 수 없거나 목록 항목의 일부가 아닙니다. 접근성 서비스에서 계속 컴포저블을 하나의 요소로 간주하도록 할 수 있습니다. 예를 들어 사용자의 아바타, 이름, 추가 정보를 보여주는 컴포저블이 있다고 가정해 보겠습니다.

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

semantics 수정자의 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 요소의 그룹. 모든 요소가 함께 선택되어 있습니다.

맞춤 작업 추가

다음 목록 항목을 살펴보세요.

일반적인 목록 항목, 기사 제목, 저자, 북마크 아이콘이 포함됨

TalkBack과 같은 스크린 리더를 사용하여 화면에 표시된 내용을 듣는 경우 스크린 리더는 먼저 전체 항목을 선택한 다음 북마크 아이콘을 선택합니다.

목록 항목, 모든 요소가 함께 선택됨

목록 항목, 북마크 아이콘이 선택됨

목록이 길면 이 작업이 매우 반복적일 수 있습니다. 더 나은 접근 방식은 사용자가 항목을 북마크할 수 있는 맞춤 작업을 정의하는 것입니다. 북마크 아이콘 자체의 동작을 명시적으로 삭제하여 접근성 서비스에서 북마크 아이콘을 선택하지 않도록 해야 합니다. clearAndSetSemantics 수정자를 사용하면 됩니다.

@Composable
private fun PostCardSimple(
    /* ... */
    isFavorite: Boolean,
    onToggleFavorite: () -> Boolean
) {
    val actionLabel = stringResource(
        if (isFavorite) R.string.unfavorite else R.string.favorite
    )
    Row(
        modifier = Modifier
            .clickable(onClick = { /* ... */ })
            .semantics {
                // Set any explicit semantic properties
                customActions = listOf(
                    CustomAccessibilityAction(actionLabel, onToggleFavorite)
                )
            }
    ) {
        /* ... */
        BookmarkButton(
            isBookmarked = isFavorite,
            onClick = onToggleFavorite,
            // Clear any semantics properties set on this node
            modifier = Modifier.clearAndSetSemantics { }
        )
    }
}

요소의 상태 설명

컴포저블은 Android 프레임워크에서 컴포저블의 상태를 읽는 데 사용되는 시맨틱의 stateDescription을 정의할 수 있습니다. 예를 들어 전환 가능한 한 컴포저블의 상태는 '선택됨' 또는 '선택 해제됨'일 수 있습니다. 경우에 따라 Compose에서 사용하는 기본 상태 설명 라벨을 재정의할 수 있습니다. 이렇게 하려면 컴포저블을 전환 가능한 컴포저블로 정의하기 전에 상태 설명 라벨을 명시적으로 지정하면 됩니다.

@Composable
private fun TopicItem(itemTitle: String, selected: Boolean, onToggle: () -> Unit) {
    val stateSubscribed = stringResource(R.string.subscribed)
    val stateNotSubscribed = stringResource(R.string.not_subscribed)
    Row(
        modifier = Modifier
            .semantics {
                // Set any explicit semantic properties
                stateDescription = if (selected) stateSubscribed else stateNotSubscribed
            }
            .toggleable(
                value = selected,
                onValueChange = { onToggle() }
            )
    ) {
        /* ... */
    }
}

제목 정의

앱에서 한 화면의 스크롤 가능한 컨테이너에 많은 콘텐츠를 표시하는 경우가 있습니다. 예를 들어 사용자가 읽고 있는 기사의 전체 내용을 화면에 표시할 수 있습니다.

블로그 게시물의 스크린샷. 스크롤 가능한 컨테이너에 있는 기사 텍스트가 있음

접근성 기능이 필요한 사용자는 이러한 화면을 탐색하는 데 어려움이 있습니다. 탐색에 도움이 되도록 어느 요소가 제목인지 표시할 수 있습니다. 위의 예에서 각 하위 섹션 제목을 접근성을 위한 제목으로 정의할 수 있습니다. TalkBack과 같은 일부 접근성 서비스를 사용하면 사용자가 제목 간을 직접 이동할 수 있습니다.

Compose에서는 시맨틱 속성을 정의하여 컴포저블이 제목임을 나타냅니다.

@Composable
private fun Subsection(text: String) {
    Text(
        text = text,
        style = MaterialTheme.typography.headlineSmall,
        modifier = Modifier.semantics { heading() }
    )
}

접근성 속성 자동 테스트

위에 나열된 사용 사례를 따르는 경우와 같이 앱의 시맨틱 속성을 맞춤설정할 때 자동화된 UI 테스트를 사용하여 정확성을 확인하고 회귀를 방지할 수 있습니다.

예를 들어 요소의 클릭 라벨이 올바르게 설정되어 있는지 테스트하려면 다음 코드를 사용합니다.

@Test
fun test() {
    composeTestRule
        .onNode(nodeMatcher)
        .assert(
            SemanticsMatcher("onClickLabel is set correctly") {
                it.config.getOrNull(SemanticsActions.OnClick)?.label == "My Click Label"
            }
        )
}

맞춤 하위 수준 컴포저블 만들기

고급 사용 사례에서는 앱의 특정 Material 구성요소를 맞춤 버전으로 바꿉니다. 이 시나리오에서는 접근성 고려사항을 염두에 두어야 합니다. Material Checkbox를 자체 구현으로 바꾼다고 가정해 보겠습니다. 이 구성요소의 접근성 속성을 처리하는 triStateToggleable 수정자를 추가하는 것을 잊어버리기 쉽습니다.

일반적으로 Material 라이브러리에서 구성요소 구현을 확인하고, 찾을 수 있는 접근성 동작을 모방해야 합니다. 또한 UI 수준 수정자가 아니라 기초 수정자를 많이 활용하세요. 기초 수정자에는 접근성 고려 사항이 기본적으로 포함되어 있습니다. 여러 접근성 서비스로 맞춤 구성요소 구현을 테스트하여 동작을 확인해야 합니다.

isTraversalGrouptraversalIndex를 사용하여 순회 순서 수정

기본적으로 Compose 앱의 접근성 스크린 리더 동작은 예상되는 읽기 순서로 구현됩니다. 이는 일반적으로 왼쪽에서 오른쪽, 그런 다음 위에서 아래로 진행됩니다. 그러나 추가 힌트 없이는 알고리즘이 실제 읽기 순서를 결정할 수 없는 앱 레이아웃 유형이 있습니다. 뷰 기반 앱에서는 traversalBeforetraversalAfter 속성을 사용하여 이러한 문제를 해결할 수 있습니다. Compose 1.5부터 Compose는 똑같이 유연한 API를 제공하지만 새로운 개념 모델을 제공합니다.

isTraversalGrouptraversalIndex는 기본 정렬 알고리즘이 적절하지 않은 시나리오에서 접근성 및 TalkBack 포커스 순서를 제어할 수 있는 시맨틱 속성입니다. isTraversalGroup는 의미상 중요한 그룹을 식별하는 반면 traversalIndex는 이러한 그룹 내에서 개별 요소의 순서를 조정합니다. isTraversalGroup만 단독으로 사용하거나 추가 맞춤설정을 위해 traversalIndex와 함께 사용할 수 있습니다.

이 페이지에서는 앱에서 isTraversalGrouptraversalIndex를 사용하여 스크린 리더 순회 순서를 제어하는 방법을 설명합니다.

isTraversalGroup를 사용하여 요소 그룹화

isTraversalGroup시맨틱 노드가 순회 그룹인지 여부를 정의하는 불리언 속성입니다. 이 유형의 노드는 하위 요소를 구성할 때 경계 또는 테두리 역할을 하는 노드입니다.

노드에 isTraversalGroup = true를 설정하면 다른 요소로 이동하기 전에 해당 노드의 모든 하위 요소를 방문합니다. 스크린 리더가 아닌 포커스 가능 노드(예: 열, 행, 상자)에는 isTraversalGroup를 설정할 수 있습니다.

이 예에서는 스니펫이 isTraversalGroup를 사용하도록 수정되었습니다. 아래 스니펫은 4개의 텍스트 요소를 내보냅니다. 왼쪽 2개 요소는 하나의 CardBox 요소에 속하고, 오른쪽 2개 요소는 다른 CardBox 요소에 속합니다.

// CardBox() function takes in top and bottom sample text.
@Composable
fun CardBox(
    topSampleText: String,
    bottomSampleText: String,
    modifier: Modifier = Modifier
) {
    Box(modifier) {
        Column {
            Text(topSampleText)
            Text(bottomSampleText)
        }
    }
}

@Composable
fun TraversalGroupDemo() {
    val topSampleText1 = "This sentence is in "
    val bottomSampleText1 = "the left column."
    val topSampleText2 = "This sentence is "
    val bottomSampleText2 = "on the right."
    Row {
        CardBox(
            topSampleText1,
            bottomSampleText1
        )
        CardBox(
            topSampleText2,
            bottomSampleText2
        )
    }
}

이 코드는 다음과 비슷한 출력을 생성합니다.

왼쪽 열에는 'This sentence is in the left column'이, 오른쪽 열에는 'This sentence is on the right'이 표시된 두 개의 텍스트 열이 있는 레이아웃입니다.
그림 1. 두 문장으로 구성된 레이아웃 (왼쪽 열에 하나는 오른쪽 열에 하나씩)

시맨틱이 설정되지 않았으므로 스크린 리더의 기본 동작은 왼쪽에서 오른쪽으로, 위에서 아래로 요소를 순회하는 것입니다. 이 기본값 때문에 TalkBack은 문장 조각을 잘못된 순서로 읽습니다.

'This sentence is in' → 'This sentence is' → 'the left column.' → '오른쪽에.'

프래그먼트를 올바르게 정렬하려면 원래 스니펫을 수정하여 isTraversalGrouptrue로 설정하세요.

@Composable
fun TraversalGroupDemo2() {
    val topSampleText1 = "This sentence is in "
    val bottomSampleText1 = "the left column."
    val topSampleText2 = "This sentence is"
    val bottomSampleText2 = "on the right."
    Row {
        CardBox(
//      1,
            topSampleText1,
            bottomSampleText1,
            Modifier.semantics { isTraversalGroup = true }
        )
        CardBox(
//      2,
            topSampleText2,
            bottomSampleText2,
            Modifier.semantics { isTraversalGroup = true }
        )
    }
}

isTraversalGroup는 각 CardBox에서 구체적으로 설정되므로 요소를 정렬할 때 CardBox 경계가 적용됩니다. 이 경우 왼쪽 CardBox을 먼저 읽은 후 오른쪽 CardBox를 읽습니다.

이제 TalkBack은 문장 조각을 올바른 순서로 읽습니다.

"이 문장은" → "왼쪽 열" → '이 문장은' → '오른쪽에.'

TrasalIndex로 순회 순서 추가 맞춤설정

traversalIndex는 TalkBack 순회 순서를 맞춤설정할 수 있는 부동 속성입니다. 요소를 그룹화하는 것만으로는 TalkBack이 올바르게 작동하지 않는 경우 traversalIndexisTraversalGroup와 함께 사용하여 스크린 리더 순서를 추가로 맞춤설정할 수 있습니다.

traversalIndex 속성에는 다음과 같은 특성이 있습니다.

  • traversalIndex 값이 낮은 요소에 우선순위가 높습니다.
  • 양수 또는 음수일 수 있습니다.
  • 기본값은 0f입니다.
  • 텍스트나 버튼과 같은 화면상의 요소와 같이 스크린 리더에 포커스를 둘 수 있는 노드에만 영향을 미칩니다. 예를 들어 Column에 isTraversalGroup도 설정되어 있지 않으면 Column에 traversalIndex만 설정해도 아무런 효과가 없습니다.

다음 예는 traversalIndexisTraversalGroup를 함께 사용하는 방법을 보여줍니다.

예: 순회 시계 페이스

시계 페이스는 표준 순회 순서가 작동하지 않는 일반적인 시나리오입니다. 이 섹션의 예는 시간 선택 도구를 기반으로 합니다. 사용자는 시계 페이스에서 숫자를 번갈아 가면서 시와 분 단위의 숫자를 선택할 수 있습니다.

위에 시간 선택기가 있는 시계 페이스
그림 2. 시계 페이스 이미지

다음의 단순화된 스니펫에는 12부터 시작하여 시계 방향으로 원을 중심으로 12개의 숫자가 그려지는 CircularLayout가 있습니다.

@Composable
fun ClockFaceDemo() {
    CircularLayout {
        repeat(12) { hour ->
            ClockText(hour)
        }
    }
}

@Composable
private fun ClockText(value: Int) {
    Box(modifier = Modifier) {
        Text((if (value == 0) 12 else value).toString())
    }
}

시계 페이스는 기본적인 왼쪽에서 오른쪽 및 위에서 아래로 순서로 논리적으로 판독되지 않으므로 TalkBack은 숫자를 비순차적으로 읽습니다. 이 문제를 해결하려면 다음 스니펫과 같이 증분 카운터 값을 사용하세요.

@Composable
fun ClockFaceDemo() {
    CircularLayout(Modifier.semantics { isTraversalGroup = true }) {
        repeat(12) { hour ->
            ClockText(hour)
        }
    }
}

@Composable
private fun ClockText(value: Int) {
    Box(modifier = Modifier.semantics { this.traversalIndex = value.toFloat() }) {
        Text((if (value == 0) 12 else value).toString())
    }
}

순회 순서를 올바르게 설정하려면 먼저 CircularLayout를 순회 그룹으로 만들고 isTraversalGroup = true을 설정합니다. 그런 다음 각 시계 텍스트가 레이아웃에 그려지면 상응하는 traversalIndex를 카운터 값으로 설정합니다.

카운터 값은 계속 증가하기 때문에 화면에 숫자가 추가될 때 각 클록 값의 traversalIndex는 더 커집니다. 클록 값 0의 traversalIndex는 0이고 클록 값 1의 traversalIndex는 1인 식입니다. 이렇게 하면 TalkBack에서 읽는 순서가 설정됩니다. 이제 CircularLayout 내부의 숫자가 예상된 순서대로 읽힙니다.

설정된 traversalIndexes는 동일한 그룹 내의 다른 색인에만 상대적이므로 화면 순서의 나머지 부분은 그대로 유지됩니다. 즉, 위 코드 스니펫에 표시된 의미론적 변경사항은 isTraversalGroup = true가 설정된 시계 페이스 내의 순서만 수정합니다.

CircularLayout's 의미 체계를 isTraversalGroup = true로 설정하지 않아도 traversalIndex 변경사항이 계속 적용됩니다. 그러나 이를 바인딩하는 CircularLayout가 없으면 화면의 다른 모든 요소를 방문한 후 시계 페이스의 12자리가 마지막으로 읽힙니다. 이는 다른 모든 요소의 기본 traversalIndex0f이고 시계 텍스트 요소가 다른 모든 0f 요소 이후에 읽히기 때문입니다.

예: 플로팅 작업 버튼의 순회 순서 맞춤설정

이 예에서는 traversalIndexisTraversalGroup를 사용하여 Material Design 플로팅 작업 버튼 (FAB)의 순회 순서를 제어합니다. 이 예는 다음 레이아웃을 기반으로 합니다.

상단 앱 바, 샘플 텍스트, 플로팅 작업 버튼, 하단 앱 바가 있는 레이아웃
그림 3. 상단 앱 바, 샘플 텍스트, 플로팅 작업 버튼, 하단 앱 바가 있는 레이아웃

기본적으로 위 레이아웃의 TalkBack 순서는 다음과 같습니다.

상단 앱 바 → 샘플 텍스트 0~6 → 플로팅 작업 버튼 (FAB) → 하단 앱 바

스크린 리더가 먼저 FAB에 포커스를 맞추도록 할 수 있습니다. FAB와 같은 Material 요소에 traversalIndex를 설정하려면 다음을 실행하세요.

@Composable
fun FloatingBox() {
    Box(modifier = Modifier.semantics { isTraversalGroup = true; traversalIndex = -1f }) {
        FloatingActionButton(onClick = {}) {
            Icon(imageVector = Icons.Default.Add, contentDescription = "fab icon")
        }
    }
}

이 스니펫에서 isTraversalGrouptrue로 설정된 상자를 만들고 동일한 상자에 traversalIndex를 설정하면(-1f은 기본값 0f보다 낮음) 플로팅 상자가 화면의 다른 모든 요소 앞에 표시됩니다.

이제 플로팅 상자와 기타 요소를 Scaffold에 배치하여 간단한 Material Design 레이아웃을 구현합니다.

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ColumnWithFABFirstDemo() {
    Scaffold(
        topBar = { TopAppBar(title = { Text("Top App Bar") }) },
        floatingActionButtonPosition = FabPosition.End,
        floatingActionButton = { FloatingBox() },
        content = { padding -> ContentColumn(padding = padding) },
        bottomBar = { BottomAppBar { Text("Bottom App Bar") } }
    )
}

TalkBack은 다음 순서로 요소와 상호작용합니다.

FAB → 상단 앱 바 → 샘플 텍스트 0~6 → 하단 앱 바

자세히 알아보기

Compose 코드의 접근성 지원에 관해 자세히 알아보려면 Jetpack Compose의 접근성 Codelab을 참고하세요.