Jetpack Compose의 접근성

1. 소개

이 Codelab에서는 Jetpack Compose를 사용하여 앱의 접근성을 개선하는 방법을 알아봅니다. 몇 가지 일반 사용 사례를 살펴보고 단계별로 샘플 앱을 개선할 것입니다. 그리고 터치 영역 크기, 콘텐츠 설명, 클릭 라벨 등에 관해서도 다룰 것입니다.

시각 장애, 색맹, 청각 장애, 수동기민성 장애, 인지 장애가 있는 사용자를 비롯한 많은 장애인이 일상생활에서 Android 기기를 사용하여 작업을 완료합니다. 접근성을 염두에 두고 앱을 개발할 때는 특히 이러한 장애가 있는 사용자, 그리고 그밖에 다른 접근성 기능이 필요한 사용자를 고려하여 사용자 환경을 개선해야 합니다.

이 Codelab에서는 TalkBack을 사용하여 코드 변경을 수동으로 테스트합니다. TalkBack은 주로 시각 장애가 있는 사용자가 사용하는 접근성 서비스입니다. 다른 접근성 서비스(예: 스위치 제어)로도 코드 변경을 테스트해야 합니다.

Jetnews의 홈 화면을 이동하는 직사각형에 포커스가 지정된 TalkBack. TalkBack 공지사항 문구는 화면 하단에 표시됩니다.

Jetnews 앱에서 TalkBack이 작동하는 모습.

학습할 내용

이 Codelab에서는 다음에 관해 알아봅니다.

  • 터치 영역 크기를 늘려 수동기민성 장애가 있는 사용자를 지원하는 방법
  • 시맨틱 속성에 관한 내용 및 시맨틱 속성 변경 방법
  • 접근성을 높이기 위해 컴포저블에 정보를 제공하는 방법

필요한 항목

빌드할 항목

이 Codelab에서는 뉴스 읽기 앱의 접근성을 개선합니다. 배운 내용을 중요한 접근성 기능이 누락된 앱에 적용하여 접근성이 필요한 사람들에게 더 유용한 앱으로 만들어 봅니다.

2. 설정

이를 위해 이 단계에서는 간단한 뉴스 리더 앱을 구성하는 코드를 다운로드합니다.

필요한 항목

코드 가져오기

이 Codelab의 코드는 android-compose-codelabs GitHub 저장소에서 찾을 수 있습니다. 클론하려면 다음을 실행합니다.

$ git clone https://github.com/android/codelab-android-compose

또는 ZIP 파일 두 개를 다운로드해도 됩니다.

샘플 앱 확인

다운로드한 코드에는 사용 가능한 모든 Compose Codelab용 코드가 포함되어 있습니다. 이 Codelab을 완료하려면 Android 스튜디오 내에서 AccessibilityCodelab 프로젝트를 엽니다.

main 브랜치의 코드로 시작하고 각자의 속도에 맞게 Codelab을 단계별로 따라하는 것이 좋습니다.

TalkBack 설정

이 Codelab에서는 TalkBack을 사용하여 변경사항을 확인합니다. 테스트에 실제 기기를 사용할 경우 다음 안내에 따라 TalkBack을 사용 설정합니다. 기본적으로 에뮬레이터는 TalkBack이 설치된 상태로 제공되지 않습니다. Play 스토어가 포함된 에뮬레이터를 선택하고 Android 접근성 도구 모음을 다운로드합니다.

3. 터치 영역 크기

사용자가 클릭, 터치 등의 방법으로 상호작용할 수 있는 화면상의 요소는 안정적으로 상호작용할 수 있도록 충분히 커야 합니다. 이러한 요소의 너비와 높이가 최소 48dp인지 확인합니다.

이러한 컨트롤의 크기가 동적으로 설정된 경우, 즉 콘텐츠 크기에 따라 조절되는 경우 sizeIn 수정자를 사용하여 크기의 하한선을 설정해 보세요.

일부 머티리얼 구성요소는 이러한 크기를 자동으로 설정합니다. 예를 들어 Button 컴포저블은 MinHeight가 36dp로 설정되어 있고 8dp의 세로 패딩을 사용합니다. 그러면 필요한 높이가 48dp가 됩니다.

