앱 기본사항

Jetpack Compose를 사용하여 터치스크린을 넘어 입력 상호작용 확장

스피커

  • 알렉스 바뇨

    소프트웨어 엔지니어

  • 제레미 워커

    Compose 소프트웨어 엔지니어

정보

강연 소개

새로운 폼 팩터가 터치스크린을 넘어 발전함에 따라 Compose의 입력 시스템도 이러한 과제를 해결하기 위해 진화했습니다. 계층화된 아키텍처는 고급 트랙패드 동작부터 새로운 하드웨어 입력까지 모든 것을 원활하게 지원하는 기반을 제공합니다.

명확한 아키텍처 분석과 실습 코드 예시를 통해 OS 플랫폼 레이어에서 Compose 동작 감지기 및 UI 구성요소에 이르기까지 입력 이벤트의 수명 주기를 따라가 봅니다. 전체 스택에서 이벤트를 추적하고, 새로운 입력 유형에 주요 아키텍처 패턴을 적용하고, 자체 앱에서 복잡한 동작 상호작용을 자신 있게 디버그하는 방법을 알아봅니다.

요약

  • Jetpack Compose는 6단계 파이프라인을 통해 수신되는 Android 포인터 이벤트를 처리합니다. 이 파이프라인은 히트 경로 트리를 유지하고 UI 노드 전반에 걸쳐 세 번의 패스로 이벤트를 전달합니다.
  • Compose의 트랙패드 입력이 이제 터치 대신 마우스 이벤트에 매핑됩니다. 또한 Compose는 플랫폼 가짜 손가락 이벤트를 전용 화면 이동 및 크기 조절 포인터 이벤트로 변환합니다.
  • 지능형 글라스 트랙패드는 화면 좌표 터치 대신 간접 입력 유형을 도입합니다. 기본 방향 모션 축을 사용하여 가로 스와이프를 세로 다음 또는 이전 탐색에 매핑합니다.
  • 테스트 지원이 performTrackpadInput과 같은 새로운 방법을 통해 터치 이상으로 확장되었습니다. 이러한 API는 앱이 비터치 기기를 올바르게 처리하는지 검증할 수 있는 익숙한 테스트 노출 영역을 제공합니다.

Gemini Notebook

주제

  • jetpack-compose
  • pointer-input
  • 트랙패드
  • 동작
  • intelligent-eyewear
  • 접근성

스크립트

소개 및 입력 철학

  • Hi, everyone!

  • Android Developer Summit에서 즐거운 시간을 보내고 계시기를 바라며, 강연에 참석해 주셔서 감사합니다.

  • 저는 Compose팀의 소프트웨어 엔지니어인 알렉스 바뇨입니다. Compose팀의 소프트웨어 엔지니어인 제레미도 함께합니다.

  • 저희는 Jetpack Compose의 입력 시스템을 개선하기 위해 노력해 왔으며, 오늘은 터치스크린을 넘어 비터치 입력을 기본적으로 더 쉽게 지원할 수 있도록 확장한 방법에 대해 이야기해 보겠습니다.

  • 입력은 광범위하게 사용자가 앱과 상호작용하는 모든 방식을 포착합니다.

  • 터치스크린은 모바일 앱에서 가장 먼저 떠오르는 입력 방식이지만, 유일한 입력 방식은 아닙니다.

  • 다른 유형의 입력에는 실제 키보드, 마우스, 트랙패드, 스타일러스, 컨트롤러, 로터리 기기가 있습니다.

  • 또한 지능형 안경의 트랙패드, XR의 눈 및 손 추적, Wear의 손가락 및 손목 동작과 같은 새로운 입력 유형도 등장하고 있습니다.

  • 더 일반적인 유형의 입력도 있습니다. 텍스트 편집을 위한 소프트웨어 IME, 마이크, 카메라, 기타 기기 센서 등이 있습니다.

  • 앱이 다양한 입력 유형과 함께 사용될 것으로 예상해야 합니다.

  • 그 이유 중 하나는 애플리케이션이 실행되는 기기의 종류가 계속 증가하고 있으며 이러한 기기의 적응성이 높아지고 있기 때문입니다.

  • 기기 폼 팩터에는 가장 일반적인 입력 방법이 있을 수 있지만 특정 기기 폼 팩터가 특정 입력 유형의 사용을 의미한다고 가정하지 않는 것이 중요합니다.

  • Chromebook에는 트랙패드와 키보드가 내장되어 있지만 이러한 입력 유형이 이 폼 팩터에만 국한되는 것은 아닙니다.

  • 휴대전화, 태블릿, TV에서도 실제 키보드, 트랙패드, 마우스가 사용될 수 있으며 일부 TV에는 터치 스크린도 있습니다.

  • 여러 입력 유형을 예상하고 지원해야 하는 두 번째 큰 이유는 접근성입니다.

  • 사용자가 원하는 입력 장치를 사용하고 적절하다고 판단되는 입력 장치로 전환할 수 있도록 허용하는 것은 애플리케이션의 접근성과 사용성을 높이는 올바른 방법입니다.

  • 관할권에 따라 이러한 입력 유형, 특히 키보드 및 마우스와 같은 항목에 대해 애플리케이션에 특정 호환 동작을 포함하는 것이 현지 법규일 수 있습니다.

  • 간단히 말해 폼 팩터와 관계없이 다양한 입력 유형을 지원해야 합니다.

  • 그럼 Jetpack Compose가 어떻게 앱이 이러한 사용자 요구를 충족하도록 지원하는지 살펴보겠습니다.

  • 일반적으로 Compose의 철학은 이러한 다양한 입력 유형이 특별한 조치 없이도 표준 구성요소와 함께 즉시 적절하게 작동해야 한다는 것입니다.

  • 그런 다음 필요에 따라 동작을 맞춤설정하고 조정하는 데 필요한 도구를 갖추어야 합니다.

  • 따라서 이러한 초기 입력 유형 외에도 기본 플랫폼에서 더 많은 입력 유형을 지원하게 되면 Compose 구성요소에 입력 유형 지원을 빌드하여 Compose 버전만 업데이트해도 애플리케이션의 동작을 개선할 수 있도록 하는 것이 목표입니다.

  • 이 강연에서는 Compose에 최근 추가된 두 가지 예시를 살펴보겠습니다.

  • 우연히도 둘 다 트랙패드와 같은 기기이지만, 보시다시피 매우 다른 방식으로 사용됩니다.

  • 노트북 스타일의 트랙패드에 관한 최근 작업부터 살펴보겠습니다.

  • 트랙패드는 포인터 기반 입력 장치입니다.

  • 더 구체적으로 말하자면 트랙패드를 사용하면 2차원 좌표를 얻을 수 있습니다.

  • 하지만 터치스크린과 달리 트랙패드에서 손가락의 좌표는 UI의 좌표 공간에 직접 있지 않습니다.

  • 대신 노트북과 같은 트랙패드의 경우 커서를 제어하는 변환이 있습니다.

  • 이 변환 과정에서 커서 가속, 팜 리젝션과 같은 복잡한 플랫폼 변환이 많이 발생할 수 있습니다.

  • 따라서 Compose 측에서 얻는 것은 일반적으로 마우스 입력처럼 작동하는 커서 위치에 해당하는 포인터입니다.

  • Android 플랫폼에서 마우스 오버 정보, 보조 또는 삼차 클릭을 포함한 클릭을 가져오고 이 모든 정보는 MotionEvent로 래핑됩니다.

  • 최신 버전의 Compose에서 동작이 어떻게 개선되었는지 이해하기 위해 먼저 Compose가 이러한 포인터 이벤트를 더 면밀히 처리하는 방법을 간략하게 살펴보겠습니다.

