순회 순서 수정

순회 순서는 접근성 서비스가 UI 요소를 탐색하는 순서입니다. Compose 앱에서 요소는 예상되는 읽기 순서로 정렬되며, 이는 일반적으로 왼쪽에서 오른쪽, 위에서 아래로 진행됩니다. 그러나 Compose에서 올바른 읽기 순서를 결정하기 위해 추가 힌트가 필요한 시나리오도 있습니다.

isTraversalGroup 및 traversalIndex는 Compose의 기본 정렬 알고리즘이 충분하지 않은 시나리오에서 접근성 서비스의 순회 순서에 영향을 줄 수 있는 시맨틱 속성입니다. isTraversalGroup는 맞춤설정이 필요한 의미론적으로 중요한 그룹을 식별하고 traversalIndex는 이러한 그룹 내 개별 요소의 순서를 조정합니다. isTraversalGroup만 사용하여 그룹 내의 모든 요소를 함께 선택해야 함을 나타내거나 traversalIndex와 함께 사용하여 추가 맞춤설정을 할 수 있습니다.

앱에서 isTraversalGroup 및 traversalIndex를 사용하여 스크린 리더 순회 순서를 제어합니다.

순회를 위한 요소 그룹화

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
        )
    }
}

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

두 개의 텍스트 열이 있는 레이아웃으로, 왼쪽 열에는 '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." → "오른쪽에."

프래그먼트를 올바르게 정렬하려면 원본 스니펫을 수정하여 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은 문장 조각을 올바른 순서로 읽습니다.

"이 문장은" → "왼쪽 열에 있습니다." → 'This sentence is' → 'on the right.'

순회 순서 맞춤설정

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

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

  • traversalIndex 값이 낮은 요소가 먼저 우선순위가 지정됩니다.
  • 양수 또는 음수일 수 있습니다.
  • 기본값은 0f입니다.
  • 순회 색인이 순회 동작에 영향을 미치려면 텍스트나 버튼과 같은 화면 요소와 같이 접근성 서비스에서 선택하고 포커스를 맞출 수 있는 구성요소에 설정해야 합니다.
    • 예를 들어 Column에서 traversalIndex만 설정하면 열에 isTraversalGroup도 설정되어 있지 않는 한 아무런 효과가 없습니다.

다음 예시에서는 traversalIndex 및 isTraversalGroup를 함께 사용하는 방법을 보여줍니다.

시계 페이스는 표준 순회 순서가 작동하지 않는 일반적인 시나리오입니다. 이 섹션의 예는 사용자가 시계 페이스의 숫자를 탐색하고 시 및 분 슬롯의 숫자를 선택할 수 있는 시간 선택 도구입니다.

시계 페이스 위에 시간 선택 도구가 있습니다.
그림 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자리 숫자가 마지막으로 읽힙니다. 이는 다른 모든 요소의 기본 traversalIndex가 0f이고 시계 텍스트 요소가 다른 모든 0f 요소 다음에 읽히기 때문입니다.

API 고려사항

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

  • isTraversalGroup = true는 그룹화된 요소를 포함하는 상위 요소에 설정해야 합니다.
  • traversalIndex는 시맨틱스를 포함하고 접근성 서비스에서 선택할 하위 구성요소에 설정해야 합니다.
  • 조사 중인 모든 요소가 동일한 zIndex 수준에 있는지 확인합니다. 이는 시맨틱과 순회 순서에도 영향을 미치기 때문입니다.
  • 시맨틱스가 불필요하게 병합되지 않도록 합니다. 이는 순회 색인이 적용되는 구성요소에 영향을 미칠 수 있습니다.