동작 이해하기

이해해야 할 몇 가지 용어와 개념이 있습니다. 제스처를 처리할 때 발생합니다. 이 페이지에서는 동작으로 구성되며 다양한 추상화를 도입합니다. 동작 레벨을 제공합니다. 또한 이벤트 소비와 있습니다.

정의

이 페이지의 다양한 개념을 이해하려면 용어를 정리해 보겠습니다.

  • 포인터: 애플리케이션과 상호작용하는 데 사용할 수 있는 물리적 객체입니다. 휴대기기에서 가장 일반적인 포인터는 할 수 있습니다. 또는 스타일러스를 사용하여 손가락을 대신 사용할 수도 있습니다. 대형 화면의 경우 마우스나 트랙패드를 사용하여 합니다. 입력 장치는 '가리키기'할 수 있어야 합니다. 최대 90개의 포인터로 간주할 수 있으므로 키보드와 같은 것은 있습니다. Compose에서 포인터 유형은 PointerType
  • 포인터 이벤트: 하나 이상의 포인터의 하위 수준 상호작용을 설명합니다. 할 수 있습니다. 모든 포인터 상호작용(예: 화면에 손가락이나 마우스를 드래그하면 이벤트가 트리거됩니다. 포함 Compose, 이러한 이벤트와 관련된 모든 정보는 PointerEvent 클래스
  • 동작: 단일 동작으로 해석될 수 있는 일련의 포인터 이벤트입니다. 있습니다. 예를 들어 탭 동작을 아래로 스크롤하는 동작으로 간주할 수 있습니다. 업 이벤트가 뒤따르는 이벤트입니다. 많은 사람들이 사용하는 일반적인 동작이 있습니다. 탭, 드래그, 변환 등의 앱이 필요하지만 자체 커스텀 앱을 만들 수도 동작을 취합니다.

다양한 추상화 수준

Jetpack Compose는 동작 처리를 위한 다양한 수준의 추상화를 제공합니다. 가장 상위 수준은 구성요소 지원입니다. Button와 같은 컴포저블 동작 지원을 자동으로 포함합니다. 맞춤에 동작 지원을 추가하는 방법 clickable와 같은 동작 수정자를 임의의 이미지에 추가할 수 있습니다. 구성 가능한 함수입니다. 마지막으로 맞춤 동작이 필요한 경우 pointerInput 수정자.

일반적으로 추상화를 구현하는 가장 높은 수준의 추상화를 사용할 수 있습니다. 이렇게 하면 레이어에 있습니다 예를 들어 Button에는 원시 데이터보다 더 많은 정보를 포함하는 clickable보다 접근성이 높은 pointerInput 구현

구성요소 지원

Compose의 즉시 사용 가능한 여러 구성요소에는 일종의 내부 동작이 포함되어 있습니다. 있습니다. 예를 들어 LazyColumn는 드래그 동작에 다음과 같이 응답합니다. 콘텐츠를 스크롤하면 Button를 누르면 물결 효과가 표시됩니다. SwipeToDismiss 구성요소에는 닫기를 위한 스와이프 로직이 포함되어 있습니다. 요소가 포함됩니다. 이러한 유형의 동작 처리는 자동으로 작동합니다.

내부 동작 처리 외에도 많은 구성요소는 호출자가 다음을 수행하도록 요구합니다. 동작을 처리합니다. 예를 들어 Button는 자동으로 탭을 감지합니다. 클릭 이벤트를 트리거합니다. onClick 람다를 Button에 전달합니다. 반응합니다. 마찬가지로 onValueChange 람다를 Slider: 슬라이더 핸들을 드래그하는 사용자에게 반응합니다.

사용 사례에 적합한 경우 구성요소에 포함된 동작을 사용하는 것이 좋습니다. 포커스와 접근성을 위한 즉시 사용 가능한 지원을 포함하며 있습니다. 예를 들어 Button는 특별한 방식으로 표시되어 접근성 서비스에서 버튼이 아닌 버튼으로 올바르게 설명합니다. 다음과 같습니다.

// Talkback: "Click me!, Button, double tap to activate"
Button(onClick = { /* TODO */ }) { Text("Click me!") }
// Talkback: "Click me!, double tap to activate"
Box(Modifier.clickable { /* TODO */ }) { Text("Click me!") }

Compose의 접근성에 관한 자세한 내용은 Compose의 접근성을 참고하세요. 작성하기를 참고하세요.

수정자를 사용하여 임의의 컴포저블에 특정 동작 추가

임의의 컴포저블에 동작 수정자를 적용하여 컴포저블이 동작을 수신 대기합니다. 예를 들어 일반 Boxclickable를 만들어 탭 동작을 처리하거나 Column verticalScroll을 적용하여 세로 스크롤을 처리합니다.

다양한 유형의 동작을 처리하는 여러 수정자가 있습니다.

일반적으로 맞춤 동작 처리보다는 즉시 사용 가능한 동작 수정자를 사용하는 것이 좋습니다. 수정자는 순수 포인터 이벤트 처리 외에도 더 많은 기능을 추가합니다. 예를 들어 clickable 수정자는 누르기 감지를 추가할 뿐만 아니라 의미론적 정보, 상호작용에 대한 시각적 표시, 마우스 오버, 포커스 및 키보드 지원이 있습니다. 이 페이지에서 소스 코드 clickable의 데이터를 가져와 기능이 을(를) 추가하는 중입니다.

pointerInput 수정자를 사용하여 임의의 컴포저블에 맞춤 동작 추가

모든 동작이 즉시 사용 가능한 동작 수정자로 구현되는 것은 아닙니다. 대상 예를 들어 수정자를 사용하여 길게 누른 후 드래그에 반응할 수는 없습니다. Control-클릭 또는 세 손가락으로 탭하세요. 대신 동작을 직접 작성하여 이 맞춤 동작을 식별하는 데 사용됩니다. 다음 명령어를 사용하여 동작 핸들러를 만들 수 있습니다. pointerInput 수정자: 원시 포인터에 액세스할 수 있습니다. 이벤트를 수신합니다.

다음 코드는 원시 포인터 이벤트를 수신 대기합니다.

@Composable
private fun LogPointerEvents(filter: PointerEventType? = null) {
    var log by remember { mutableStateOf("") }
    Column {
        Text(log)
        Box(
            Modifier
                .size(100.dp)
                .background(Color.Red)
                .pointerInput(filter) {
                    awaitPointerEventScope {
                        while (true) {
                            val event = awaitPointerEvent()
                            // handle pointer event
                            if (filter == null || event.type == filter) {
                                log = "${event.type}, ${event.changes.first().position}"
                            }
                        }
                    }
                }
        )
    }
}

이 스니펫을 분할하면 핵심 구성요소는 다음과 같습니다.

  • pointerInput 수정자 하나 이상의 를 전달합니다. 이 이러한 키 중 하나의 값이 변경되면 수정자 콘텐츠 람다는 재실행됩니다 샘플은 선택적 필터를 컴포저블에 전달합니다. 만약 해당 필터의 값이 변경되면 포인터 이벤트 핸들러는 다음과 같아야 합니다. 올바른 이벤트가 로깅되는지 확인하기 위해 다시 실행되는 것입니다.
  • awaitPointerEventScope는 다음과 같이 코루틴 범위를 만드는 데 사용할 수 있습니다. 포인터 이벤트를 기다립니다.
  • awaitPointerEvent는 다음 포인터 이벤트까지 코루틴을 정지합니다. 발생합니다

원시 입력 이벤트를 수신 대기하는 것은 강력하지만 작성하기도 복잡합니다. 맞춤 동작을 구현합니다. 맞춤 인벤토리 생성을 간소화하기 위해 동작, 다양한 유틸리티 메서드를 사용할 수 있습니다.

전체 동작 감지

원시 포인터 이벤트를 처리하는 대신 특정 동작을 수신 대기할 수 있습니다. 적절한 조치를 취할 수 있습니다 AwaitPointerEventScope는 수신 대기 메서드:

최상위 검사 프로그램이므로 하나의 검사 프로그램 내에 여러 검사 프로그램을 추가할 수 없습니다. pointerInput 수정자. 다음 스니펫은 탭만 감지하며 드래그:

var log by remember { mutableStateOf("") }
Column {
    Text(log)
    Box(
        Modifier
            .size(100.dp)
            .background(Color.Red)
            .pointerInput(Unit) {
                detectTapGestures { log = "Tap!" }
                // Never reached
                detectDragGestures { _, _ -> log = "Dragging" }
            }
    )
}

내부적으로 detectTapGestures 메서드는 코루틴을 차단하고 두 번째 메서드는 없습니다 하나 이상의 동작 리스너를 구성 가능한 함수라면 별도의 pointerInput 수정자 인스턴스를 대신 사용하세요.

var log by remember { mutableStateOf("") }
Column {
    Text(log)
    Box(
        Modifier
            .size(100.dp)
            .background(Color.Red)
            .pointerInput(Unit) {
                detectTapGestures { log = "Tap!" }
            }
            .pointerInput(Unit) {
                // These drag events will correctly be triggered
                detectDragGestures { _, _ -> log = "Dragging" }
            }
    )
}

동작별 이벤트 처리

정의에 따라 동작은 포인터 아래로 이벤트로 시작됩니다. 이 awaitEachGesture 도우미 메서드를 while(true) 루프로 대체하여 각 원시 이벤트를 통과합니다. awaitEachGesture 메서드는 모든 포인터가 해제되면 이 블록이 포함됩니다. 이는 동작이 완료됨:

@Composable
private fun SimpleClickable(onClick: () -> Unit) {
    Box(
        Modifier
            .size(100.dp)
            .pointerInput(onClick) {
                awaitEachGesture {
                    awaitFirstDown().also { it.consume() }
                    val up = waitForUpOrCancellation()
                    if (up != null) {
                        up.consume()
                        onClick()
                    }
                }
            }
    )
}

실제로는 다음과 같은 경우를 제외하고 거의 항상 awaitEachGesture를 사용합니다. 포인터 이벤트에 응답할 수 없습니다. 이와 관련된 예는 hoverable - 포인터를 위 또는 아래 이벤트에 응답하지 않음: 단지 포인터가 경계에 진입하거나 이탈하는 시점을 알아야 합니다.

특정 이벤트 또는 하위 동작 대기

동작의 일반적인 부분을 식별하는 데 도움이 되는 일련의 메서드가 있습니다.

멀티터치 이벤트에 계산 적용

사용자가 두 개 이상의 포인터를 사용하여 멀티 터치 동작을 수행하는 경우 원시 값에 따라 필요한 변환을 이해하는 것은 복잡합니다. transformable 수정자 또는 detectTransformGestures인 경우 사용 사례에 대한 세부적인 제어를 충분히 제공하지 않는 경우 원시 이벤트를 수신 대기하고 이에 대해 계산을 적용합니다. 이러한 도우미 메서드 calculateCentroid, calculateCentroidSize, calculatePan, calculateRotation, calculateZoom

이벤트 전달 및 히트 테스트

모든 포인터 이벤트가 모든 pointerInput 수정자에 전송되는 것은 아닙니다. 이벤트 전달은 다음과 같이 작동합니다.

  • 포인터 이벤트는 구성 가능한 계층 구조로 전달됩니다. 한 순간은 새 포인터가 첫 번째 포인터 이벤트를 트리거하면 시스템에서 히트 테스트를 시작함 '자격요건 충족' 구성 가능한 함수입니다. 컴포저블이 다음과 같은 경우 운영 가능한 것으로 간주됩니다. 사용할 수 있습니다. UI 상단의 히트 테스트 흐름 표시됩니다. 컴포저블의 '조회' 포인터 이벤트가 발생한 시점 이 컴포저블의 경계 내에 있어야 합니다. 이 과정을 통해 다음과 같은 체인을 생성할 수 있습니다. 긍정적으로 히트 테스트를 하는 컴포저블이 있습니다.
  • 기본적으로 Z-색인이 가장 높은 컴포저블만 '조회'입니다. 대상 예를 들어 Box에 겹치는 Button 컴포저블 두 개를 추가하는 경우 위에 그려진 레이어가 모든 포인터 이벤트를 수신합니다. 이론적으로는 자체 PointerInputModifierNode를 만들어 이 동작을 재정의합니다. sharePointerInputWithSiblings를 true로 설정합니다.
  • 동일한 포인터에 대한 추가 이벤트는 해당 포인터의 동일한 체인으로 컴포저블이벤트 전파 로직에 따른 흐름을 처리합니다. 시스템 이 포인터에 대한 히트 테스트를 더 이상 실행하지 않습니다. 즉, 각 체인의 컴포저블이 해당 포인터의 모든 이벤트를 수신합니다. 이 컴포저블의 경계 밖에서 발생합니다. 구성 요소가 포함되지 않은 포인터 이벤트가 경계 안에 있어야 합니다.

마우스 오버 또는 스타일러스 마우스 오버로 트리거되는 마우스 오버 이벤트는 정의할 수 있습니다 마우스 오버 이벤트는 도달한 모든 컴포저블로 전송됩니다. 그래서 사용자가 한 컴포저블의 경계에서 다음 컴포저블의 경계에서 포인터를 가져갈 때 이벤트를 첫 번째 컴포저블로 보내는 대신, 새로운 컴포저블을 살펴보겠습니다.

이벤트 사용

둘 이상의 컴포저블에 동작 핸들러가 할당되어 있는 경우 충돌하지 않아야 합니다 예를 들어 다음 UI를 살펴보겠습니다.

이미지, 두 개의 텍스트가 있는 열, 버튼이 있는 목록 항목

사용자가 북마크 버튼을 탭하면 버튼의 onClick 람다가 이를 처리합니다. 동작입니다. 사용자가 목록 항목의 다른 부분을 탭하면 ListItem 해당 동작을 처리하고 기사로 이동합니다. 포인터 입력의 경우 Button은 이 이벤트를 소비하여 상위 요소가 알림을 사용하지 못하게 해야 합니다. 반응할 수 있습니다. 기본 구성요소 및 일반적인 동작 수정자에는 이러한 소비 동작이 포함되지만, 개발자는 이벤트를 수동으로 사용해야 합니다. 수행할 작업 PointerInputChange.consume 메서드 사용:

Modifier.pointerInput(Unit) {

    awaitEachGesture {
        while (true) {
            val event = awaitPointerEvent()
            // consume all changes
            event.changes.forEach { it.consume() }
        }
    }
}

이벤트를 소비해도 다른 컴포저블로의 이벤트 전파는 중지되지 않습니다. 가 대신 컴포저블은 소비된 이벤트를 명시적으로 무시해야 합니다. 작성 시 이미 다른 사람이 이벤트를 소비했는지 확인해야 합니다. 요소의 다음 속성을 사용합니다.

Modifier.pointerInput(Unit) {
    awaitEachGesture {
        while (true) {
            val event = awaitPointerEvent()
            if (event.changes.any { it.isConsumed }) {
                // A pointer is consumed by another gesture handler
            } else {
                // Handle unconsumed event
            }
        }
    }
}

이벤트 전파

앞서 언급했듯이 포인터 변경사항은 조회되는 각 컴포저블에 전달됩니다. 그러나 이러한 컴포저블이 두 개 이상 있는 경우 이벤트는 어떤 순서로 무엇인가요? 마지막 섹션의 예를 보면 이 UI는 ListItemButton만 응답하는 다음 UI 트리 포인터 이벤트:

트리 구조 맨 위 레이어는 ListItem이고 두 번째 레이어에는 Image, Column, Button이 있으며 Column은 두 개의 Text로 분할됩니다. ListItem과 Button이 강조표시되어 있습니다.

포인터 이벤트는 이러한 각 컴포저블을 통해 세 번 흐릅니다. "passes":

  • 초기 패스에서 이벤트는 UI 트리 상단에서 있습니다. 이 흐름을 사용하면 상위 요소가 이벤트를 가로채기 전에 사용할 수 없습니다. 예를 들어 도움말에서 길게 눌러야 합니다. Google의 예를 들어 ListItemButton 전에 이벤트를 수신합니다.
  • 기본 패스에서 이벤트는 UI 트리의 리프 노드에서 UI 트리의 루트입니다. 이 단계에서 일반적으로 동작을 소비하며 기본 패스입니다. 이 패스의 동작 처리 즉, 리프 노드가 상위 노드인 리프 노드보다 대부분의 동작에 대해 가장 논리적인 동작을 수행합니다. 이 예에서 ButtonListItem 전에 이벤트를 발생시킵니다.
  • 최종 패스에서 이벤트가 UI 상단에서 한 번 더 진행됩니다. 리프 노드로 전달됩니다 이 흐름을 통해 스택의 상위 요소는 이벤트 소비에 응답할 수 있습니다. 예를 들어 버튼은 누르기가 스크롤 가능한 상위 요소의 드래그로 전환될 때 물결 효과 표시가 있습니다.

시각적으로 이벤트 흐름은 다음과 같이 나타낼 수 있습니다.

입력 변경이 소비되면 이 정보는 이러한 점들이 있습니다.

코드에서 관심 있는 패스를 지정할 수 있습니다.

Modifier.pointerInput(Unit) {
    awaitPointerEventScope {
        val eventOnInitialPass = awaitPointerEvent(PointerEventPass.Initial)
        val eventOnMainPass = awaitPointerEvent(PointerEventPass.Main) // default
        val eventOnFinalPass = awaitPointerEvent(PointerEventPass.Final)
    }
}

이 코드 스니펫에서는 이러한 메서드는 메서드 호출을 대기하지만 소비에 대한 데이터는 변경할 수 있습니다.

동작 테스트

테스트 메서드에서 performTouchInput 메서드를 사용하여 지도 가장자리에 패딩을 추가할 수 있습니다. 이렇게 하면 상위 수준 전체 동작 (예: 손가락 모으기 또는 길게 클릭) 또는 낮은 수준 동작 (예: 커서를 특정 픽셀만큼 이동)

composeTestRule.onNodeWithTag("MyList").performTouchInput {
    swipeUp()
    swipeDown()
    click()
}

더 많은 예는 performTouchInput 문서를 참고하세요.

자세히 알아보기

Jetpack Compose의 동작에 관한 자세한 내용은 다음을 참고하세요. 리소스: