사용자 확장 가능 콘텐츠 지원

핀치 투 줌 동작을 구현하여 앱에서 확장 가능한 콘텐츠를 지원하세요. 이는 접근성을 개선하는 표준적이고 플랫폼 일관적인 방법으로, 사용자가 필요에 맞게 텍스트와 UI 요소의 크기를 직관적으로 조정할 수 있습니다. 앱은 세부적인 제어와 상황별 동작으로 맞춤 확장 동작을 정의하여 사용자가 화면 확대와 같은 시스템 수준 기능보다 더 빠르게 발견하는 환경을 제공할 수 있습니다.

확장 전략 선택

이 가이드에서 다루는 전략을 사용하면 화면 너비에 맞게 UI가 리플로우되고 재구성됩니다. 이렇게 하면 가로로 이동할 필요가 없고 긴 텍스트 줄을 읽을 때 필요했던 답답한 '지그재그' 동작이 필요하지 않아 접근성이 크게 향상됩니다.

추가 자료: 연구에 따르면 저시력 사용자의 경우 콘텐츠의 리플로우가 2차원 패닝이 필요한 인터페이스보다 훨씬 더 읽기 쉽고 탐색하기 쉬운 것으로 확인되었습니다. 자세한 내용은 휴대기기에서 팬앤스캔과 리플로우 콘텐츠 비교를 참고하세요.

모든 요소 또는 텍스트 요소만 크기 조정

다음 표에서는 각 확장 전략의 시각적 효과를 보여줍니다.

전략 밀도 조정 글꼴 크기 조정

동작

모든 항목을 비례적으로 조정합니다. 콘텐츠가 컨테이너에 맞게 리플로우되므로 사용자가 모든 콘텐츠를 보기 위해 가로로 이동할 필요가 없습니다.

텍스트 요소에만 영향을 미칩니다. 전체 레이아웃과 비텍스트 구성요소는 동일한 크기로 유지됩니다.

무슨 체중계인가요?

모든 시각적 요소: 텍스트, 구성요소 (버튼, 아이콘), 이미지, 레이아웃 간격 (패딩, 여백)

텍스트만 표시

데모

권장사항

이제 시각적 차이점을 확인했으므로 다음 표를 통해 장단점을 비교하고 콘텐츠에 가장 적합한 전략을 선택할 수 있습니다.

UI 유형

권장 전략

추론

읽기 중심 레이아웃

예: 뉴스 기사, 메시지 앱

밀도 또는 글꼴 크기 조정

밀도 조정은 인라인 이미지를 포함한 전체 콘텐츠 영역을 조정하는 데 적합합니다.

텍스트만 확장해야 하는 경우 글꼴 확장이 간단한 대안입니다.

시각적으로 구조화된 레이아웃

예: 앱 스토어, 소셜 미디어 피드

밀도 조정

캐러셀 또는 그리드에서 이미지와 텍스트 간의 시각적 관계를 유지합니다. 리플로우 특성으로 인해 중첩된 스크롤 요소와 충돌할 수 있는 가로 패닝이 방지됩니다.

Jetpack Compose에서 스케일링 동작 감지

사용자 확장 가능한 콘텐츠를 지원하려면 먼저 멀티터치 동작을 감지해야 합니다. Jetpack Compose에서는 Modifier.transformable를 사용하여 이를 실행할 수 있습니다.

transformable 수정자는 마지막 동작 이벤트 이후의 zoomChange 델타를 제공하는 상위 수준 API입니다. 이렇게 하면 상태 업데이트 로직이 직접 누적 (예: scale *= zoomChange)으로 간소화되어 이 가이드에서 다루는 적응형 확장 전략에 적합합니다.

구현 예시

다음 예에서는 밀도 조정 및 글꼴 조정 전략을 구현하는 방법을 보여줍니다.

밀도 조정

이 접근 방식은 UI 영역의 기본 density를 확장합니다. 따라서 패딩, 간격, 구성요소 크기 등 모든 레이아웃 기반 측정값이 화면 크기나 해상도가 변경된 것처럼 조정됩니다. 텍스트 크기는 밀도에도 의존하므로 비례적으로 조정됩니다. 이 전략은 특정 영역 내의 모든 요소를 균일하게 확대하여 UI의 전반적인 시각적 리듬과 비율을 유지하려는 경우에 효과적입니다.

private class DensityScalingState(
    // Note: For accessibility, typical min/max values are ~0.75x and ~3.5x.
    private val minScale: Float = 0.75f,
    private val maxScale: Float = 3.5f,
    private val currentDensity: Density
) {
    val transformableState = TransformableState { zoomChange, _, _ ->
        scaleFactor.floatValue =
            (scaleFactor.floatValue * zoomChange).coerceIn(minScale, maxScale)
    }
    val scaleFactor = mutableFloatStateOf(1f)
    fun scaledDensity(): Density {
        return Density(
            currentDensity.density * scaleFactor.floatValue,
            currentDensity.fontScale
        )
    }
}

글꼴 크기 조정

이 전략은 fontScale 요소만 수정하여 더 타겟팅됩니다. 결과적으로 텍스트 요소만 커지거나 작아지고 컨테이너, 패딩, 아이콘과 같은 다른 모든 레이아웃 구성요소는 고정된 크기로 유지됩니다. 이 전략은 읽기 중심 앱에서 텍스트 가독성을 개선하는 데 적합합니다.

class FontScaleState(
    // Note: For accessibility, typical min/max values are ~0.75x and ~3.5x.
    private val minScale: Float = 0.75f,
    private val maxScale: Float = 3.5f,
    private val currentDensity: Density
) {
    val transformableState = TransformableState { zoomChange, _, _ ->
        scaleFactor.floatValue =
            (scaleFactor.floatValue * zoomChange).coerceIn(minScale, maxScale)
    }
    val scaleFactor = mutableFloatStateOf(1f)
    fun scaledFont(): Density {
        return Density(
            currentDensity.density,
            currentDensity.fontScale * scaleFactor.floatValue
        )
    }
}

공유 데모 UI

이는 앞의 두 예에서 서로 다른 확장 동작을 강조하기 위해 사용되는 공유 DemoCard 컴포저블입니다.

@Composable
private fun DemoCard() {
    Card(
        modifier = Modifier
            .width(360.dp)
            .padding(16.dp),
        shape = RoundedCornerShape(12.dp)
    ) {
        Column(
            modifier = Modifier.padding(16.dp),
            verticalArrangement = Arrangement.spacedBy(16.dp)
        ) {
            Text("Demo Card", style = MaterialTheme.typography.headlineMedium)
            var isChecked by remember { mutableStateOf(true) }
            Row(verticalAlignment = Alignment.CenterVertically) {
                Text("Demo Switch", Modifier.weight(1f), style = MaterialTheme.typography.bodyLarge)
                Switch(checked = isChecked, onCheckedChange = { isChecked = it })
            }
            Row(verticalAlignment = Alignment.CenterVertically) {
                Icon(Icons.Filled.Person, "Icon", Modifier.size(32.dp))
                Spacer(Modifier.width(8.dp))
                Text("Demo Icon", style = MaterialTheme.typography.bodyLarge)
            }
            Row(
                Modifier.fillMaxWidth(),
                horizontalArrangement = Arrangement.SpaceBetween
            ) {
                Box(
                    Modifier
                        .width(100.dp)
                        .weight(1f)
                        .height(80.dp)
                        .background(Color.Blue)
                )
                Box(
                    Modifier
                        .width(100.dp)
                        .weight(1f)
                        .height(80.dp)
                        .background(Color.Red)
                )
            }
            Text(
                "Demo Text: Lorem ipsum dolor sit amet, consectetur adipiscing elit," +
                    " sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
                style = MaterialTheme.typography.bodyMedium,
                textAlign = TextAlign.Justify
            )
        }
    }
}

도움말 및 고려사항

더 세련되고 접근성 높은 환경을 만들려면 다음 권장사항을 고려하세요.

  • 동작이 아닌 스케일 컨트롤 제공 고려: 일부 사용자는 동작에 어려움을 느낄 수 있습니다. 이러한 사용자를 지원하려면 동작을 사용하지 않고 체중계를 조정하거나 재설정할 수 있는 대체 방법을 제공하는 것이 좋습니다.
  • 모든 크기에 맞게 빌드: 인앱 크기 조정과 시스템 전체 글꼴 또는 디스플레이 설정을 모두 사용하여 UI를 테스트합니다. 앱의 레이아웃이 콘텐츠를 깨거나, 겹치거나, 숨기지 않고 올바르게 적응하는지 확인합니다. 적응형 레이아웃을 빌드하는 방법을 자세히 알아보세요.