기본적으로 Compose 앱의 접근성 스크린 리더 동작은 예상되는 읽기 순서로 구현됩니다. 이는 일반적으로 왼쪽에서 오른쪽, 그런 다음 위에서 아래로 진행됩니다.
그러나 추가 힌트 없이는 알고리즘이 실제 읽기 순서를 결정할 수 없는 앱 레이아웃 유형이 있습니다. 뷰 기반 앱에서는 traversalBefore
및 traversalAfter
속성을 사용하여 이러한 문제를 해결할 수 있습니다.
Compose 1.5부터 Compose는 똑같이 유연한 API를 제공하지만 새로운 개념 모델을 제공합니다.
isTraversalGroup
및 traversalIndex
는 기본 정렬 알고리즘이 적절하지 않은 시나리오에서 접근성 및 TalkBack 포커스 순서를 제어할 수 있는 시맨틱 속성입니다. isTraversalGroup
는 의미상 중요한 그룹을 식별하는 반면 traversalIndex
는 이러한 그룹 내에서 개별 요소의 순서를 조정합니다. isTraversalGroup
만 단독으로 사용하거나 추가 맞춤설정을 위해 traversalIndex
와 함께 사용할 수 있습니다.
앱에서 isTraversalGroup
및 traversalIndex
를 사용하여 스크린 리더 순회 순서를 제어합니다.
isTraversalGroup
를 사용하여 요소 그룹화
isTraversalGroup
는 시맨틱 노드가 순회 그룹인지 여부를 정의하는 불리언 속성입니다. 이 유형의 노드는 노드의 하위 요소를 구성할 때 경계 또는 경계선 역할을 하는 노드입니다.
노드에 isTraversalGroup = true
를 설정하면 다른 요소로 이동하기 전에 해당 노드의 모든 하위 요소를 방문합니다. 스크린 리더가 아닌 포커스 가능 노드(예: 열, 행, 상자)에는 isTraversalGroup
를 설정할 수 있습니다.
다음 예에서는 isTraversalGroup
를 사용합니다. 4개의 텍스트 요소를 내보냅니다. 왼쪽의 두 요소는 하나의 CardBox
요소에 속하고, 오른쪽 두 요소는 다른 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 ) } }
이 코드는 다음과 비슷한 출력을 생성합니다.
시맨틱이 설정되지 않았으므로 스크린 리더의 기본 동작은 왼쪽에서 오른쪽으로, 위에서 아래로 요소를 순회하는 것입니다. 이 기본값 때문에 TalkBack은 문장 조각을 잘못된 순서로 읽습니다.
'This sentence is in' → 'This sentence is' → 'the left column.' → '오른쪽에.'
프래그먼트를 올바르게 정렬하려면 원래 스니펫을 수정하여 isTraversalGroup
를 true
로 설정하세요.
@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은 문장 조각을 올바른 순서로 읽습니다.
"이 문장은" → "왼쪽 열" → '이 문장은' → '오른쪽에.'
순회 순서 추가 맞춤설정
traversalIndex
은 TalkBack 순회 순서를 맞춤설정할 수 있는 부동 속성입니다. 요소를 그룹화하는 것만으로 TalkBack이 올바르게 작동하지 않는 경우 traversalIndex
를 isTraversalGroup
와 함께 사용하여 스크린 리더 순서를 추가로 맞춤설정합니다.
traversalIndex
속성에는 다음과 같은 특성이 있습니다.
traversalIndex
값이 낮은 요소에 우선순위가 높습니다.- 양수 또는 음수일 수 있습니다.
- 기본값은
0f
입니다. - 텍스트나 버튼과 같은 화면상의 요소와 같이 스크린 리더에 포커스를 둘 수 있는 노드에만 영향을 미칩니다. 예를 들어 열에
isTraversalGroup
도 설정되어 있지 않으면 열에traversalIndex
만 설정해도 아무 효과가 없습니다.
다음 예는 traversalIndex
와 isTraversalGroup
를 함께 사용하는 방법을 보여줍니다.
예: 순회 시계 페이스
시계 페이스는 표준 순회 순서가 작동하지 않는 일반적인 시나리오입니다. 이 섹션의 예는 사용자가 시계 페이스의 숫자를 탐색하고 시와 분 단위의 숫자를 선택할 수 있는 시간 선택 도구입니다.
다음의 단순화된 스니펫에는 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자리가 마지막으로 읽힙니다. 이는 다른 모든 요소의 기본 traversalIndex
가 0f
이고 시계 텍스트 요소가 다른 모든 0f
요소 이후에 읽히기 때문입니다.
예: 플로팅 작업 버튼의 순회 순서 맞춤설정
이 예에서 traversalIndex
및 isTraversalGroup
는 Material Design 플로팅 작업 버튼 (FAB)의 순회 순서를 제어합니다. 이 예의 기초는 다음과 같은 레이아웃입니다.
기본적으로 이 예의 레이아웃에는 다음과 같은 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") } } }
이 스니펫에서 isTraversalGroup
가 true
로 설정된 상자를 만들고 동일한 상자에 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 → 하단 앱 바
추가 리소스
- 접근성: 모든 Android 앱 개발에 공통된 필수 개념 및 기법
- 접근성 높은 앱 빌드: 앱의 접근성을 높이기 위해 취할 수 있는 주요 단계
- 앱 접근성 개선 원칙: 앱의 접근성을 개선할 때 유의해야 할 주요 원칙
- 접근성 테스트: Android 접근성의 원칙 및 도구 테스트