사용자 상호작용 처리

컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요.

사용자 인터페이스 구성요소는 사용자 상호작용에 반응하는 방식으로 기기 사용자에게 피드백을 제공합니다. 모든 구성요소에는 상호작용에 반응하는 고유한 방식이 있으므로 상호작용으로 무슨 일이 벌어지고 있는지 사용자가 파악할 수 있습니다. 예를 들어, 사용자가 기기의 터치스크린에 있는 버튼을 터치하면 강조표시 색상이 추가되는 등의 방식으로 버튼이 변경될 수 있습니다. 이 변경을 통해 사용자는 자신이 버튼을 터치한 것을 인식할 수 있습니다. 사용자가 버튼을 터치하려는 것이 아니었다면 손가락을 떼기 전에 손가락을 버튼 밖으로 드래그하면 됩니다. 그러지 않으면 버튼이 활성화됩니다.

Compose 동작 문서에서는 Compose 구성요소가 포인터 이동 및 클릭과 같은 하위 수준 포인터 이벤트를 처리하는 방법을 다룹니다. Compose는 즉시 이러한 하위 수준 이벤트를 상위 수준 상호작용으로 추상화합니다. 예를 들어, 일련의 포인터 이벤트가 버튼 누르기 및 해제에까지 추가될 수 있습니다. 더 높은 수준의 추상화를 이해하면 UI가 사용자에게 반응하는 방식을 맞춤설정하는 데 도움이 됩니다. 예를 들어, 사용자가 구성요소와 상호작용할 때 구성요소의 외양이 변경되는 방식을 맞춤설정할 수 있고 또는 이러한 사용자 작업의 로그를 유지관리할 수도 있습니다. 이 문서에서는 표준 UI 요소를 수정하거나 직접 디자인하는 데 필요한 정보를 제공합니다.

상호작용

대부분의 경우 Compose 구성요소가 사용자 상호작용을 해석하는 방법을 알 필요는 없습니다. 예를 들어, ButtonModifier.clickable을 통해 사용자가 버튼을 클릭했는지 파악합니다. 앱에 일반 버튼을 추가하는 경우 버튼의 onClick 코드를 정의할 수 있고 Modifier.clickable은 적절한 시기에 이 코드를 실행합니다. 즉, 사용자가 화면을 탭했는지 또는 키보드로 버튼을 선택했는지 알 필요가 없습니다. Modifier.clickable은 사용자가 클릭했는지 파악하고 onClick 코드를 실행하여 응답합니다.

그러나 사용자의 동작에 맞게 UI 구성요소의 응답을 맞춤설정하려면 내부에서 무슨 일이 일어나는지 더 자세히 알 필요가 있습니다. 이 섹션에서는 이와 관련된 정보를 일부 제공합니다.

사용자가 UI 구성요소와 상호작용하면 시스템은 다양한 Interaction 이벤트를 생성하여 동작을 표현합니다. 예를 들어 사용자가 버튼을 터치하면 버튼은 PressInteraction.Press를 생성합니다. 사용자가 버튼 내에서 손가락을 떼면 클릭이 완료되었음을 버튼이 알 수 있도록 PressInteraction.Release가 생성됩니다. 반면, 사용자가 버튼 밖으로 손가락을 드래그한 다음 손가락을 떼면 버튼은 PressInteraction.Cancel을 생성하여 버튼을 누른 것이 취소되었고 완료되지 않았음을 나타냅니다.

이러한 상호작용은 독단적이지 않습니다. 즉, 이러한 하위 수준 상호작용 이벤트는 사용자 작업 또는 작업 시퀀스의 의미를 해석하려고 하지 않습니다. 또한, 어떤 사용자 작업이 더 우선순위가 높은지 해석하지 않습니다.

일반적으로 이러한 상호작용은 시작과 끝이 있는 쌍으로 구성됩니다. 두 번째 상호작용에는 첫 번째 상호작용에 관한 참조가 포함됩니다. 예를 들어, 사용자가 버튼을 터치한 후 손가락을 떼면 터치 동작으로 PressInteraction.Press 상호작용이 생성되고 해제 동작으로 PressInteraction.Release 상호작용이 생성됩니다. Release는 앞선 PressInteraction.Press를 인식할 수 있도록 press 속성을 포함합니다.

InteractionSource를 관찰하여 특정 구성요소의 상호작용을 확인할 수 있습니다. InteractionSourceKotlin 흐름을 기반으로 빌드되므로 다른 흐름에서와 같은 방식으로 상호작용을 수집할 수 있습니다.

상호작용 상태

상호작용을 직접 추적하여 구성요소의 내장 기능을 확장할 수도 있습니다. 예를 들어 버튼을 눌렀을 때 버튼 색상이 변경되도록 할 수 있습니다. 상호작용을 추적하는 가장 간단한 방법은 적절한 상호작용 상태를 관찰하는 것입니다. InteractionSource는 다양한 상호작용의 상황을 상태로 나타내는 여러 메서드를 제공합니다. 예를 들어, 특정 버튼이 눌렸는지 확인하려면 버튼의 InteractionSource.collectIsPressedAsState() 메서드를 호출하면 됩니다.

val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()

Button(
    onClick = { /* do something */ },
    interactionSource = interactionSource) {
    Text(if (isPressed) "Pressed!" else "Not pressed")
}

Compose는 collectIsPressedAsState() 외에 collectIsFocusedAsState(), collectIsDraggedAsState(), collectIsHoveredAsState()를 제공합니다. 이러한 메서드는 실제로 하위 수준의 InteractionSource API를 기반으로 빌드된 편의 메서드입니다. 어떤 경우에는 이러한 하위 수준 함수를 직접 사용하는 것이 좋습니다.

