순회 순서 수정

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

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

앱에서 isTraversalGrouptraversalIndex를 사용하여 스크린 리더 탐색 순서를 제어합니다.

탐색을 위한 요소 그룹화

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

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

텍스트 열이 두 개인 레이아웃으로 왼쪽 열에는 '이 문장은 왼쪽 열에 있습니다'라고, 오른쪽 열에는 '이 문장은 오른쪽에 있습니다'라고 표시되어 있습니다.
그림 1. 두 문장 (왼쪽 열에 하나, 오른쪽 열에 하나)이 있는 레이아웃

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

'이 문장은' → '이 문장은' → '왼쪽 열에 있습니다.' → '오른쪽'

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

'이 문장은' → '왼쪽 열에 있습니다.' → '이 문장은' → '옳습니다.'

순회 순서 맞춤설정

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

traversalIndex 속성의 특성은 다음과 같습니다.

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

다음 예는 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 요소 뒤에 읽히기 때문에 발생합니다.

API 고려사항

탐색 API를 사용할 때는 다음을 고려하세요.

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