샘플 앱을 열고 TalkBack을 실행하면 게시물 카드의 X표 아이콘에 매우 작은 터치 영역이 있습니다. 이 터치 영역을 48dp 이상으로 만들고자 합니다.

다음은 왼쪽에는 원래 앱이, 오른쪽에는 개선된 솔루션이 나와 있는 스크린샷입니다.

왼쪽에 있는 작은 윤곽선의 X표 아이콘과 오른쪽에 있는 큰 윤곽선의 X표 아이콘을 보여주는 목록 항목 비교

구현을 살펴보고 이 컴포저블의 크기를 확인해 보겠습니다. PostCards.kt를 열고 PostCardHistory 컴포저블을 찾습니다. 보시다시피, 구현은 더보기 메뉴 아이콘의 크기를 24dp로 설정합니다.

@Composable
fun PostCardHistory(post: Post, navigateToArticle: (String) -> Unit) {
   // ...

   Row(
       // ...
   ) {
       // ...
       CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
           Icon(
               imageVector = Icons.Default.Close,
               contentDescription = stringResource(R.string.cd_show_fewer),
               modifier = Modifier
                   .clickable { openDialog = true }
                   .size(24.dp)
           )
       }
   }
   // ...
}

Icon의 터치 영역 크기를 늘리려면 패딩을 추가하면 됩니다.

@Composable
fun PostCardHistory(post: Post, navigateToArticle: (String) -> Unit) {
   // ...
   Row(
       // ...
   ) {
       // ...
       CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
           Icon(
               imageVector = Icons.Default.Close,
               contentDescription = stringResource(R.string.cd_show_fewer),
               modifier = Modifier
                   .clickable { openDialog = true }
                   .padding(12.dp)
                   .size(24.dp)
           )
       }
   }
   // ...
}

이 사용 사례에서는 터치 영역을 최소 48dp로 만드는 더 쉬운 방법이 있습니다. 이를 자동으로 처리해주는 머티리얼 구성요소 IconButton을 사용하면 됩니다.

@Composable
fun PostCardHistory(post: Post, navigateToArticle: (String) -> Unit) {
   // ...
   Row(
       // ...
   ) {
       // ...
       CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
           IconButton(onClick = { openDialog = true }) {
               Icon(
                   imageVector = Icons.Default.Close,
                   contentDescription = stringResource(R.string.cd_show_fewer)
               )
           }
       }
   }
   // ...
}

이제 TalkBack을 사용하여 화면을 탐색하면 48dp의 터치 영역이 올바르게 표시됩니다. 또한 IconButton은 물결 효과 표시도 추가합니다. 이 표시를 통해 사용자는 요소가 클릭 가능함을 알 수 있습니다.

4. 라벨 클릭

앱의 클릭 가능한 요소는 기본적으로 그 요소를 클릭하면 어떤 작업이 발생하는지에 관한 정보를 제공하지 않습니다. 따라서 TalkBack과 같은 접근성 서비스에는 매우 일반적인 기본 설명이 사용됩니다.

접근성 요구사항이 있는 사용자에게 최상의 환경을 제공하기 위해 Google은 사용자가 관련 요소를 클릭하면 어떻게 되는지 설명하는 특정 설명을 제공할 수 있습니다.

사용자는 Jetnews 앱에서 전체 게시물을 읽기 위해 다양한 게시물 카드를 클릭할 수 있습니다. 이 경우 기본적으로 클릭 가능한 요소의 콘텐츠와 'Double tap to activate'라는 텍스트가 차례대로 읽힙니다. 대신 우리는 좀 더 구체적으로 'Double tap to read article'을 사용하고자 합니다. 다음은 원래 버전과 우리의 이상적인 솔루션을 비교해 놓은 것입니다.

TalkBack이 사용 설정된 두 개의 화면 녹화. 세로 목록의 게시물과 가로 캐러셀의 게시물이 탭됨.

컴포저블의 클릭 라벨이 변경됨. 변경 전(왼쪽)과 변경 후(오른쪽) 비교.

clickable 수정자에는 이 클릭 라벨을 직접 설정할 수 있는 매개변수가 포함되어 있습니다.