포인터 입력 처리의 6단계

  • 이 과정을 6단계로 나누어 차례대로 살펴보겠습니다.

  • 첫 번째 단계는 MotionEvent가 Compose로 이동하는 것입니다.

  • 따라서 Compose는 내부적으로는 단지 뷰일 뿐입니다. 많은 작업을 수행하는 매우 복잡한 뷰이지만 단지 뷰일 뿐이므로 포인터 입력을 위해 전체 Android 시스템 내에서 작동해야 합니다.

  • 따라서 Compose에서는 Compose가 일반 활동 내의 일반 창 내에 있으므로 Android 플랫폼의 MotionEvent를 가져옵니다.

  • 그러면 Compose는 이를 처리하고 포인터 입력과 관련하여 Compose 내에서 원하는 모든 작업을 실행해야 합니다.

  • 첫 번째 단계는 이러한 MotionEvent를 Compose에서 PointerInputEvent라고 하는 내부 플랫폼 중립 표현으로 변환하는 것입니다.

  • 이는 내부 유형이므로 직접 볼 수는 없습니다.

  • 여기에서 발생하는 모든 세부정보를 확인하려면 MotionEventAdapter를 살펴보면 되지만 여기서는 세부정보를 건너뛰겠습니다.

  • 이제 Compose에 플랫폼에 구애받지 않는 MotionEvent 표현이 있습니다.

  • 다음으로, 입력은 일반적으로 시간이 지남에 따라 발생합니다.

  • 입력 이벤트가 연달아 발생하므로 시간이 지남에 따라 어떤 일이 발생하는지 실제로 파악하는 것이 매우 유용합니다.

  • 동작, 속도 추적, 슬롭 감지와 같은 것들은 모두 각 시점에 어떤 일이 발생하는지 이해해야 합니다.

  • 따라서 이 처리 파이프라인의 다음 단계는 이 시간 정보를 추가하는 것이며, 연속적인 포인터 이벤트를 기반으로 일부 사전 처리를 수행합니다.

  • 코드에서는 이를 현재 PointerInputEvent를 가져와 이전 PointerInputEvent에서 발생한 이벤트와 결합한 다음 시간 인식 InternalPointerEvent를 만드는 것으로 볼 수 있습니다.

  • 이름에서 알 수 있듯이 이 역시 내부 유형이지만, 다양한 포인터 ID를 포인터 입력 변경사항에 매핑하는 것으로 생각할 수 있으며 PointerInputChanges는 공개 유형입니다.

  • 첫 번째 질문입니다.

  • 다양한 포인터 ID를 고려할 때 알 수 있는 점은 이제 시간 추적을 인식하므로 포인터 ID가 나타나고 사라지는 시점도 추적할 수 있다는 것입니다.

  • 하나만 있는 것이 아닙니다.

  • 터치 스크린에 여러 손가락을 대면 여러 포인터가 동시에 나타났다가 사라질 수 있으며 이를 처리해야 합니다.

  • 세부정보를 자세히 살펴보려면 이 모든 작업을 실행하는 PointerInputChangeEventProducer를 살펴보면 되지만 여기서는 건너뛰겠습니다.

  • 아직 살펴볼 내용이 많이 남아 있습니다.

  • 이제 입력의 Compose 표현이 있고 시간 정보가 있습니다.

  • 다음 단계는 해당 이벤트로 무엇을 할지 파악하는 것입니다.

  • 어디로 보내야 하나요?

  • 이로 인해 Hit Path 테스트가 진행됩니다.

  • MotionEvent가 전송되고 이 ComposeView가 전체 창, 어쩌면 전체 화면을 채우는 상황을 상상해 보겠습니다.

  • 어디로 보내야 할지 알아봐야 합니다.

  • Compose는 뷰일 뿐이지만 Compose 내부에는 수많은 항목, 다양한 노드, 버튼, 상호작용하는 항목이 있으므로 Compose에서 특정 위치에서 발생하는 이벤트를 어디로 보낼지 파악하고 실제로 어떤 노드가 관심이 있는지 파악하는 모든 작업을 수행해야 합니다.

  • 이러한 레이아웃 노드에는 좌표, 경계, 도형이 있으므로 어떤 노드가 적중되는지 파악해야 합니다.

  • 하지만 이 역시 가장 일반적인 관점은 아닙니다.

  • 목록 안에 버튼을 넣는 것처럼 상호작용 가능한 항목 안에 다른 상호작용 가능한 항목을 넣는 것을 좋아합니다.

  • 이런 일이 자주 발생합니다.

  • 따라서 일반적인 경우, 특정 입력 이벤트는 입력 이벤트의 좌표에 의해 적중되는 모든 상호작용 가능한 노드를 통과하는 전체 적중 경로를 생성합니다.

  • 따라서 이 경우 중첩 계층 구조를 거치면서 3이 됩니다.

  • 따라서 조회 경로가 있지만 이것만으로는 충분하지 않습니다.

  • 여러 포인터 입력 이벤트가 있고 여러 포인터가 다운될 수 있다고 말씀드린 것을 기억하시나요?

  • 전체 히트 경로 트리가 필요합니다.

  • 포인터가 여러 개 있는 경우 Hit Path가 하나만 있는 것은 아닙니다.

  • 이것이 다음 단계인 적중 경로 트리를 만들고 업데이트하는 단계입니다.

  • 이것이 진행 상황을 더 자세히 보여주는 것입니다.

  • 왼쪽에는 시각적으로 배치된 노드가 있고 왼쪽 하단에는 계층적 표현이 표시됩니다. 어떤 노드가 다른 노드 내부에 있는지, 어떤 노드가 서로의 상위 또는 하위 요소인지 확인할 수 있습니다.

  • 포인터가 내려가면 트리를 통과하는 전체 경로인 적중 경로를 추적한 다음 특정 포인터의 적중 경로를 가져옵니다.

  • 이 히트 경로는 두 번째 포인터를 가져올 때 트리가 됩니다.

  • 다른 경로와 동일한 노드 중 일부와 교차할 수도 있지만 다른 노드와 상호작용할 수도 있습니다.

  • 오른쪽은 현재 유지 관리 중인 적중 경로 트리입니다.

  • 포인터는 나타났다가 사라질 수 있으므로, 현재 진행 중인 모든 이벤트에 관심이 있는 노드의 전체 목록을 확보하려면 시간이 지남에 따라 이를 직접 유지해야 합니다.

  • 좋습니다.

  • 이제 이러한 입력 이벤트에 대해 유지 관리하는 히트 경로 트리가 있으므로 다음 단계로 넘어갑니다.

  • 이제 포인터 입력 이벤트를 전송할 위치와 각 특정 이벤트에 관심이 있는 노드를 알 수 있습니다.

  • 이제 이를 알았으니 좌표 변환을 수행할 수 있습니다.

  • 노드가 자체 로컬 좌표 공간 내에서 입력 이벤트를 가져오면 좌표 수학을 직접 수행할 필요가 없으므로 매우 편리합니다.

  • 이를 알게 되면 포인터 입력 이벤트를 가져와 관심 있는 노드의 좌표와 결합할 수 있습니다. 그러면 최종적으로 공개 PointerEvent가 생성되어 여러분과 포인터 입력 수정자에게 전달됩니다.

  • 이것이 6단계의 마지막 단계로, 실제로 이벤트를 전송하는 단계입니다.

  • 이것은 이벤트가 발생한 포인터 입력 이벤트이며, 노드로 전송되어 원하는 작업을 수행합니다.

  • 이 작업은 세 가지 단계로 진행됩니다.

  • 초기 패스, 기본 패스, 최종 패스가 있습니다.

  • 세 가지 패스를 사용하는 이유는 노드가 서로 통신하고 특정 포인터 입력 이벤트의 특정 부분에 대한 관심을 전달한 다음 디스패치의 일부로 다양한 부분을 사용할 수 있도록 하기 위함입니다.

  • 따라서 첫 번째 패스, 즉 초기 패스는 상위 요소에서 하위 요소로 이동합니다.

  • 이 기능이 유용한 예는 일부 외부 레이아웃 노드가 스크롤 컨테이너일 수 있고 스크롤이 시작되었음을 알고 있으며 포인터 이벤트가 하위 요소에 도달하지 못하도록 중지하려는 경우입니다.

  • 이렇게 하면 초기 패스에서 이벤트를 소비할 수 있으며 하위 요소는 더 이상 이벤트를 가져오지 않습니다.

  • 두 번째 패스는 기본 패스입니다.

  • 그리고 이것은 하위 요소에서 상위 요소로 이동하는 유일한 요소입니다.

  • 이름에서 알 수 있듯이 이 패스는 작업을 수행하는 기본 패스입니다.

  • 예를 들어 하위 요소가 다운 이벤트를 소비하여 클릭 처리를 시작하는 경우, 중첩된 클릭 가능 요소가 있다면 상위 요소보다 먼저 클릭 처리를 수행할 수 있습니다.

  • 마지막 단계인 최종 단계는 초기 단계와 동일하게 상위 요소에서 하위 요소로 이동합니다.

  • 여기에서 하위 요소는 상위 요소가 이벤트의 일부를 소비했다고 상위 요소에 알릴 수 있습니다.

  • 따라서 하위 요소가 클릭을 처리하기 시작했지만 상위 요소가 스크롤을 시작한 경우 하위 요소는 상위 요소가 스크롤을 시작했음을 확인하고 처리를 시작한 클릭을 취소할 수 있습니다.

  • 이것이 세 가지 패스와 여섯 가지 단계입니다.

  • 이는 포인터 입력 시스템 위에 있는 모든 것의 기반입니다.

  • 모든 동작 처리, 스크롤 가능, 드래그 가능, 클릭 가능, 맞춤 포인터 입력은 모두 이 기반 위에 빌드됩니다.

