Android 및 ChromeOS는 사용자에게 탁월한 스타일러스 환경을 제공하는 앱을 빌드하는 데 도움이 되는 다양한 API를 제공합니다. MotionEvent
클래스는 스타일러스 압력, 방향, 기울기, 마우스 오버, 손바닥 감지 등 화면과 스타일러스의 상호작용에 관한 정보를 노출합니다. 지연 시간이 짧은 그래픽 및 모션 예측 라이브러리는 스타일러스 화면 렌더링을 개선하여 종이와 펜 같은 자연스러운 환경을 제공합니다.
MotionEvent
MotionEvent
클래스는 화면에 있는 터치 포인터의 위치 및 이동과 같은 사용자 입력 상호작용을 나타냅니다. 스타일러스 입력의 경우 MotionEvent
는 압력, 방향, 기울기, 마우스 오버 데이터도 노출합니다.
이벤트 데이터
MotionEvent
데이터에 액세스하려면 onTouchListener를 설정합니다.
Kotlin
val onTouchListener = View.OnTouchListener { view, event -> // Process motion event. }
Java
View.OnTouchListener listener = (view, event) -> { // Process motion event. };
리스너는 시스템에서 MotionEvent
객체를 수신하므로 앱에서 이러한 객체를 처리할 수 있습니다.
MotionEvent
객체는 UI 이벤트의 다음과 같은 측면과 관련된 데이터를 제공합니다.
- 작업: 기기와의 물리적 상호작용(화면 터치, 포인터를 화면 표면 위로 이동, 포인터를 화면 표면 위로 가져가기)
- 포인터: 화면과 상호작용하는 객체 식별자(손가락, 스타일러스, 마우스)
- 축: 데이터 유형 — x 및 y 좌표, 압력, 기울기, 방향, 마우스 오버 (거리)
작업
스타일러스 지원을 구현하려면 사용자가 실행하는 작업을 이해해야 합니다.
MotionEvent
는 모션 이벤트를 정의하는 다양한 ACTION
상수를 제공합니다. 스타일러스에 가장 중요한 작업은 다음과 같습니다.
작업 | 설명 |
---|---|
ACTION_DOWN ACTION_POINTER_DOWN |
포인터가 화면에 닿습니다. |
ACTION_MOVE | 화면에서 포인터가 움직입니다. |
ACTION_UP ACTION_POINTER_UP |
포인터가 더 이상 화면에 닿지 않습니다. |
ACTION_CANCEL | 이전 또는 현재 모션 세트를 취소해야 하는 경우입니다. |
앱은 ACTION_DOWN
발생 시 새 획 시작, ACTION_MOVE,
로 획 그리기, ACTION_UP
트리거 시 획 완료와 같은 작업을 실행할 수 있습니다.
지정된 포인터의 ACTION_DOWN
에서 ACTION_UP
에 이르는 MotionEvent
작업 세트를 모션 세트라고 합니다.
포인터
대부분의 화면은 멀티 터치입니다. 시스템은 각 손가락, 스타일러스, 마우스 또는 화면과 상호작용하는 기타 포인팅 객체에 포인터를 할당합니다. 포인터 색인을 사용하면 특정 포인터의 축 정보(예: 화면을 터치하는 첫 번째 손가락 또는 두 번째 손가락의 위치)를 가져올 수 있습니다.
포인터 인덱스 범위는 0부터 MotionEvent#pointerCount()
에서 반환된 포인터 수에서 1을 뺀 값까지입니다.
포인터의 축 값은 getAxisValue(axis,
pointerIndex)
메서드로 액세스할 수 있습니다.
포인터 인덱스가 생략되면 시스템은 첫 번째 포인터인 포인터 0의 값을 반환합니다.
MotionEvent
객체에는 사용 중인 포인터 유형에 관한 정보가 포함됩니다. 포인터 인덱스를 반복하고 getToolType(pointerIndex)
메서드를 호출하여 포인터 유형을 가져올 수 있습니다.
포인터에 관한 자세한 내용은 멀티 터치 동작 처리를 참고하세요.
스타일러스 입력
TOOL_TYPE_STYLUS
를 사용하여 스타일러스 입력을 필터링할 수 있습니다.
Kotlin
val isStylus = TOOL_TYPE_STYLUS == event.getToolType(pointerIndex)
Java
boolean isStylus = TOOL_TYPE_STYLUS == event.getToolType(pointerIndex);
또한 스타일러스는 TOOL_TYPE_ERASER
를 사용하여 지우개로 사용된다고 보고할 수 있습니다.
Kotlin
val isEraser = TOOL_TYPE_ERASER == event.getToolType(pointerIndex)
Java
boolean isEraser = TOOL_TYPE_ERASER == event.getToolType(pointerIndex);
스타일러스 축 데이터
ACTION_DOWN
및 ACTION_MOVE
는 스타일러스에 관한 축 데이터(즉, x 및 y 좌표, 압력, 방향, 기울기, 마우스 오버)를 제공합니다.
이 데이터에 액세스할 수 있도록 MotionEvent
API는 getAxisValue(int)
를 제공합니다. 여기서 매개변수는 다음 축 식별자 중 하나입니다.
축 | getAxisValue() 의 반환 값 |
---|---|
AXIS_X |
모션 이벤트의 X 좌표입니다. |
AXIS_Y |
모션 이벤트의 Y 좌표입니다. |
AXIS_PRESSURE |
터치 스크린 또는 터치패드의 경우 손가락, 스타일러스 또는 기타 포인터로 적용된 압력입니다. 마우스나 트랙볼의 경우 기본 버튼을 누르면 1, 누르지 않으면 0입니다. |
AXIS_ORIENTATION |
터치 스크린 또는 터치패드의 경우 기기의 수직면을 기준으로 손가락, 스타일러스 또는 기타 포인터의 방향입니다. |
AXIS_TILT |
라디안으로 표시되는 스타일러스의 기울기 각도입니다. |
AXIS_DISTANCE |
화면과 스타일러스의 거리입니다. |
예를 들어 MotionEvent.getAxisValue(AXIS_X)
는 첫 번째 포인터의 x 좌표를 반환합니다.
멀티 터치 동작 처리도 참고하세요.
위치
다음 호출을 사용하여 포인터의 x 및 y 좌표를 가져올 수 있습니다.
MotionEvent#getAxisValue(AXIS_X)
또는MotionEvent#getX()
MotionEvent#getAxisValue(AXIS_Y)
또는MotionEvent#getY()
압력
MotionEvent#getAxisValue(AXIS_PRESSURE)
또는 첫 번째 포인터의 경우 MotionEvent#getPressure()
를 사용하여 포인터 압력을 가져올 수 있습니다.
터치 스크린 또는 터치패드의 압력 값은 0 (압력 없음)에서 1 사이의 값이지만 화면 보정에 따라 더 높은 값이 반환될 수 있습니다.
방향
방향은 스타일러스가 가리키는 방향을 나타냅니다.
포인터 방향은 getAxisValue(AXIS_ORIENTATION)
또는 getOrientation()
(첫 번째 포인터의 경우)를 사용하여 가져올 수 있습니다.
스타일러스의 경우 방향은 시계 방향 0~파이(π) 또는 시계 반대 방향 0~-파이(π) 사이의 라디안 값으로 반환됩니다.
방향을 사용하면 실제 브러시를 구현할 수 있습니다. 예를 들어 스타일러스가 평면 브러시를 나타내는 경우 평면 브러시의 너비는 스타일러스 방향에 따라 달라집니다.
기울기
기울기는 화면을 기준으로 스타일러스의 경사를 측정합니다.
기울기는 스타일러스의 양의 각도(라디안 단위)를 반환합니다. 여기서 0은 화면에 수직이고 π/2는 표면에 평평합니다.
기울기 각도는 getAxisValue(AXIS_TILT)
(첫 번째 포인터의 단축키 없음)를 사용하여 가져올 수 있습니다.
기울기를 사용하면 기울어진 연필로 음영을 흉내 내는 등 실제 도구에 최대한 가깝게 재현할 수 있습니다.
마우스 오버
화면과 스타일러스의 거리는 getAxisValue(AXIS_DISTANCE)
로 구할 수 있습니다. 이 메서드는 스타일러스가 화면에서 멀어짐에 따라 0.0 (화면에 접촉)에서 더 높은 값으로 값을 반환합니다. 화면과 스타일러스의 촉 (포인트) 사이의 마우스 오버 거리는 화면과 스타일러스 제조업체에 따라 다릅니다. 구현은 다양할 수 있으므로 앱에 중요한 기능의 경우 정확한 값에 의존하지 마세요.
스타일러스 마우스 오버를 사용하여 브러시의 크기를 미리 보거나 버튼이 선택될 것임을 나타낼 수 있습니다.
참고: Compose는 UI 요소의 상호작용 상태에 영향을 주는 수정자를 제공합니다.
hoverable
: 포인터 들어가기 및 나가기 이벤트를 사용하여 마우스 오버가 가능한 구성요소를 구성합니다.indication
: 상호작용이 발생할 때 이 구성요소의 시각적 효과를 그립니다.
손바닥 움직임 무시, 탐색, 원치 않는 입력
멀티 터치 화면에서는 사용자가 필기를 할 때 지지를 위해 자연스럽게 손을 화면에 기대는 등 원치 않는 터치를 등록할 수 있습니다.
손바닥 움직임 무시는 이 동작을 감지하고 마지막 MotionEvent
세트를 취소해야 한다고 알리는 메커니즘입니다.
따라서 원치 않는 터치가 화면에서 삭제되고 적절한 사용자 입력이 다시 렌더링될 수 있도록 사용자 입력 기록을 유지해야 합니다.
ACTION_CANCEL 및 FLAG_CANCELED
ACTION_CANCEL
및 FLAG_CANCELED
는 모두 이전 MotionEvent
세트가 마지막 ACTION_DOWN
에서 취소되어야 함을 알리기 위해 설계되었습니다. 따라서 예를 들어 그리기 앱의 특정 포인터에 관한 마지막 획을 실행취소할 수 있습니다.
ACTION_CANCEL
Android 1.0(API 수준 1)에 추가되었습니다.
ACTION_CANCEL
은 이전 모션 이벤트 세트를 취소해야 함을 나타냅니다.
다음 중 하나라도 감지되면 ACTION_CANCEL
이 트리거됩니다.
- 탐색 동작
- 손바닥 움직임 무시
ACTION_CANCEL
가 트리거되면 getPointerId(getActionIndex())
로 활성 포인터를 식별해야 합니다. 그런 다음 입력 기록에서 해당 포인터로 만든 획을 삭제하고 장면을 다시 렌더링합니다.
FLAG_CANCELED
Android 13(API 수준 33)에 추가되었습니다.
FLAG_CANCELED
는 위로 올라가는 포인터가 의도치 않은 사용자 터치였음을 나타냅니다. 이 플래그는 일반적으로 사용자가 기기를 쥐거나 화면에 손바닥을 놓는 등 실수로 화면을 터치할 때 설정됩니다.
플래그 값에 액세스하는 방법은 다음과 같습니다.
Kotlin
val cancel = (event.flags and FLAG_CANCELED) == FLAG_CANCELED
Java
boolean cancel = (event.getFlags() & FLAG_CANCELED) == FLAG_CANCELED;
플래그가 설정된 경우 이 포인터의 마지막 ACTION_DOWN
에서 마지막 MotionEvent
세트를 실행취소해야 합니다.
ACTION_CANCEL
과 마찬가지로 포인터는 getPointerId(actionIndex)
로 찾을 수 있습니다.
전체 화면, 더 넓은 화면, 탐색 동작
앱이 전체 화면이고 가장자리 근처에 실행 가능한 요소(예: 그리기 또는 메모 앱의 캔버스)가 있는 경우 화면 하단에서 스와이프하여 탐색을 표시하거나 앱을 백그라운드로 이동하면 캔버스에 원치 않는 터치가 발생할 수 있습니다.
동작으로 인해 앱에서 원치 않는 터치가 트리거되지 않도록 하려면 인셋과 ACTION_CANCEL
를 활용하면 됩니다.
손바닥 움직임 무시, 탐색, 원치 않는 입력 섹션도 참고하세요.
WindowInsetsController
의 setSystemBarsBehavior()
메서드 및 BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
를 사용하여 탐색 동작으로 인해 원치 않는 터치 이벤트가 발생하지 않도록 하세요.
Kotlin
// Configure the behavior of the hidden system bars. windowInsetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
Java
// Configure the behavior of the hidden system bars. windowInsetsController.setSystemBarsBehavior( WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE );
인셋 및 동작 관리에 관한 자세한 내용은 다음을 참고하세요.
짧은 지연 시간
지연 시간은 하드웨어, 시스템, 애플리케이션에서 사용자 입력을 처리하고 렌더링하는 데 필요한 시간입니다.
지연 시간 = 하드웨어 및 OS 입력 처리 + 앱 처리 + 시스템 합성
- 하드웨어 렌더링
지연 시간 소스
- 터치스크린 (하드웨어)에 스타일러스 등록: 스타일러스와 OS가 등록 및 동기화되도록 통신할 때 초기 무선 연결입니다.
- 터치 샘플링 레이트(하드웨어): 터치 스크린이 포인터가 표면을 터치하는지 확인하는 초당 횟수입니다(60~1000Hz).
- 입력 처리 (앱): 사용자 입력에 색상, 그래픽 효과, 변환 적용
- 그래픽 렌더링(OS + 하드웨어): 버퍼 전환, 하드웨어 처리
지연 시간이 짧은 그래픽
Jetpack 지연 시간이 짧은 그래픽 라이브러리는 사용자 입력과 화면 렌더링 간의 처리 시간을 줄여줍니다.
라이브러리는 다중 버퍼 렌더링을 피하고 전면 버퍼 렌더링 기술(화면에 직접 쓰기)을 활용하여 처리 시간을 단축합니다.
전면 버퍼 렌더링
전면 버퍼는 화면이 렌더링에 사용하는 메모리입니다. 화면에 직접 그림을 그릴 수 있는 가장 가까운 앱입니다. 지연 시간이 짧은 라이브러리를 사용하면 앱이 전면 버퍼에 직접 렌더링할 수 있습니다. 이렇게 하면 일반적인 다중 버퍼 렌더링이나 이중 버퍼 렌더링 (가장 일반적인 사례)에서 발생할 수 있는 버퍼 전환을 방지하여 성능이 향상됩니다.
전면 버퍼 렌더링은 화면의 작은 영역을 렌더링하는 훌륭한 기법이지만 전체 화면을 새로고침하는 데 사용하도록 설계되지는 않았습니다. 전면 버퍼 렌더링을 사용하면 앱이 디스플레이가 읽고 있는 버퍼로 콘텐츠를 렌더링합니다. 따라서 아티팩트 렌더링이나 테어링 이 발생할 수 있습니다(아래 참고).
지연 시간이 짧은 라이브러리는 Android 10 (API 수준 29) 이상부터 그리고 Android 10 (API 수준 29) 이상을 실행하는 ChromeOS 기기에서 사용할 수 있습니다.
종속 항목
지연 시간이 짧은 라이브러리는 전면 버퍼 렌더링 구현을 위한 구성요소를 제공합니다. 라이브러리는 앱의 모듈 build.gradle
파일에 종속 항목으로 추가됩니다.
dependencies {
implementation "androidx.graphics:graphics-core:1.0.0-alpha03"
}
GLFrontBufferRenderer 콜백
지연 시간이 짧은 라이브러리에는 다음 메서드를 정의하는 GLFrontBufferRenderer.Callback
인터페이스가 포함됩니다.
지연 시간이 짧은 라이브러리는 GLFrontBufferRenderer
와 함께 사용하는 데이터 유형에 관해 독단적이지 않습니다.
그러나 라이브러리는 이 데이터를 수백 개의 데이터 포인트 스트림으로 처리하므로 메모리 사용량과 할당을 최적화하도록 데이터를 설계합니다.
콜백
렌더링 콜백을 사용 설정하려면 GLFrontBufferedRenderer.Callback
를 구현하고 onDrawFrontBufferedLayer()
및 onDrawDoubleBufferedLayer()
를 재정의합니다.
GLFrontBufferedRenderer
는 콜백을 사용하여 가능한 한 가장 최적화된 방식으로 데이터를 렌더링합니다.
Kotlin
val callback = object: GLFrontBufferedRenderer.Callback<DATA_TYPE> { override fun onDrawFrontBufferedLayer( eglManager: EGLManager, bufferInfo: BufferInfo, transform: FloatArray, param: DATA_TYPE ) { // OpenGL for front buffer, short, affecting small area of the screen. } override fun onDrawMultiDoubleBufferedLayer( eglManager: EGLManager, bufferInfo: BufferInfo, transform: FloatArray, params: Collection<DATA_TYPE> ) { // OpenGL full scene rendering. } }
Java
GLFrontBufferedRenderer.Callback<DATA_TYPE> callbacks = new GLFrontBufferedRenderer.Callback<DATA_TYPE>() { @Override public void onDrawFrontBufferedLayer(@NonNull EGLManager eglManager, @NonNull BufferInfo bufferInfo, @NonNull float[] transform, DATA_TYPE data_type) { // OpenGL for front buffer, short, affecting small area of the screen. } @Override public void onDrawDoubleBufferedLayer(@NonNull EGLManager eglManager, @NonNull BufferInfo bufferInfo, @NonNull float[] transform, @NonNull Collection<? extends DATA_TYPE> collection) { // OpenGL full scene rendering. } };
GLFrontBufferedRenderer 인스턴스 선언
앞서 만든 SurfaceView
및 콜백을 제공하여 GLFrontBufferedRenderer
를 준비합니다. GLFrontBufferedRenderer
는 콜백을 사용하여 전면 및 이중 버퍼에 대한 렌더링을 최적화합니다.
Kotlin
var glFrontBufferRenderer = GLFrontBufferedRenderer<DATA_TYPE>(surfaceView, callbacks)
Java
GLFrontBufferedRenderer<DATA_TYPE> glFrontBufferRenderer = new GLFrontBufferedRenderer<DATA_TYPE>(surfaceView, callbacks);
렌더링
전면 버퍼 렌더링은 onDrawFrontBufferedLayer()
콜백을 트리거하는 renderFrontBufferedLayer()
메서드를 호출할 때 시작됩니다.
이중 버퍼 렌더링은 onDrawMultiDoubleBufferedLayer()
콜백을 트리거하는 commit()
함수를 호출할 때 다시 시작됩니다.
다음 예에서는 사용자가 화면에 그리기를 시작하고 (ACTION_DOWN
) 포인터를 이리저리 이동할 때 (ACTION_MOVE
) 프로세스가 전면 버퍼로 렌더링됩니다 (빠른 렌더링). 이 프로세스는 포인터가 화면 표면을 벗어날 때 (ACTION_UP
) 이중 버퍼로 렌더링됩니다.
requestUnbufferedDispatch()
를 사용하여 입력 시스템이 모션 이벤트를 일괄 처리하지 않고 사용 가능한 상태가 되는 즉시 전달하도록 요청할 수 있습니다.
Kotlin
when (motionEvent.action) { MotionEvent.ACTION_DOWN -> { // Deliver input events as soon as they arrive. view.requestUnbufferedDispatch(motionEvent) // Pointer is in contact with the screen. glFrontBufferRenderer.renderFrontBufferedLayer(DATA_TYPE) } MotionEvent.ACTION_MOVE -> { // Pointer is moving. glFrontBufferRenderer.renderFrontBufferedLayer(DATA_TYPE) } MotionEvent.ACTION_UP -> { // Pointer is not in contact in the screen. glFrontBufferRenderer.commit() } MotionEvent.CANCEL -> { // Cancel front buffer; remove last motion set from the screen. glFrontBufferRenderer.cancel() } }
Java
switch (motionEvent.getAction()) { case MotionEvent.ACTION_DOWN: { // Deliver input events as soon as they arrive. surfaceView.requestUnbufferedDispatch(motionEvent); // Pointer is in contact with the screen. glFrontBufferRenderer.renderFrontBufferedLayer(DATA_TYPE); } break; case MotionEvent.ACTION_MOVE: { // Pointer is moving. glFrontBufferRenderer.renderFrontBufferedLayer(DATA_TYPE); } break; case MotionEvent.ACTION_UP: { // Pointer is not in contact in the screen. glFrontBufferRenderer.commit(); } break; case MotionEvent.ACTION_CANCEL: { // Cancel front buffer; remove last motion set from the screen. glFrontBufferRenderer.cancel(); } break; }
렌더링 권장사항 및 금지사항
화면의 작은 부분, 필기, 그리기, 스케치
전체 화면 업데이트, 화면 이동, 확대/축소. 테어링이 발생할 수 있습니다.
테어링
테어링은 화면 버퍼가 수정되는 동시에 화면을 새로고침할 때 발생합니다. 화면의 일부에는 새 데이터가 표시되고 다른 일부에는 이전 데이터가 표시됩니다.
모션 예측
Jetpack 모션 예측 라이브러리는 사용자의 획 경로를 추정하고 렌더기에 임시 인공 지점을 제공하여 체감 지연 시간을 줄입니다.
모션 예측 라이브러리는 실제 사용자 입력을 MotionEvent
객체로 가져옵니다.
객체에는 x 및 y 좌표, 압력, 시간에 관한 정보가 포함되어 있으며, 모션 예측기에서 이를 활용하여 향후 MotionEvent
객체를 예측합니다.
예측된 MotionEvent
객체는 추정치일 뿐입니다. 예측된 이벤트는 인지된 지연 시간을 줄일 수 있지만, 예측된 데이터는 수신되면 실제 MotionEvent
데이터로 대체되어야 합니다.
모션 예측 라이브러리는 Android 4.4 (API 수준 19) 이상부터 그리고 Android 9 (API 수준 28) 이상을 실행하는 ChromeOS 기기에서 사용할 수 있습니다.
종속 항목
모션 예측 라이브러리는 예측 구현을 제공합니다. 라이브러리는 앱의 모듈 build.gradle
파일에 종속 항목으로 추가됩니다.
dependencies {
implementation "androidx.input:input-motionprediction:1.0.0-beta01"
}
구현
모션 예측 라이브러리에는 다음 메서드를 정의하는 MotionEventPredictor
인터페이스가 포함되어 있습니다.
MotionEventPredictor
의 인스턴스를 선언합니다.
Kotlin
var motionEventPredictor = MotionEventPredictor.newInstance(view)
Java
MotionEventPredictor motionEventPredictor = MotionEventPredictor.newInstance(surfaceView);
예측기에 데이터 제공
Kotlin
motionEventPredictor.record(motionEvent)
Java
motionEventPredictor.record(motionEvent);
예측
Kotlin
when (motionEvent.action) { MotionEvent.ACTION_MOVE -> { val predictedMotionEvent = motionEventPredictor?.predict() if(predictedMotionEvent != null) { // use predicted MotionEvent to inject a new artificial point } } }
Java
switch (motionEvent.getAction()) { case MotionEvent.ACTION_MOVE: { MotionEvent predictedMotionEvent = motionEventPredictor.predict(); if(predictedMotionEvent != null) { // use predicted MotionEvent to inject a new artificial point } } break; }
모션 예측의 권장사항 및 금지사항
새로운 예측 포인트가 추가되면 예측 포인트를 삭제합니다.
최종 렌더링에는 예측 포인트를 사용하지 않습니다.
메모 작성 앱
ChromeOS를 사용하면 앱에서 일부 메모 작성 작업을 선언할 수 있습니다.
ChromeOS에서 앱을 메모 작성 앱으로 등록하려면 입력 호환성을 참고하세요.
Android에서 앱을 메모 작성 앱으로 등록하려면 메모 앱 만들기를 참고하세요.
Android 14 (API 수준 34)에서는 앱이 잠금 화면에서 메모 작성 활동을 시작할 수 있는 ACTION_CREATE_NOTE
인텐트를 도입했습니다.
ML Kit를 통한 디지털 잉크 인식
ML Kit 디지털 잉크 인식을 사용하면 앱이 디지털 표면의 필기 텍스트를 수백 개의 언어로 인식할 수 있습니다. 스케치를 분류할 수도 있습니다.
ML Kit는 Ink
객체를 만드는 Ink.Stroke.Builder
클래스를 제공합니다. 이 객체는 필기 입력을 텍스트로 변환하기 위해 머신러닝 모델에서 처리할 수 있습니다.
모델은 필기 인식 외에도 삭제 및 원과 같은 동작을 인식할 수 있습니다.
자세한 내용은 디지털 잉크 인식을 참고하세요.