PostCardHistory 구현을 다시 살펴보겠습니다.

@Composable
fun PostCardHistory(
   // ...
) {
   Row(
       Modifier.clickable { navigateToArticle(post.id) }
   ) {
       // ...
   }
}

보시다시피 이 구현은 clickable 수정자를 사용합니다. 클릭 라벨을 설정하려면 onClickLabel 매개변수를 설정하면 됩니다.

@Composable
fun PostCardHistory(
   // ...
) {
   Row(
       Modifier.clickable(
               // R.string.action_read_article = "read article"
               onClickLabel = stringResource(R.string.action_read_article)
           ) {
               navigateToArticle(post.id)
           }
   ) {
       // ...
   }
}

이제 TalkBack에서 올바로 "Double tap to read article"이라고 알립니다.

홈 화면의 다른 게시물 카드에 동일한 일반 클릭 라벨이 있습니다. PostCardPopular 컴포저블의 구현을 살피고 관련 클릭 라벨을 업데이트해 보겠습니다.

@Composable
fun PostCardPopular(
   // ...
) {
   Card(
       shape = MaterialTheme.shapes.medium,
       modifier = modifier.size(280.dp, 240.dp),
       onClick = { navigateToArticle(post.id) }
   ) {
       // ...
   }
}

이 컴포저블은 내부적으로 Card 컴포저블을 사용하므로 클릭 라벨을 직접 설정할 수 없습니다. 대신 semantics 수정자를 사용하여 클릭 라벨을 설정하면 됩니다.

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun PostCardPopular(
   post: Post,
   navigateToArticle: (String) -> Unit,
   modifier: Modifier = Modifier
) {
   val readArticleLabel = stringResource(id = R.string.action_read_article)
   Card(
       shape = MaterialTheme.shapes.medium,
       modifier = modifier
          .size(280.dp, 240.dp)
          .semantics { onClick(label = readArticleLabel, action = null) },
       onClick = { navigateToArticle(post.id) }
   ) {
       // ...
   }
}

5. 맞춤 작업

대다수 앱은 일종의 목록을 표시하고 이 목록의 각 항목에는 하나 이상의 작업이 포함됩니다. 스크린 리더를 사용할 때 이러한 목록을 탐색하면 다소 지루할 수 있습니다. 동일한 작업이 반복적으로 포커스를 받기 때문입니다.

대신 컴포저블에 맞춤 접근성 작업을 추가할 수 있습니다. 그렇게 하면 동일한 목록 항목과 관련된 동작을 서로 그룹화할 수 있습니다.

Jetnews 앱에는 사용자가 읽을 수 있는 기사 목록이 표시됩니다. 각 목록 항목에는 사용자가 이 주제를 간략히 보고자 함을 나타내기 위한 작업이 포함되어 있습니다. 이 섹션에서는 목록을 더 쉽게 탐색할 수 있도록 이 작업을 맞춤 접근성 작업으로 이동합니다.

왼쪽에서는 각 X표 아이콘에 포커스를 둘 수 있는 기본 상황을 보여줍니다. 오른쪽에서는 관련 작업이 TalkBack의 맞춤 작업에 포함된 솔루션을 보여줍니다.

TalkBack이 사용 설정된 두 개의 화면 녹화. 왼쪽 화면은 게시물 항목에서 X표 아이콘을 선택하는 방법을 보여줍니다. 두 번 탭하면 대화상자가 열립니다. 오른쪽 화면은 세 번 탭 동작을 사용하여 맞춤 작업 메뉴를 여는 방법을 보여줍니다. '이 항목 간략히 보기' 작업을 탭하면 동일한 대화상자가 열립니다.

게시물 항목에 맞춤 작업이 추가됩니다. 변경 전(왼쪽)과 변경 후(오른쪽) 비교.

PostCards.kt를 열고 PostCardHistory 컴포저블의 구현을 살펴보겠습니다. Modifier.clickableonClick을 사용하는 RowIconButton의 클릭 가능한 속성에 주목합니다.