트랙패드 업데이트 및 여러 손가락 동작

  • 이제 Compose에서 트랙패드 동작 지원을 개선한 구체적인 방법에 관해 조금 이야기해 보겠습니다.

  • 따라서 Compose 1.12 이전에는 트랙패드 입력 이벤트가 터치 포인터 유형으로 보고되었습니다. 손가락이 트랙패드와 물리적으로 상호작용하여 커서를 제어하기 때문입니다.

  • 하지만 사용자는 이 입력 유형이 실물 마우스와 훨씬 더 유사하게 작동할 것으로 기대합니다.

  • 따라서 포인터 유형을 PointerType.Touch에서 PointerType.Mouse로 변경했습니다.

  • 이 변경사항을 통해 사용자는 이 정보를 확인하는 모든 구성요소에서 훨씬 더 일관되게 정렬된 마우스 및 트랙패드 환경을 확인할 수 있습니다.

  • 예를 들어 도움말 동작의 경우 트랙패드와 마우스에 동일한 도움말 동작을 적용하는 것이 좋습니다.

  • 트랙패드와 관련된 두 번째 큰 변화는 트랙패드에서 수행하는 여러 손가락 동작과 관련이 있습니다.

  • 이전에는 Compose가 Android 플랫폼의 영리하지만 다소 기괴한 가짜 손가락 개념을 통과했습니다.

  • 이것이 무엇을 의미하는지 살펴보겠습니다.

  • 트랙패드에서 두 손가락을 사용하여 화면을 이동하면 Android는 커서가 있는 위치에 가상의 터치스크린 손가락 하나를 생성합니다.

  • 그런 다음 트랙패드에서 손가락을 움직이면 터치스크린의 가짜 손가락이 원하는 동작을 모방하기 위해 올바른 거리와 방향으로 움직인 다음 손을 뗍니다.

  • 따라서 트랙패드에서 두 손가락을 위로 움직이면 터치스크린에서 하나의 가짜 손가락이 위로 움직이는 것으로 변환되어 아래로 스크롤됩니다.

  • 여기서 재미있는 점은 저처럼 트랙패드 역방향 스크롤 기능을 사용하는 경우 스크롤이 실제로 반대 방향으로 이동하기를 원한다는 것입니다.

  • 이를 달성하는 방법은 가짜 손가락이 향하는 방향을 뒤집는 것입니다.

  • 이제 트랙패드에서 두 손가락을 위로 움직이면 터치스크린에서 가짜 손가락 하나가 아래로 움직이는 것으로 변환되어 위로 스크롤됩니다. 이전과는 반대입니다.

  • 확장도 비슷한 방식으로 작동합니다.

  • 트랙패드에서 두 손가락을 사용하여 크기를 조절하면 Android는 두 개의 가짜 터치스크린 손가락을 생성하여 동작에 따라 아래로 이동한 다음 서로 가까워지거나 멀어지게 하여 원하는 확대 또는 축소 동작을 모방합니다.

  • 따라서 이동의 경우 Compose 1.12부터, 크기 조절의 경우 Compose 1.13부터 가짜 손가락을 가로챕니다.

  • 플랫폼에서 제공하는 대로 전송하지 않고 새로운 포인터 이벤트 유형으로 재해석합니다. 패닝의 경우 PanStart, PanMove, PanEnd, 크기 조절의 경우 ScaleStart, ScaleChange, ScaleEnd입니다.

  • 이 작업은 앞서 언급한 Android에서 Compose로의 이벤트 변환 단계에서 이루어집니다.

  • 따라서 가짜 손가락 이벤트를 적절한 유형으로 변환한 다음 이 플랫폼 정의 동작을 통해 커서에 해당하는 적중 경로로 전송합니다.

  • 이것이 왜 중요한지 살펴보겠습니다.

  • 이러한 작업을 하는 이유는 무엇일까요?

  • 여기 이 상자가 확대하고 싶은 멋진 이미지라고 가정해 보겠습니다.

  • 트랙패드에서 두 손가락을 사용하여 크기를 조절할 때 두 개의 가짜 손가락 중 하나가 요소를 놓칠 수 있었습니다.

  • 적중 경로는 이미지와 교차하지 않으며 경계 외부에서 시작됩니다.

  • 따라서 크기 조절 대신 이미지가 스크롤될 수 있습니다.

  • 이미지 외부에 스크롤 가능한 다른 요소가 있는 경우 해당 요소도 함께 스크롤되기 시작할 수 있습니다.

  • 사용자가 원하는 것이 아닙니다.

  • 확장하고 싶었습니다.

  • 이제 가짜 손가락을 통해 전송하는 대신 커서 위치 아래의 요소에만 크기 조절 동작을 전송하여 올바른 동작을 유도합니다.

  • 커서의 위치나 크기를 조절하려는 요소의 가장자리에 얼마나 가까운지에 관계없이 이제 크기가 올바르게 조절됩니다.

  • 좋은 소식은 이러한 재해석된 입력 이벤트가 기본적으로 scrollable 및 transformable과 같은 구성요소에서 사용되어, 이러한 기본 구성요소를 사용하는 경우 트랙패드 입력에 대해 더 높은 충실도의 동작을 제공한다는 것입니다.

  • 이러한 구성요소를 기반으로 빌드된 항목을 사용하는 경우 Compose 버전을 최신 버전으로 업데이트하면 개선된 동작을 얻을 수 있습니다.

  • 맞춤 포인터 입력 수정자를 작성하는 경우 이러한 새로운 API를 살펴보고 지원하여 다양한 입력 장치에서 사용자에게 최상의 환경을 제공하시기 바랍니다.

  • 물론 테스트 API도 추가했으므로 앱이 이러한 다양한 입력 유형을 모두 올바르게 처리하는지 확인할 수 있습니다.

  • 따라서 performTrackpadInput는 마우스 및 터치용 기존 테스트 API와 매우 유사합니다.

    onNodeWithContentDescription("Photo")
        .performTrackpadInput {
            click(center)
    
            pan(Offset(0f, 100f))
    
            scale(0.9f)
        }
  • 이러한 모든 기능을 사용하여 사용자가 기기에서 어떤 입력 장치를 사용하든 앱이 올바르게 작동하는지 확인할 수 있습니다.

  • 이 설명이 터치패드에 손가락을 대는 것부터 앱에 이르기까지 포인터 이벤트가 어떻게 진행되는지 이해하는 데 도움이 되었기를 바랍니다.

  • 이제 제러미에게 넘겨 트랙패드가 사용되는 매우 다른 방식에 대해 이야기해 보겠습니다.

