탭한 후 누르기

많은 컴포저블에는 탭 또는 클릭 지원이 내장되어 있으며 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 동작을 조정하려고 하는 대신 더 낮은 추상화 수준으로 드롭다운한 다음 pointerInput 수정자를 detectTapGestures 메서드와 함께 직접 사용할 수 있습니다.

@OptIn(ExperimentalComposeUiApi::class)
@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
        }
)