@Composable
fun PostCardHistory(post: Post, navigateToArticle: (String) -> Unit) {
   // ...
   Row(
       Modifier.clickable(
           onClickLabel = stringResource(R.string.action_read_article)
       ) {
           navigateToArticle(post.id)
       }
   ) {
       // ...
       CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
           IconButton(onClick = { openDialog = true }) {
               Icon(
                   imageVector = Icons.Default.Close,
                   contentDescription = stringResource(R.string.cd_show_fewer)
               )
           }
       }
   }
   // ...
}

기본적으로 RowIconButton 컴포저블은 모두 클릭 가능하고, 결과적으로 TalkBack을 통해 포커스를 받습니다. 이러한 상황은 목록의 각 항목에 발생합니다. 즉, 목록을 탐색하는 동안 많은 스와이프가 발생한다는 의미입니다. 차라리 IconButton과 관련된 작업을 목록 항목의 맞춤 작업으로 포함하고자 합니다. clearAndSetSemantics 수정자를 사용하여, 접근성 서비스에 이 Icon과 상호작용하지 않도록 지시할 수 있습니다.

@Composable
fun PostCardHistory(post: Post, navigateToArticle: (String) -> Unit) {
   // ...
   Row(
       Modifier.clickable(
           onClickLabel = stringResource(R.string.action_read_article)
       ) {
           navigateToArticle(post.id)
       }
   ) {
       // ...
       CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
            IconButton(
                modifier = Modifier.clearAndSetSemantics { },
                onClick = { openDialog = true }
            ) {
                Icon(
                    imageVector = Icons.Default.Close,
                    contentDescription = stringResource(R.string.cd_show_fewer)
                )
            }
       }
   }
   // ...
}

그러나 IconButton의 시맨틱을 삭제하면 더 이상 그 작업을 실행할 방법이 없습니다. 목록 항목에 그 작업을 추가하려면 대신 semantics 수정자에 맞춤 작업을 추가하면 됩니다.

@Composable
fun PostCardHistory(post: Post, navigateToArticle: (String) -> Unit) {
   // ...
   val showFewerLabel = stringResource(R.string.cd_show_fewer)
   Row(
        Modifier
            .clickable(
                onClickLabel = stringResource(R.string.action_read_article)
            ) {
                navigateToArticle(post.id)
            }
            .semantics {
                customActions = listOf(
                    CustomAccessibilityAction(
                        label = showFewerLabel,
                        // action returns boolean to indicate success
                        action = { openDialog = true; true }
                    )
                )
            }
   ) {
       // ...
       CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
            IconButton(
                modifier = Modifier.clearAndSetSemantics { },
                onClick = { openDialog = true }
            ) {
                Icon(
                    imageVector = Icons.Default.Close,
                    contentDescription = showFewerLabel
                )
            }
       }
   }
   // ...
}

이제 TalkBack의 맞춤 작업 팝업을 사용하여 작업을 적용할 수 있습니다. 이 작업은 목록 항목 내 작업 수가 증가할수록 관련성이 더 높아집니다.

6. 시각적 요소 설명

앱의 모든 사용자가 아이콘, 삽화 같이 앱에 표시되는 시각적 요소를 보거나 해석할 수 있는 것은 아닙니다. 접근성 서비스가 관련 픽셀만으로 시각적 요소를 이해하는 방법도 없습니다. 따라서 개발자가 앱의 시각적 요소에 관한 자세한 정보를 접근성 서비스에 전달해야 합니다.

ImageIcon 같은 시각적 컴포저블에는 contentDescription 매개변수가 포함됩니다. 여기서 이 시각적 요소의 현지화된 설명 또는 null(요소가 완전히 장식용인 경우)을 전달할 수 있습니다.

앱의 기사 화면에 일부 콘텐츠 설명이 누락되어 있습니다. 앱을 실행하고 상단 기사를 선택하여 기사 화면으로 이동해 보겠습니다.

TalkBack이 사용 설정된 두 개의 화면 녹화. 기사 화면에서 뒤로 버튼을 탭함. 왼쪽은 'Button—double tap to activate'를 호출합니다. 오른쪽은 'Navigate up—double tap to activate'를 호출합니다.

시각적 콘텐츠 설명이 추가됨. 변경 전(왼쪽)과 변경 후(오른쪽) 비교.

