동기
디자인 파트와의 협업을 진행하다 보면, 툴팁 표시 등의 기능을 위한 UI 가 다소 작게 구성되어 있는 경우가 왕왕 있습니다. 이러한 경우, Jetpack Compose 는 자동으로 터치 영역을 확장하여 UX 를 개선하도록 구현되어 있습니다.
어떠한 방식으로 이러한 개선이 이루어지는지, 별도로 활용할 수 있는지, 이를 해제하고 싶다면 어떻게 하면 좋을지 등을 학습하고, 정리하고 싶어 포스트를 작성합니다.
개요
머티리얼 디자인 시스템의 규칙을 따라, 위젯은 최소 48dp 의 터치 영역을 확보하는 편이 좋은데요.
Android View System 에는 이에 대한 자체 처리가 안 되어 있어, 마켓에 앱을 올리면 이에 대한 경고를 주기도 합니다.
Jetpack Compose 에는 이를 간편히 만족시킬 수 있도록 구현된 메서드가 있습니다.
@OptIn(ExperimentalMaterial3Api::class)
fun Modifier.minimumInteractiveComponentSize(): Modifier = composed(
inspectorInfo = debugInspectorInfo {
name = "minimumInteractiveComponentSize"
// TODO: b/214589635 - surface this information through the layout inspector in a better way
// - for now just add some information to help developers debug what this size represents.
properties["README"] = "Reserves at least 48.dp in size to disambiguate touch " +
"interactions if the element would measure smaller"
}
) {
if (LocalMinimumInteractiveComponentEnforcement.current) {
MinimumInteractiveComponentSizeModifier(minimumInteractiveComponentSize)
} else {
Modifier
}
}
작은 아이콘을 사용한 디자인이나 작은 사이즈의 텍스트를 사용하는 UI 가 많기에 꽤 효과를 볼 수 있습니다. 이와 같이 별도로 메서드가 존재하지만, 사실 호출할 일은 잘 없습니다. 이유는 간단한데, 보통 클릭 이벤트를 구현할 때에 사용되는 Modifier.clickable {} 메서드와 Modifier.pointerInput() 메서드 내부에 이에 대한 구현이 이미 적용되어 있기 때문입니다.
Modifier.clickable {} 의 경우, SemanticsNode 에 주석으로 작성되어 있습니다.
/**
* The rectangle of the touchable area.
*
* If this is a clickable region, this is the rectangle that accepts touch input. This can
* be larger than [size] when the layout is less than
* [ViewConfiguration.minimumTouchTargetSize]
*/
val touchBoundsInRoot: Rect
get() {
val entity = if (unmergedConfig.isMergingSemanticsOfDescendants) {
(layoutNode.outerMergingSemantics ?: outerSemanticsNode)
} else {
outerSemanticsNode
}
return entity.node.touchBoundsInRoot(unmergedConfig.useMinimumTouchTarget)
}
Modifier 에 적용된 크기가 ViewConfiguration 의 minimumTouchTargetSize 보다 작은 경우 재설정 될 수 있다고 이해할 수 있겠습니다. 이러한 프로세스는 clickable {} 메서드를 통해 기존 Modifier 의 노드와 결합되는 ClickableSemanticsNode 에 의해 진행됩니다.
pointerInput() 메서드에도 관련된 코드가 작성되어 있습니다.
override val extendedTouchPadding: Size
get() {
val minimumTouchTargetSize = viewConfiguration.minimumTouchTargetSize.toSize()
val size = size
val horizontal = max(0f, minimumTouchTargetSize.width - size.width) / 2f
val vertical = max(0f, minimumTouchTargetSize.height - size.height) / 2f
return Size(horizontal, vertical)
}
clickable {} 메서드와 동일하게, viewConfiguration.minimumTouchTargetSize 를 그대로 활용하고 있습니다.
즉, clickable {} 메서드와 pointerInput() 메서드를 사용하는 경우, 위 서술한 메서드를 별도로 호출하지 않아도 자동으로 적용됩니다.
콜 사이트에 따라 다르게 적용된다
이미 적용되어 있음에도 불구하고 직접 호출할 상황이 있을 수는 있겠는데요. 바로, 자체 패딩을 통한 영역 확보가 필요한 경우입니다.
메서드 콜 사이트에 따라 UI 에 반영되는 것에 약간 씩의 차이가 있습니다. 확인 하시고, 필요하다면 콜 사이트를 적절히 선택해서 사용하시면 좋겠습니다.
제거하기
이를 적용하고 싶지 않은 상황들이 있을 수도 있는데요. 오밀조밀 구현된 UI 의 경우, 이러한 구현이 오히려 방해가 될 수 있습니다. 제 경우, 회사에서 진행하고 있는 영상 플레이어 앱의 Seekbar Thumb 의 사이즈가 의도적으로 작게 구현되었는데, 이러한 설정이 적용되어 Thumb 과 매우 인접한 Seekbar 영역을 터치할 때에 불편함이 있었습니다.
학습한 바, ViewConfiguration 의 minimumTouchTargetSize 값을 조정하면 될 것이라 생각했고, 이는 CompositionLocalProvider 를 통해 설정할 수 있었습니다. CompositionLocalProvider 에 대한 정보는 다음 포스트에서 확인하실 수 있습니다.
[Jetpack Compose] CompositionLocal 은 어떻게 동작하는가?
동기 Jetpack Compose 를 꽤 오랜 시간 사용해 왔지만, 여전히 모르는 부분이 많습니다. 그중 가장 가려웠던 부분은 CompositionLocal 인데, 언제 어떻게 사용해야 하는지에 대해서는 알고 있으나, 어떠한
blothhundr.tistory.com
LocalViewConfiguration 은 현재 적용된 ViewConfiguration 을 제공하고 있는데요. 이는 데이터 클래스가 아니기 때문에, copy() 와 같은 메서드를 사용할 수 없습니다. 그래서 내부 값을 수정하기 위한 클래스를 따로 정의하고, 이를 LocalCompositionProvider 에 전달하여 해결하는 방식을 사용해야 합니다.
class CustomViewConfiguration(
private val defaultViewConfiguration: ViewConfiguration,
) : ViewConfiguration {
override val doubleTapMinTimeMillis: Long
get() = defaultViewConfiguration.doubleTapMinTimeMillis
override val doubleTapTimeoutMillis: Long
get() = defaultViewConfiguration.doubleTapTimeoutMillis
override val longPressTimeoutMillis: Long
get() = defaultViewConfiguration.longPressTimeoutMillis
override val touchSlop: Float
get() = defaultViewConfiguration.touchSlop
override val minimumTouchTargetSize: DpSize
get() = DpSize(0.dp, 0.dp)
}
minimumTouchTargetSize 말고도 다른 값들 역시 조정할 수 있으니, 필요 시 조정하시면 되겠습니다. 굉장히 매력적인 설정들이네요.
기본 값을 전달해주어야 하기 때문에, 프로퍼티로 기본 ViewConfigration 을 전달할 수 있도록 구현하면 됩니다. 이후에는 내부 코드 블럭에 원하는 UI 코드를 작성하면 됩니다.
val defaultViewConfiguration = LocalViewConfiguration.current
val customViewConfiguration = remember { CustomViewConfiguration(defaultViewConfiguration) }
CompositionLocalProvider(LocalViewConfiguration.provides(customViewConfiguration)) {
Text(
modifier = Modifier.clickable { },
text = "안녕하세요"
)
}
코드를 보면 아시겠지만, 반대로 늘릴 수도 있습니다.
Jetpack Compose 는 이제 메인 스트림으로 자리 잡았고, 수많은 개선을 거쳐 현재에 이르렀습니다. 다양한 지원이 존재하고, 또 이에 대해서 꽤 세밀한 부분까지 조정할 수 있구나 싶어 새삼 놀랍네요.
'Android > Trouble Shoot' 카테고리의 다른 글
Third-Party-Library 없이 영상으로부터 빠르게 프레임 추출하기 (feat.YUV) (0) | 2023.12.08 |
---|---|
AccessToken 재발급과 예외 처리 (feat.Ktor) (0) | 2023.12.02 |
[Jetpack Compose] List 아이템의 필드 변경이 Recomposition 을 트리거 해야 할 때 (0) | 2023.05.16 |
[Jetpack Compose] LazyColumn, LazyRow 의 항목 삭제가 정상적으로 이루어지지 않는 이슈 (0) | 2023.04.25 |
[Jetpack Compose] Column 속 LazyColumn 의 OverScroll 을 막는 방법 (0) | 2023.02.23 |