탭한 후 누르기

많은 컴포저블에는 탭 또는 클릭에 대한 기본 지원이 있으며 onClick 람다를 포함합니다. 예를 들어 노출 영역과의 상호작용에 적합한 모든 Material Design 동작을 포함하는 클릭 가능한 Surface를 만들 수 있습니다.

Surface(onClick = { /* handle click */ }) {
    Text("Click me!", Modifier.padding(24.dp))
}

하지만 클릭만으로 컴포저블과 상호작용할 수 있는 것은 아닙니다. 이 페이지에서는 단일 포인터가 포함된 동작에 중점을 둡니다. 이때 포인터의 위치는 해당 이벤트 처리에 중요하지 않습니다. 다음 표에는 이러한 유형의 동작이 나와 있습니다.

동작

설명

탭 (또는 클릭)

포인터가 아래로 이동한 다음 위로 이동함

두 번 탭

포인터가 아래, 위, 아래, 위로 이동합니다.

길게 누르기

포인터가 아래로 내려가 더 오래 유지됨

보도자료

포인터가 아래로 이동함

탭 또는 클릭에 응답

clickable는 컴포저블이 탭 또는 클릭에 반응하도록 하는 일반적으로 사용되는 수정자입니다. 이 수정자는 포커스 지원, 마우스 및 스타일러스 마우스 오버, 누르면 맞춤설정 가능한 시각적 표시와 같은 추가 기능도 제공합니다. 이 수정자는 마우스나 손가락뿐만 아니라 키보드 입력을 통한 클릭 이벤트 또는 접근성 서비스를 사용할 때의 클릭 이벤트 등 넓은 의미의 '클릭'에 반응합니다.

사용자가 이미지를 클릭하면 이미지가 전체 화면으로 표시되는 이미지 그리드를 생각해 보세요.

그리드의 각 항목에 clickable 수정자를 추가하여 이 동작을 구현할 수 있습니다.

@Composable
private fun ImageGrid(photos: List<Photo>) {
    var activePhotoId by rememberSaveable { mutableStateOf<Int?>(null) }
    LazyVerticalGrid(columns = GridCells.Adaptive(minSize = 128.dp)) {
        items(photos, { it.id }) { photo ->
            ImageItem(
                photo,
                Modifier.clickable { activePhotoId = photo.id }
            )
        }
    }
    if (activePhotoId != null) {
        FullScreenImage(
            photo = photos.first { it.id == activePhotoId },
            onDismiss = { activePhotoId = null }
        )
    }
}

clickable 수정자는 다음과 같은 추가 동작도 추가합니다.

  • interactionSourceindication: 사용자가 컴포저블을 탭할 때 기본적으로 리플을 그립니다. 사용자 상호작용 처리 페이지에서 이를 맞춤설정하는 방법을 알아보세요.
  • 접근성 서비스가 시맨틱 정보를 설정하여 요소와 상호작용할 수 있도록 합니다.
  • 포커스를 허용하고 Enter 또는 D-패드 중앙을 눌러 상호작용할 수 있도록 하여 키보드 또는 조이스틱 상호작용을 지원합니다.
  • 마우스나 스타일러스가 요소 위로 마우스 오버할 때 요소가 반응하도록 요소를 마우스 오버할 수 있도록 만듭니다.

길게 눌러 컨텍스트 컨텍스트 메뉴 표시

combinedClickable를 사용하면 일반 클릭 동작 외에도 더블 탭 또는 길게 눌러야 하는 동작을 추가할 수 있습니다. combinedClickable를 사용하여 사용자가 그리드 이미지를 길게 터치할 때 컨텍스트 메뉴를 표시할 수 있습니다.

var contextMenuPhotoId by rememberSaveable { mutableStateOf<Int?>(null) }
val haptics = LocalHapticFeedback.current
LazyVerticalGrid(columns = GridCells.Adaptive(minSize = 128.dp)) {
    items(photos, { it.id }) { photo ->
        ImageItem(
            photo,
            Modifier
                .combinedClickable(
                    onClick = { activePhotoId = photo.id },
                    onLongClick = {
                        haptics.performHapticFeedback(HapticFeedbackType.LongPress)
                        contextMenuPhotoId = photo.id
                    },
                    onLongClickLabel = stringResource(R.string.open_context_menu)
                )
        )
    }
}
if (contextMenuPhotoId != null) {
    PhotoActionsSheet(
        photo = photos.first { it.id == contextMenuPhotoId },
        onDismissSheet = { contextMenuPhotoId = null }
    )
}