아무 정보도 제공되지 않으면 왼쪽 상단의 탐색 아이콘에 'Button, double tap to activate'라고만 표시됩니다. 이 경우 버튼을 활성화하면 어떤 작업이 실행되는지 사용자에게 아무런 정보가 제공되지 않습니다. ArticleScreen.kt를 열어 보겠습니다.

@Composable
fun ArticleScreen(
   // ...
) {
   // ...
   Scaffold(
       topBar = {
           InsetAwareTopAppBar(
               title = {
                   // ...
               },
               navigationIcon = {
                   IconButton(onClick = onBack) {
                       Icon(
                           imageVector = Icons.Filled.ArrowBack,
                           contentDescription = null
                       )
                   }
               }
           )
       }
   ) {
       // ...
   }
}

Icon에 의미 있는 콘텐츠 설명을 추가합니다.

@Composable
fun ArticleScreen(
   // ...
) {
   // ...
   Scaffold(
       topBar = {
           InsetAwareTopAppBar(
               title = {
                   // ...
               },
               navigationIcon = {
                   IconButton(onClick = onBack) {
                       Icon(
                           imageVector = Icons.Filled.ArrowBack,
                           contentDescription = stringResource(
                               R.string.cd_navigate_up
                           )
                       )
                   }
               }
           )
       }
   ) {
       // ...
   }
}

이 기사의 또 다른 시각적 요소는 헤더 이미지입니다. 이 사례에서 이 이미지는 완전히 장식용이므로, 사용자에게 전달하는 데 필요한 항목을 표시하지 않습니다. 따라서 콘텐츠 설명은 null로 설정되고, 접근성 서비스를 사용할 때 이 요소는 건너뛰게 됩니다.

화면의 마지막 시각적 요소는 프로필 사진입니다. 이 사례에서는 일반 아바타를 사용하므로 여기에 콘텐츠 설명을 추가할 필요가 없습니다. 관련 저자의 실제 프로필 사진을 사용할 경우 Google에서는 저자에게 프로필 사진에 관한 적절한 콘텐츠 설명을 제공해 달라고 요청할 수 있습니다.

7. 제목

이 기사 화면과 같이 화면에 텍스트가 많으면 시각 장애가 있는 사용자가 원하는 섹션을 빠르게 찾기가 상당히 어렵습니다. 이 점을 돕기 위해 텍스트의 어느 부분이 제목인지 나타낼 수 있습니다. 그러면 사용자는 위 또는 아래로 스와이프하여 빠르게 다양한 제목을 탐색할 수 있습니다.

기본적으로 컴포저블은 제목으로 표시되지 않으므로 탐색이 불가능합니다. 기사 화면에서 제목 탐색을 통해 제목을 제공하고자 합니다.

TalkBack이 사용 설정된 두 개의 화면 녹화. 아래로 스와이프하는 방식으로 제목을 탐색함 왼쪽 화면에는 'No next heading'이라고 표시되어 있음 오른쪽 화면에서는 제목을 순서대로 돌아가며 각 제목을 읽음

제목이 추가됨. 변경 전(왼쪽)과 변경 후(오른쪽) 비교.

기사의 제목은 PostContent.kt에 정의되어 있습니다. 그 파일을 열고 Paragraph 컴포저블까지 스크롤해 보겠습니다.

@Composable
private fun Paragraph(paragraph: Paragraph) {
   // ...
   Box(modifier = Modifier.padding(bottom = trailingPadding)) {
       when (paragraph.type) {
           // ...
           ParagraphType.Header -> {
               Text(
                   modifier = Modifier.padding(4.dp),
                   text = annotatedString,
                   style = textStyle.merge(paragraphStyle)
               )
           }
           // ...
       }
   }
}

여기서 Header는 간단한 Text 컴포저블로 정의되어 있습니다. 이 컴포저블이 제목임을 나타내기 위해 heading 시맨틱 속성을 설정할 수 있습니다.

@Composable
private fun Paragraph(paragraph: Paragraph) {
   // ...
   Box(modifier = Modifier.padding(bottom = trailingPadding)) {
       when (paragraph.type) {
           // ...
           ParagraphType.Header -> {
               Text(
                   modifier = Modifier.padding(4.dp)
                     .semantics { heading() },
                   text = annotatedString,
                   style = textStyle.merge(paragraphStyle)
               )
           }
           // ...
       }
   }
}

