Compose로 작성된 앱은 요구사항이 서로 다른 여러 사용자의 접근성을 지원해야 합니다. 접근성 서비스는 화면에 표시된 내용을 특정한 요구사항이 있는 사용자에게 보다 적합한 형식으로 변환하는 데 사용됩니다. 접근성 서비스를 지원하기 위해 앱은 Android 프레임워크의 API를 사용하여 UI 요소에 관한 시맨틱 정보를 노출합니다. 그런 다음 Android 프레임워크에서 이 시맨틱 정보를 접근성 서비스에 전달합니다. 각 접근성 서비스에서 사용자에게 앱을 가장 잘 설명하는 방법을 선택할 수 있습니다. Android는 TalkBack, 스위치 제어 등 여러 접근성 서비스를 제공합니다.
시맨틱
Compose는 시맨틱 속성을 사용하여 접근성 서비스에 정보를 전달합니다. 시맨틱 속성은 사용자에게 표시되는 UI 요소에 관한 정보를 제공합니다. Text
및 Button
과 같은 대부분의 내장 컴포저블은 이 시맨틱 속성을 컴포저블 및 컴포저블의 하위 요소로부터 추론된 정보로 채웁니다. toggleable
및 clickable
과 같은 일부 수정자는 특정 시맨틱 속성도 설정합니다. 하지만 프레임워크에서 사용자에게 UI 요소를 설명하는 방법을 이해하기 위해 더 많은 정보가 필요한 경우도 있습니다.
이 문서에서는 Android 프레임워크에 올바로 설명할 수 있도록 컴포저블에 추가 정보를 명시적으로 추가해야 하는 여러 가지 상황을 설명합니다. 또한 특정 컴포저블의 시맨틱 정보를 완전히 바꾸는 방법을 설명합니다. 여기서는 Android의 접근성을 기본적으로 이해하고 있다고 가정합니다.
일반적인 사용 사례
접근성 기능이 필요한 사용자가 앱을 성공적으로 사용할 수 있도록 지원하려면 앱이 이 페이지에 설명된 권장사항을 따라야 합니다.
터치 영역 최소 크기 고려
사용자가 클릭, 터치 등의 방법으로 상호작용할 수 있는 화면상의 요소는 안정적으로 상호작용할 수 있도록 충분히 커야 합니다. 이러한 요소의 크기를 조절할 때 Material Design 접근성 가이드라인을 정확히 준수하도록 최소 크기를 48dp로 설정해야 합니다.
Checkbox
,
RadioButton
,
Switch
,
Slider
,
Surface
와 같은 Material 구성요소는 이 최소 크기를 내부적으로 설정합니다. 단, 구성요소가 사용자 작업을 수신할 수 있는 경우에 한합니다. 예를 들어 Checkbox
의 onCheckedChange
매개변수가 null이 아닌 값으로 설정된 경우 너비와 높이가 최소 48dp인 패딩이 포함됩니다.
@Composable
fun CheckableCheckbox() {
Checkbox(checked = true, onCheckedChange = {})
}
onCheckedChange
매개변수가 null로 설정된 경우 구성요소와 직접 상호작용할 수 없으므로 패딩이 포함되지 않습니다.
@Composable
fun NonClickableCheckbox() {
Checkbox(checked = true, onCheckedChange = null)
}
Switch
, RadioButton
, Checkbox
와 같은 선택 설정을 구현할 경우 일반적으로 클릭 가능한 동작을 상위 컨테이너로 올리고, 컴포저블에 관한 클릭 콜백을 null
로 설정하고, toggleable
또는 selectable
수정자를 상위 컴포저블에 추가합니다.
@Composable
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
fun DefaultPreview() {
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
fun DefaultPreview() {
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
fun ArticleListItem(openArticle: () -> Unit) {
Row(
Modifier.clickable(
// R.string.action_read_article = "read article"
onClickLabel = stringResource(R.string.action_read_article),
onClick = openArticle
)
) {
// ..
}
}
또는 클릭 가능한 수정자에 액세스할 수 없는 경우 시맨틱 수정자에 클릭 라벨을 설정할 수 있습니다.
@Composable
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
fun ShareButton(onClick: () -> Unit) {
IconButton(onClick = onClick) {
Icon(
imageVector = Icons.Filled.Share,
contentDescription = stringResource(R.string.label_share)
)
}
}
일부 시각적 요소는 완전히 장식용이므로 사용자에게 전달하지 않아도 됩니다. contentDescription
매개변수를 null
로 설정하면 Android 프레임워크에 이 요소에 연결된 작업 또는 상태가 없음을 나타냅니다.
@Composable
fun PostImage(post: Post, modifier: Modifier = Modifier) {
val image = post.imageThumb ?: imageResource(R.drawable.placeholder_1_1)
Image(
bitmap = 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
의 경우에도 마찬가지입니다. 목록 항목 내 요소가 병합되며 접근성 서비스에서 하나의 요소로 간주합니다.
컴포저블의 집합이 논리적 그룹을 구성할 수 있지만 이 그룹은 클릭할 수 없거나 목록 항목의 일부가 아닙니다. 접근성 서비스에서 계속 컴포저블을 하나의 요소로 간주하도록 할 수 있습니다. 예를 들어 사용자의 아바타, 이름, 추가 정보를 보여주는 컴포저블이 있다고 가정해 보겠습니다.
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")
}
}
}
이제 접근성 서비스가 전체 컨테이너에 한 번에 포커스를 맞추고 콘텐츠를 병합합니다.
맞춤 작업 추가
다음 목록 항목을 살펴보세요.
TalkBack과 같은 스크린 리더를 사용하여 화면에 표시된 내용을 듣는 경우 스크린 리더는 먼저 전체 항목을 선택한 다음 북마크 아이콘을 선택합니다.
목록이 길면 이 작업이 매우 반복적일 수 있습니다. 더 나은 접근 방식은 사용자가 항목을 북마크할 수 있는 맞춤 작업을 정의하는 것입니다. 북마크 아이콘 자체의 동작을 명시적으로 삭제하여 접근성 서비스에서 북마크 아이콘을 선택하지 않도록 해야 합니다.
clearAndSetSemantics
수정자를 사용하면 됩니다.
@Composable
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.h5,
modifier = Modifier.semantics { heading() }
)
}
맞춤 하위 수준 컴포저블 만들기
고급 사용 사례에서는 앱의 특정 Material 구성요소를 맞춤 버전으로 바꿉니다. 이 시나리오에서는 접근성 고려사항을 염두에 두어야 합니다. Material Checkbox
를 자체 구현으로 바꾼다고 가정해 보겠습니다. 이 구성요소의 접근성 속성을 처리하는 triStateToggleable
수정자를 추가하는 것을 잊어버리기 쉽습니다.
일반적으로 Material 라이브러리에서 구성요소 구현을 확인하고, 찾을 수 있는 접근성 동작을 모방해야 합니다. 또한 UI 수준 수정자가 아니라 기초 수정자를 많이 활용하세요. 기초 수정자에는 접근성 고려 사항이 기본적으로 포함되어 있습니다. 여러 접근성 서비스로 맞춤 구성요소 구현을 테스트하여 동작을 확인해야 합니다.
자세히 알아보기
Compose 코드의 접근성 지원에 관해 자세히 알아보려면 Jetpack Compose의 접근성 Codelab을 참고하세요.