사용자가 요소를 길게 누르면 햅틱 반응을 포함하는 것이 좋습니다. 이 때문에 스니펫에 performHapticFeedback 호출이 포함되어 있습니다.

스림을 탭하여 컴포저블 닫기

위의 예에서 clickablecombinedClickable는 컴포저블에 유용한 기능을 추가합니다. 상호작용에 시각적 표시를 보여주고 마우스 오버에 반응하며 포커스, 키보드, 접근성 지원을 포함합니다. 하지만 이 추가 동작이 항상 바람직한 것은 아닙니다.

이미지 세부정보 화면을 살펴보겠습니다. 배경은 반투명해야 하며 사용자가 배경을 탭하여 세부정보 화면을 닫을 수 있어야 합니다.

이 경우 백그라운드에는 상호작용에 대한 시각적 표시가 없어야 하며 마우스 오버에 응답해서는 안 되고 포커스를 받을 수 없어야 하며 키보드 및 접근성 이벤트에 대한 응답이 일반적인 컴포저블의 응답과 다릅니다. clickable 동작을 조정하는 대신 더 낮은 추상화 수준으로 드롭다운하고 detectTapGestures 메서드와 함께 pointerInput 수정자를 직접 사용할 수 있습니다.

@Composable
private fun Scrim(onClose: () -> Unit, modifier: Modifier = Modifier) {
    val strClose = stringResource(R.string.close)
    Box(
        modifier
            // handle pointer input
            .pointerInput(onClose) { detectTapGestures { onClose() } }
            // handle accessibility services
            .semantics(mergeDescendants = true) {
                contentDescription = strClose
                onClick {
                    onClose()
                    true
                }
            }
            // handle physical keyboard input
            .onKeyEvent {
                if (it.key == Key.Escape) {
                    onClose()
                    true
                } else {
                    false
                }
            }
            // draw scrim
            .background(Color.DarkGray.copy(alpha = 0.75f))
    )
}

pointerInput 수정자의 키로 onClose 람다를 전달합니다. 이렇게 하면 람다가 자동으로 다시 실행되어 사용자가 스림을 탭할 때 올바른 콜백이 호출됩니다.

두 번 탭하여 확대

clickablecombinedClickable에 상호작용에 올바르게 응답하기에 충분한 정보가 포함되지 않는 경우가 있습니다. 예를 들어 컴포저블은 상호작용이 발생한 컴포저블 경계 내 위치에 액세스해야 할 수 있습니다.

이미지 세부정보 화면을 다시 살펴보겠습니다. 더블탭하여 이미지를 확대할 수 있도록 하는 것이 좋습니다.

동영상에서 볼 수 있듯이 탭 이벤트의 위치를 중심으로 확대가 발생합니다. 이미지의 왼쪽 부분을 확대하면 결과가 오른쪽 부분을 확대할 때와 다릅니다. pointerInput 수정자를 detectTapGestures와 함께 사용하여 탭 위치를 계산에 통합할 수 있습니다.

var zoomed by remember { mutableStateOf(false) }
var zoomOffset by remember { mutableStateOf(Offset.Zero) }
Image(
    painter = rememberAsyncImagePainter(model = photo.highResUrl),
    contentDescription = null,
    modifier = modifier
        .pointerInput(Unit) {
            detectTapGestures(
                onDoubleTap = { tapOffset ->
                    zoomOffset = if (zoomed) Offset.Zero else
                        calculateOffset(tapOffset, size)
                    zoomed = !zoomed
                }
            )
        }
        .graphicsLayer {
            scaleX = if (zoomed) 2f else 1f
            scaleY = if (zoomed) 2f else 1f
            translationX = zoomOffset.x
            translationY = zoomOffset.y
        }
)