8. 맞춤 병합

이전 단계에서 살펴본 바와 같이, TalkBack과 같은 접근성 서비스는 요소에 따라 화면 요소를 탐색합니다. 기본적으로 Jetpack Compose에서는 하나 이상의 시맨틱 속성을 설정하는 각 하위 수준 컴포저블이 포커스를 받습니다. 따라서 Text 컴포저블 등이 text 시맨틱 속성을 설정하기 때문에 포커스를 받습니다.

하지만 화면에 포커스 가능한 요소가 너무 많으면 사용자가 하나씩 탐색할 때 혼란스러울 수 있습니다. 대신 컴포저블을 semantics 수정자와 mergeDescendants 속성을 사용하여 서로 병합할 수 있습니다.

이제 기사 화면을 확인해 보겠습니다. 대부분의 요소가 올바른 수준의 포커스를 받습니다. 그러나 현재 기사의 메타데이터가 몇몇 개별 항목처럼 소리 내어 읽힙니다. 이 점은 포커스 가능한 하나의 항목에 메타데이터를 병합하여 개선할 수 있습니다.

TalkBack이 사용 설정된 두 개의 화면 녹화. 왼쪽 화면에는 Author와 Metadata 필드의 녹색 TalkBack 직사각형이 개별적으로 표시됨. 오른쪽 화면에는 두 필드를 감싸는 직사각형 하나가 표시되고, 연결된 형태로 콘텐츠가 읽힘.

컴포저블이 병합됨. 변경 전(왼쪽)과 변경 후(오른쪽) 비교.

PostContent.kt를 열고 PostMetadata 컴포저블을 확인합니다.

@Composable
private fun PostMetadata(metadata: Metadata) {
   // ...
   Row {
       Image(
           // ...
       )
       Spacer(Modifier.width(8.dp))
       Column {
           Text(
               // ...
           )

           CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
               Text(
                   // ..
               )
           }
       }
   }
}

최상위 행에 하위 요소를 병합하도록 지시할 수 있습니다. 그러면 원하는 동작이 발생합니다.

@Composable
private fun PostMetadata(metadata: Metadata) {
   // ...
   Row(Modifier.semantics(mergeDescendants = true) {}) {
       Image(
           // ...
       )
       Spacer(Modifier.width(8.dp))
       Column {
           Text(
               // ...
           )

           CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
               Text(
                   // ..
               )
           }
       }
   }
}

9. 스위치 및 체크박스

TalkBack에 의해 선택되는 경우 SwitchCheckbox 같은 전환 가능 요소는 선택 상태를 읽습니다. 하지만 컨텍스트가 없으면 이러한 전환 가능한 요소가 무엇을 나타내는지 이해하기가 어려울 수 있습니다. 전환 가능한 상태를 위로 상향해 전환 가능한 요소의 컨텍스트를 포함할 수 있습니다. 그러면 사용자는 컴포저블 자체 또는 이를 설명하는 라벨을 눌러 Switch 또는 Checkbox를 전환할 수 있습니다.

이와 관련된 예는 Interests 화면에서 볼 수 있습니다. 이 화면에는 Home 화면에서 탐색 창을 열어 이동할 수 있습니다. Interests 화면에는 사용자가 구독할 수 있는 주제 목록이 있습니다. 기본적으로 이 화면의 체크박스는 라벨과는 별도로 포커스를 받기 때문에, 컨텍스트를 이해하기가 어렵습니다. 전체 Row를 전환 가능한 상태로 만들고자 합니다.

TalkBack이 사용 설정된 두 개의 화면 녹화. 선택 가능한 주제 목록이 있는 Interests 화면이 표시됨. 왼쪽 화면에서는 TalkBack에서 각 체크박스를 별도로 선택함. 오른쪽 화면에서는 TalkBack에서 전체 행을 선택함.

체크박스가 사용됨. 변경 전(왼쪽)과 변경 후(오른쪽) 비교.

InterestsScreen.kt를 열고 TopicItem 컴포저블의 구현을 살펴보겠습니다.