간접 입력 소개

  • 감사합니다.

  • 좋습니다.

  • 안녕하세요, 제레미 워커입니다.

  • 저는 입력 관련 작업을 많이 하는데요. Compose 입력과 관련해 진행 중인 새로운 작업과 이것이 여러분에게 미치는 영향에 관해 말씀드리면 흥미로울 것 같았습니다.

  • 특히 이 간접 입력 유형은 지능형 안경이라는 새로운 폼 팩터를 기반으로 한 새로운 입력 방식 때문에 등장했습니다.

  • 안경에서 입력을 받을 때는 몇 가지 방법이 있습니다. 무언가를 보거나, 음성을 사용하거나, 동작을 사용할 수 있지만, 저는 특히 터치패드에 대해 이야기하겠습니다.

  • 이것은 알렉스가 말한 것과는 조금 다릅니다. 머리 옆에 있고, 수직이고, 매우 넓고, 매우 짧습니다.

  • Compose에서 이 기능이 어떻게 작동할지 생각해 보았습니다. 눈앞에 작은 버튼이 있다고 상상해 보세요. 커서를 움직여 버튼을 터치하려고 하면 좋은 경험이 되지 않을 것입니다.

  • 그렇다면 어떻게 지원해야 할까요?

  • 참고로 Glasses UI는 다음과 같습니다.

  • 시야에 콘텐츠를 원활하게 배치하도록 특별히 설계되었습니다.

  • 하지만 매우 간단합니다. 간단하게 유지하려고 노력합니다.

  • 여기 예시를 보면 목록이 표시됩니다.

  • 중앙의 특정 요소에 초점이 맞춰져 있습니다.

  • 우리가 정말로 하고 싶은 일은 무엇인가?

  • 선택하거나 이전 또는 다음으로 이동하고 싶을 수 있습니다.

  • 화면 위치는 필요하지 않습니다.

  • 이건 조금 더 복잡하지만 기본적으로 세 가지 옵션이 있습니다. 전화를 끊거나, 음소거를 해제하거나, 동영상을 끌 수 있습니다.

  • 하지만 실제로는 선택하거나 다른 항목으로 이동하여 선택해야 합니다.

  • 따라서 화면의 위치나 이 경우 눈에 직접 매핑할 필요는 없다고 생각합니다.

  • 실제 모습은 이렇습니다.

  • 예를 들어 여기에서 요소 간에 이동하고 있으며, 이는 Glasses UI에 있고, 이는 우리가 원하는 것과 일치합니다.

  • 탭할 수도 있습니다.

  • 그래서 Android를 살펴보면서 이를 지원하는 데 사용할 수 있는 것이 무엇인지 생각했습니다.

  • 그리고 여러분이 만약, 죄송합니다.

  • 여기서는 건너뛰겠습니다.

  • 이전으로 돌아가겠습니다.

  • 다음은요

  • 좋습니다.

  • 이러한 기기를 사용해 본 적이 있고 Android 기기와 연결한 적이 있다면 UI 요소를 한 기기에서 다른 기기로 쉽게 이동할 수 있다는 것을 알고 계실 것입니다.

  • 예를 들어 리모컨을 사용하면 Google TV와 연결되어 있고 영화 그리드가 있는 경우 화살표 버튼을 눌러 원하는 영화로 이동하고 버튼을 눌러 선택할 수 있습니다.

  • 게임 메뉴가 있고 이 컨트롤러가 있는 경우 D패드 또는 조이스틱을 사용하여 메뉴의 UI 요소로 이동하여 선택할 수 있습니다.

  • 키보드도 마찬가지입니다.

  • 이는 Compose에서 원하는 것과 일치합니다.

  • 다시 한번 말씀드리지만, 여기에서 어떤 모습인지 확인할 수 있습니다.

  • 조금 더 거슬러 올라가면 TV와 페어링된 터치패드 유형이 있었는데, 이는 탐색 터치라는 것을 사용했습니다.

  • 여기서 핵심은 x 및 y 좌표가 화면과 전혀 연결되지 않는다는 것입니다.

  • 동작 감지에만 사용됩니다.

  • 오른쪽으로 스와이프하면 화살표를 눌러 넘어가는 것과 같은 효과가 있습니다.

  • 왼쪽으로 스와이프하면 왼쪽, 위, 아래로 이동합니다.

  • 이 방법으로 원하는 결과를 대부분 얻을 수 있습니다.

  • 드래그와 스와이프 등을 파악하기 위해 속도 추적과 같은 작업에만 x와 y를 사용합니다. 따라서 Compose를 시작하는 데 필요한 대부분의 정보를 얻을 수 있습니다.

  • TV에 매핑되어 있기 때문에 정확하지 않습니다. 따라서 TV 경로로 이동하지 않고 나중에 TV를 망치지 않도록 분할해야 합니다. 이 부분은 잠시 후에 살펴보겠습니다.

  • 하지만 시작점이 됩니다.

  • 이러한 유형의 터치 탐색을 사용할 수 있습니다.