예를 들어, 버튼을 누르고 있는지 그리고 드래그하고 있는지 알아야 하는 상황을 가정해 보겠습니다. collectIsPressedAsState()collectIsDraggedAsState()를 모두 사용하는 경우 Compose가 많은 중복 작업을 실행하고 모든 상호작용을 올바른 순서로 가져온다는 보장이 없습니다. 이 같은 경우에는 InteractionSource를 직접 사용하는 것이 좋습니다. 다음 섹션에서는 필요한 정보만 가져와서 직접 상호작용을 추적하는 방법을 설명합니다.

InteractionSource 작업

구성요소와의 상호작용에 관한 하위 수준의 정보가 필요한 경우 구성요소의 InteractionSource에 맞게 표준 흐름 API를 사용하면 됩니다. 예를 들어, InteractionSource의 누르기 및 드래그 상호작용 목록을 유지하려고 한다고 가정해 보겠습니다. 이 코드는 버튼을 누를 때 새 누르기 작업을 목록에 추가하여 작업의 절반을 실행합니다.

val interactionSource = remember { MutableInteractionSource() }
val interactions = remember { mutableStateListOf<Interaction>() }

LaunchedEffect(interactionSource) {
    interactionSource.interactions.collect { interaction ->
        when (interaction) {
            is PressInteraction.Press -> {
                interactions.add(interaction)
            }
            is DragInteraction.Start -> {
                interactions.add(interaction)
            }
        }
    }
}

하지만 새 상호작용을 추가하는 것 외에 종료된 상호작용도 삭제해야 합니다 (예: 사용자가 구성요소에서 손가락을 떼는 경우). 종료 상호작용에는 항상 연결된 시작 상호작용의 참조가 포함되어 있으므로 상호작용을 삭제하기는 쉽습니다. 이 코드는 종료된 상호작용을 삭제하는 방법을 보여줍니다.

val interactions = remember { mutableStateListOf<Interaction>() }

LaunchedEffect(interactionSource) {
    interactionSource.interactions.collect { interaction ->
        when (interaction) {
            is PressInteraction.Press -> {
                interactions.add(interaction)
            }
            is PressInteraction.Release -> {
                interactions.remove(interaction.press)
            }
            is PressInteraction.Cancel -> {
                interactions.remove(interaction.press)
            }
            is DragInteraction.Start -> {
                interactions.add(interaction)
            }
            is DragInteraction.Stop -> {
                interactions.remove(interaction.start)
            }
            is DragInteraction.Cancel -> {
                interactions.remove(interaction.start)
            }
        }
    }
}

이제 구성요소를 누르고 있는지 또는 드래그하고 있는지 알려면 interactions가 비어 있는지만 확인하면 됩니다.

val isPressedOrDragged = interactions.isNotEmpty()

가장 최근의 상호작용을 알고 싶다면 목록의 마지막 항목을 보면 됩니다. 예를 들어, 아래 코드는 Compose 물결 효과 구현이 최근 상호작용에 사용할 적절한 상태 오버레이를 파악하는 방법을 보여줍니다.

val lastInteraction = when (interactions.lastOrNull()) {
    is DragInteraction.Start -> "Dragged"
    is PressInteraction.Press -> "Pressed"
    else -> "No state"
}

예를 통해 작업하기

다음은 수정된 버튼의 예로, 입력에 맞춤 응답을 사용하는 구성요소의 빌드 방법을 보여줍니다. 이 경우 누르기에 관한 응답으로 모양을 변경하는 버튼을 만든다고 가정해 보겠습니다.

클릭 시 아이콘을 동적으로 추가하는 버튼의 애니메이션

이렇게 하려면 Button을 기반으로 맞춤 컴포저블을 빌드하고 추가 icon 매개변수를 사용하여 아이콘을 그립니다(이 경우에는 장바구니임). collectIsPressedAsState()를 호출하여 사용자가 버튼 위에 마우스 오버 중인지 추적합니다. 마우스가 버튼 위에 있다면 아이콘을 추가합니다. 코드는 다음과 같습니다.

@Composable
fun PressIconButton(
    onClick: () -> Unit,
    icon: @Composable () -> Unit,
    text: @Composable () -> Unit,
    modifier: Modifier = Modifier,
    interactionSource: MutableInteractionSource =
        remember { MutableInteractionSource() },
) {
    val isPressed by interactionSource.collectIsPressedAsState()
    Button(onClick = onClick, modifier = modifier,
        interactionSource = interactionSource) {
        AnimatedVisibility(visible = isPressed) {
            if (isPressed) {
                Row {
                    icon()
                    Spacer(Modifier.size(ButtonDefaults.IconSpacing))
                }
            }
        }
        text()
    }
}

새로운 컴포저블을 사용하는 모습은 다음과 같습니다.

PressIconButton(
    onClick = {},
    icon = { Icon(Icons.Filled.ShoppingCart, contentDescription = null) },
    text = { Text("Add to cart") }
)

이 새로운 PressIconButton은 기존 머티리얼 Button을 기반으로 빌드되었으므로 모든 일반적인 방식으로 사용자 상호작용에 반응합니다. 사용자가 버튼을 누르면 일반 머티리얼 Button과 마찬가지로 불투명도가 약간 달라집니다. 또한, 새 코드 덕분에 HoverIconButton은 아이콘을 추가하여 마우스 오버에 동적으로 응답합니다.