@Composable
private fun TopicItem(itemTitle: String, selected: Boolean, onToggle: () -> Unit) {
   // ...
   Row(
       modifier = Modifier
           .padding(horizontal = 16.dp, vertical = 8.dp)
   ) {
       // ...
       Checkbox(
           checked = selected,
           onCheckedChange = { onToggle() },
           modifier = Modifier.align(Alignment.CenterVertically)
       )
   }
}

여기서 볼 수 있듯이, Checkbox에는 요소 전환을 처리하는 onCheckedChange 콜백이 있습니다. 이 콜백을 전체 Row의 수준으로 상향할 수 있습니다.

@Composable
private fun TopicItem(itemTitle: String, selected: Boolean, onToggle: () -> Unit) {
   // ...
   Row(
       modifier = Modifier
           .toggleable(
               value = selected,
               onValueChange = { _ -> onToggle() },
               role = Role.Checkbox
           )
           .padding(horizontal = 16.dp, vertical = 8.dp)
   ) {
       // ...
       Checkbox(
           checked = selected,
           onCheckedChange = null,
           modifier = Modifier.align(Alignment.CenterVertically)
       )
   }
}

10. 상태 설명

이전 단계에서 전환 동작을 Checkbox에서 상위 Row로 상향했습니다. 컴포저블 상태에 관한 맞춤 설명을 추가하여 이 요소의 접근성을 더욱 개선할 수 있습니다.

기본적으로 Checkbox 상태는 'Ticked' 또는 'Not ticked'로 표시됩니다. 이 설명을 자체 맞춤 설명으로 바꿀 수 있습니다.

TalkBack이 사용 설정된 두 개의 화면 녹화. Interests 화면에서 한 가지 주제가 탭됨 왼쪽 화면에는 'not ticked', 오른쪽 화면에는 'not subscribed'라고 표시됨

상태 설명이 추가됨. 변경 전(왼쪽)과 변경 후(오른쪽) 비교.

이전 단계에서 조정한 TopicItem 컴포저블을 계속 사용할 수 있습니다.

@Composable
private fun TopicItem(itemTitle: String, selected: Boolean, onToggle: () -> Unit) {
   // ...
   Row(
       modifier = Modifier
           .toggleable(
               value = selected,
               onValueChange = { _ -> onToggle() },
               role = Role.Checkbox
           )
           .padding(horizontal = 16.dp, vertical = 8.dp)
   ) {
       // ...
       Checkbox(
           checked = selected,
           onCheckedChange = null,
           modifier = Modifier.align(Alignment.CenterVertically)
       )
   }
}

semantics 수정자 내에 stateDescription 속성을 사용하여 맞춤 상태 설명을 추가할 수 있습니다.

@Composable
private fun TopicItem(itemTitle: String, selected: Boolean, onToggle: () -> Unit) {
   // ...
   val stateNotSubscribed = stringResource(R.string.state_not_subscribed)
   val stateSubscribed = stringResource(R.string.state_subscribed)
   Row(
       modifier = Modifier
           .semantics {
               stateDescription = if (selected) {
                   stateSubscribed
               } else {
                   stateNotSubscribed
               }
           }
           .toggleable(
               value = selected,
               onValueChange = { _ -> onToggle() },
               role = Role.Checkbox
           )
           .padding(horizontal = 16.dp, vertical = 8.dp)
   ) {
       // ...
       Checkbox(
           checked = selected,
           onCheckedChange = null,
           modifier = Modifier.align(Alignment.CenterVertically)
       )
   }
}

11. 축하합니다.

축하합니다. 이 Codelab을 완료하여 Compose의 접근성에 관해 자세히 배웠습니다. 터치 영역, 시각적 요소 설명, 상태 설명에 관해서도 배웠습니다. 그리고 클릭 라벨과 제목, 맞춤 작업을 추가했습니다. 이제 맞춤 병합의 추가 방법과 스위치와 체크박스의 사용 방법을 알 것입니다. 이러한 학습 내용을 앱에 적용하면 앱의 접근성이 크게 향상됩니다.

Compose 과정에 관한 다른 Codelab과 Jetnews를 포함한 다른 코드 샘플을 확인하세요.

문서

이러한 주제에 관한 자세한 내용 및 안내는 다음 문서를 참고하세요.