간접 입력 및 새로운 폼 팩터를 위한 API 설계

  • 좋습니다.

  • API 설계는 어떤 모습일까요?

  • Compose에서 가장 일반적이거나 가장 흔한 입력은 터치입니다.

  • 따라서 이와 관련된 작업이 많습니다.

  • 그래서 이와 어떻게 관련이 있는지 파악해야 했습니다.

  • 참고로 PointerEvent는 다음과 같습니다.

  • 여기에서 위로 이동하는 아래쪽 이동 유형이 있고, 손가락으로 생각할 수 있는 목록인 PointerInputChange가 있습니다.

  • 손가락을 세 개 접었다면 목록에 세 가지 항목이 있습니다.

  • 그래서 이와 같은 모습으로 만들고 싶었습니다.

  • 이 모듈은 UI 모듈이 아니며, 대부분의 개발자가 입력을 처리할 때 사용하는 모듈보다 훨씬 하위 모듈입니다.

  • 기반 레이어 위에 있는 레이어가 될 것입니다.

  • 여기에 모든 동작 감지기가 있습니다.

  • 개발자는 이러한 세부적인 입력에 신경 쓰지 않기 때문입니다.

  • 드래그, 클릭, 스와이프가 발생했는지 확인하고 그에 따라 작업을 실행하기만 하면 됩니다.

  • 따라서 이를 수행하는 코드가 많이 있으며, 이러한 코드는 내부적으로 이러한 기능에 의해 구동됩니다.

  • 그래서 Foundation에 있는 코드가 이러한 새로운 유형을 쉽게 지원할 수 있도록 최대한 쉽게 만들어야겠다고 생각했습니다.

  • 그래서 거의 동일하게 보이는 새로운 유형을 만들었습니다.

  • 간접이라고 부를 뿐입니다.

  • 간접적이라는 것은 화면을 직접 터치하지 않는다는 의미입니다.

  • 따라서 터치 관점에서 생각하지 말고 이 기능이 출시될 예정이라는 점을 알려줍니다.

  • 여기서는 동작 처리와 UI 요소 간 탐색만 수행한다고 생각해 보세요.

  • 유형이 동일합니다.

  • 손가락 목록이 있습니다.

  • 이 경우에는 IndirectPointerInputChange입니다.

  • 그렇게 했더니 효과가 좋았습니다.

  • 하지만 기본 방향 모션 축이라는 항목이 하나 더 있었습니다.

  • 말이 좀 길지만, 설명해 드리면 이해가 되실 겁니다. 어떻게 만들어졌는지 살펴보면 꽤 흥미롭거든요.

  • 어떻게 시작되었나요?

  • 다시 말씀드리지만, 머리 옆에 터치패드가 있고, 넓고 짧습니다.

  • 이 시점에서 터치 탐색이라는 아이디어가 떠올랐습니다.

  • 왼쪽이나 오른쪽으로 스와이프할 수 있습니다.

  • 잘 작동합니다.

  • 하지만 현재 트랙패드가 어떻게 존재하는지 생각해 보면,

  • 왼쪽이나 오른쪽으로 이동할 때는 UI 콘텐츠가 있는 트랙패드에서 왼쪽이나 오른쪽으로 이동합니다.

  • 위 또는 아래로 이동하면 목록을 위 또는 아래로 스크롤합니다.

  • 안경을 쓰고 해보면, 이 정도면 나쁘지 않죠?

  • 하지만 이렇게 하면 좋은 경험이 아닙니다.

  • 실제로 가장 자연스러운 동작은 탭이지만, 안경 측면을 따라 스와이프하는 것도 자연스럽습니다.

  • 그래서 사용자를 대상으로 여러 연구를 진행했고, 실제로 사용자는 가로로 스와이프할 때 콘텐츠가 가로가 아닌 세로로 이동할 것으로 예상한다는 사실을 알게 되었습니다.

  • 머릿속으로 시도해 보면 알 수 있습니다. 여기에서 스와이프할 때 앞에 목록이 있으면 목록을 앞으로 이동하거나 뒤로 이동할 수 있습니다.

  • 2차원 입력을 1차원 방식으로 생각하려고 하는 것입니다.

  • 여기에서 기본 모션 축에 대한 아이디어가 나왔습니다. 가로로 이동하면 세로로 이동합니다.

  • 따라서 기기 자체에서 지정할 수 있습니다.

  • 이건 일반적인 트랙패드가 아니라고 말할 수 있습니다.

  • 기본 방향을 가로로 이동하고 싶고, 이것이 제가 원하는 유일한 기본 축입니다.

  • 이것이 바로 이 기능이 탄생한 배경입니다.

  • 좋습니다.

  • 이 부분은 꽤 만족스럽습니다.

  • Compose가 모든 것을 처리합니다.

  • 이렇게 멋진 세로 구현이 있습니다.

  • 이 점을 생각해 보세요.

  • 매핑 방식은 가로 정의를 사용하면 앞뒤로만 이동할 수 있습니다.

  • 위, 아래, 왼쪽, 오른쪽이 없습니다.

  • 앞으로 가기와 뒤로 가기만 있습니다.

  • 그런 다음, 다음 및 이전 포커스에 익숙하다면 이를 매핑할 수 있습니다.

  • 이제 다음 및 이전으로 이동합니다.

  • 세로 동영상에 적합한 기능입니다.

  • 더 멋지기도 합니다.

  • 가로 모드에서는 기본적으로 작동하죠?

  • 포커스가 지정된 항목이 많기 때문입니다.

  • 다음 또는 이전으로 이동하면 둘 다에서 작동합니다.

  • 이것은 Compose에서 이 모든 것을 정의하고 파악한 Compose의 사례이지만, 기기 자체가 이상한 모양인지 알고 있고 이 기본 축만 원하는지 알고 있기 때문에 기기 자체가 말할 수 있다면 정말 좋을 것입니다. 하지만 이를 정의할 방법이 없습니다.

  • 그래서 이 개념을 다시 플랫폼으로 가져와서 플랫폼에서 이를 정의해야 표준화할 수 있고 기기 자체에서 이를 지정할 수 있으므로 Compose가 이를 완전히 지원할 수 있다고 말해야 했습니다.

  • Compose에서 작업하고 Compose에서 앞으로 나아가고 뒤로 작업하고 함께 가져온 경우가 있습니다.

  • 그래서 Google은 이 작업을 수행했습니다.

  • 이 새로운 API를 사용하면 개발자 또는 기기가 이를 지정할 수 있습니다.

    InputDevice.ViewBehavior
    .getPrimaryDirectionalMotionAxis()
  • 앞서 터치 입력 또는 터치 탐색에 관해 이야기할 때 주로 TV를 위한 것이라고 말씀드렸습니다.

  • Google은 이러한 구현을 중단하고 싶지 않았습니다.

  • 따라서 이 새로운 유형의 유리 입력을 지정하기 위해 다른 필드를 추가했습니다.

  • 좋습니다.

  • 그렇다면 결국 이 모든 것이 여러분에게 어떤 의미가 있을까요?

  • 이는 동작 감지기로 얻을 수 있는 것의 더 구체적인 버전입니다.

    // Glimmer Library
    Box(
        modifier =
            Modifier.fillMaxSize()
                .onIndirectPointerGesture(
                    enabled = true,
                    onSwipeForward = { /* onSwipeForward */ },
                    onSwipeBackward = { /* onSwipeBackward */ },
                    onClick = { /* onClick */ },
                )
                .focusTarget()
    ) {
        // App()
  • Foundation을 사용하는 경우 별도의 설정 없이 동작 감지기와 함께 작동합니다.

  • Google에서 대신 처리해 드렸으므로 별도의 조치를 취하지 않으셔도 됩니다.

  • 하지만 안경용 앱을 만들고 싶다면 Glimmer 라이브러리를 사용하면 됩니다. 특별한 동작 감지기가 있어서, 이를 결합할 필요 없이 이 하나만 사용하면 클릭, 앞으로 프레임, 뒤로를 얻을 수 있습니다.

간접 포인터 입력 테스트

  • 좋습니다.

  • 지금까지 플랫폼과 Compose에서 API가 작동하는 방식을 살펴봤습니다.

  • 테스트는 어떻게 하나요?

  • 터치 테스트를 실행할 때 터치 화면은 다음과 같이 표시됩니다.

    @Test
    clickTest {
    
    rule.onNodeWithTag(defaultTag)
    .performTouchInput
    { click(center) }
    
    }
  • performTouchInput라고 말한 다음 동작을 취하면 됩니다.

  • 다양한 동작이 있으며, 동작을 구현하기 위해 모든 작은 이벤트를 빌드합니다.

  • 개략적으로 어떻게 작동하나요?

  • Compose UI 모듈과 UI 테스트 모듈이 있고, 이 두 모듈은 MotionEvent를 사용하여 적어도 입력 측면에서 통신합니다.

  • 또한 UI 테스트 모듈은 MotionEvent를 기반으로 하는 큐 메커니즘을 사용합니다.

  • 이 이야기를 하는 이유는 무엇일까요?

  • 간접 입력 이벤트와 같은 MotionEvent를 프로그래매틱 방식으로 만들 수 없기 때문입니다. 이는 모듈과 큐 시스템 간의 모든 통신을 다시 작성해야 한다는 의미이므로 저에게는 좋은 일이었습니다.

  • 하지만 해냈습니다.

  • 이것은 무엇을 의미할까요?

  • 터치 입력 테스트에 익숙하다면 간접 입력 테스트는 다음과 같습니다.

  • 이름에 '간접'을 추가하면 됩니다.

    @Test
    clickTest {
    
    rule.onNodeWithTag(defaultTag)
    // .performTouchInput
    .performIndirectPointerInput (
    IndirectPointerEventPrimaryDirectionalMotionAxis.X,
    inputDeviceSize,
    )
    { click(center) }
    }
  • 이제 추가 매개변수를 전달합니다.

  • 첫 번째는 가로를 지정하는 곳입니다.

  • 그러면 입력 크기는 당연히 터치패드의 크기가 됩니다.

  • 이것은 크게 달라지지 않습니다.

  • 소스 코드를 보면 확장 함수를 작성한 것을 확인할 수 있습니다.

    @Test
    clickTest {
    
    rule.onNodeWithTag(defaultTag)
    // .performTouchInput
    .performIndirectPointerInput
    { click(center) }
    
    }
  • 좋습니다.

  • 이러한 과정을 통해 프레임이나 지능형 글라스에서 트랙패드를 지원하기 위해 Google이 수행한 모든 작업을 파악할 수 있습니다.

  • 하지만 시선 및 핸드 트래킹을 위한 연습도 진행하고 있으므로 앞으로 더 많은 기능이 제공될 예정입니다.

  • 그렇다면 제 발표에서 기억해 주셨으면 하는 핵심은 무엇일까요?

요약 및 핵심 내용

  • 따라서 휴대전화나 안경, 기타 입력 장치와 함께 트랙패드를 사용하는 경우를 고려하여 미리 작업을 진행하고 있습니다.

  • 따라서 이러한 Foundation 동작 감지기를 사용하면 바로 작동합니다.

  • 첫 번째입니다.

  • 두 번째로, Compose는 기본 UI 툴킷이며 Google은 플랫폼과 툴킷 모두에서 새로운 입력 유형을 계속 탐색하고 지원할 예정입니다. 제가 여기서 한 작업이 바로 이것입니다.

  • 그리고 API 테스트를 위해 익숙한 표시 영역과 패러다임을 유지하여 이러한 새로운 유형을 최대한 쉽게 이전하고 테스트할 수 있도록 하고자 합니다.

  • 감사합니다. 도움이 되었기를 바랍니다.

코드

코드 스니펫

'트랙패드 업데이트 및 여러 손가락 동작'의 15:47에 나오는 Kotlin 스니펫
onNodeWithContentDescription("Photo")
    .performTrackpadInput {
        click(center)

        pan(Offset(0f, 100f))

        scale(0.9f)
    }
'간접 입력 및 새로운 폼 팩터를 위한 API 설계'의 25:08에 나오는 Kotlin 스니펫
InputDevice.ViewBehavior
.getPrimaryDirectionalMotionAxis()
'간접 입력 및 새로운 폼 팩터를 위한 API 설계'의 25:32에 나오는 Kotlin 스니펫
// Glimmer Library
Box(
    modifier =
        Modifier.fillMaxSize()
            .onIndirectPointerGesture(
                enabled = true,
                onSwipeForward = { /* onSwipeForward */ },
                onSwipeBackward = { /* onSwipeBackward */ },
                onClick = { /* onClick */ },
            )
            .focusTarget()
) {
    // App()
'간접 포인터 입력 테스트'의 26:11에 있는 Kotlin 스니펫
@Test
clickTest {

rule.onNodeWithTag(defaultTag)
.performTouchInput
{ click(center) }

}
'간접 포인터 입력 테스트'의 27:08에 나오는 Kotlin 스니펫
@Test
clickTest {

rule.onNodeWithTag(defaultTag)
// .performTouchInput
.performIndirectPointerInput (
IndirectPointerEventPrimaryDirectionalMotionAxis.X,
inputDeviceSize,
)
{ click(center) }
}
'간접 포인터 입력 테스트'의 27:23에 있는 Kotlin 스니펫
@Test
clickTest {

rule.onNodeWithTag(defaultTag)
// .performTouchInput
.performIndirectPointerInput
{ click(center) }

}