터치 및 포인터 움직임 추적

Compose 방법 사용해 보기
Jetpack Compose는 Android에 권장되는 UI 도구 키트입니다. Compose에서 터치 및 입력을 사용하는 방법을 알아보세요.

이 과정에서는 터치 이벤트의 움직임을 추적하는 방법을 설명합니다.

현재 터치 접촉 위치, 압력 또는 크기가 변경될 때마다 새 onTouchEvent()ACTION_MOVE 이벤트로 트리거됩니다. 일반 동작 감지에 설명된 대로 이러한 이벤트는 모두 onTouchEvent()MotionEvent 매개변수에 기록됩니다.

손가락 기반 터치가 항상 가장 정확한 상호작용 형식은 아니므로 단순한 접촉보다는 움직임을 기반으로 터치 이벤트가 감지되는 경우가 많습니다. 앱이 움직임 기반 동작 (예: 스와이프)과 움직이지 않는 동작 (예: 한 번 탭)을 구별할 수 있도록 Android에는 터치 슬롭이라는 개념이 포함되어 있습니다. 터치 슬롭은 사용자의 터치 동작이 이동 기반 동작으로 해석되기 전까지 배회할 수 있는 거리(픽셀)를 의미합니다. 이 주제에 관한 자세한 내용은 ViewGroup의 터치 이벤트 관리를 참고하세요.

동작에서 움직임을 추적하는 방법에는 애플리케이션의 요구사항에 따라 여러 가지가 있습니다. 예를 들어 다음과 같습니다.

  • 포인터의 시작 위치와 종료 위치(예: 화면에 표시된 객체를 A 지점에서 B 지점으로 이동)
  • X 및 Y 좌표로 결정되는 포인터가 이동하는 방향입니다.
  • 기록. MotionEvent 메서드 getHistorySize()를 호출하여 동작 기록의 크기를 찾을 수 있습니다. 그런 다음 모션 이벤트의 getHistorical<Value> 메서드를 사용하여 각 기록 이벤트의 위치, 크기, 시간, 압력을 구할 수 있습니다. 기록은 터치 그리기와 같이 사용자 손가락의 흔적을 렌더링할 때 유용합니다. 자세한 내용은 MotionEvent 참조를 확인하세요.
  • 포인터가 터치스크린을 가로질러 이동할 때의 속도입니다.

다음 관련 리소스를 참고하세요.

추적 속도

포인터가 이동하는 거리나 방향을 기반으로 하는 움직임 기반 동작을 사용할 수 있습니다. 그러나 속도는 동작의 특성을 추적하거나 동작이 발생했는지 여부를 결정할 때 종종 결정적인 요소입니다. 속도를 더 쉽게 계산할 수 있도록 Android에서는 VelocityTracker 클래스를 제공합니다. VelocityTracker는 터치 이벤트의 속도를 추적하는 데 도움이 됩니다. 이는 속도가 동작 기준의 일부인 동작(예: 플링)에 유용합니다.

다음은 VelocityTracker API에 있는 메서드의 목적을 보여주는 예입니다.

Kotlin

private const val DEBUG_TAG = "Velocity"

class MainActivity : Activity() {
    private var mVelocityTracker: VelocityTracker? = null

    override fun onTouchEvent(event: MotionEvent): Boolean {

        when (event.actionMasked) {
            MotionEvent.ACTION_DOWN -> {
                // Reset the velocity tracker back to its initial state.
                mVelocityTracker?.clear()
                // If necessary, retrieve a new VelocityTracker object to watch
                // the velocity of a motion.
                mVelocityTracker = mVelocityTracker ?: VelocityTracker.obtain()
                // Add a user's movement to the tracker.
                mVelocityTracker?.addMovement(event)
            }
            MotionEvent.ACTION_MOVE -> {
                mVelocityTracker?.apply {
                    val pointerId: Int = event.getPointerId(event.actionIndex)
                    addMovement(event)
                    // When you want to determine the velocity, call
                    // computeCurrentVelocity(). Then, call getXVelocity() and
                    // getYVelocity() to retrieve the velocity for each pointer
                    // ID.
                    computeCurrentVelocity(1000)
                    // Log velocity of pixels per second. It's best practice to
                    // use VelocityTrackerCompat where possible.
                    Log.d("", "X velocity: ${getXVelocity(pointerId)}")
                    Log.d("", "Y velocity: ${getYVelocity(pointerId)}")
                }
            }
            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
                // Return a VelocityTracker object back to be re-used by others.
                mVelocityTracker?.recycle()
                mVelocityTracker = null
            }
        }
        return true
    }
}

Java

public class MainActivity extends Activity {
    private static final String DEBUG_TAG = "Velocity";
        ...
    private VelocityTracker mVelocityTracker = null;
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int index = event.getActionIndex();
        int action = event.getActionMasked();
        int pointerId = event.getPointerId(index);

        switch(action) {
            case MotionEvent.ACTION_DOWN:
                if(mVelocityTracker == null) {
                    // Retrieve a new VelocityTracker object to watch the
                    // velocity of a motion.
                    mVelocityTracker = VelocityTracker.obtain();
                }
                else {
                    // Reset the velocity tracker back to its initial state.
                    mVelocityTracker.clear();
                }
                // Add a user's movement to the tracker.
                mVelocityTracker.addMovement(event);
                break;
            case MotionEvent.ACTION_MOVE:
                mVelocityTracker.addMovement(event);
                // When you want to determine the velocity, call
                // computeCurrentVelocity(). Then call getXVelocity() and
                // getYVelocity() to retrieve the velocity for each pointer ID.
                mVelocityTracker.computeCurrentVelocity(1000);
                // Log velocity of pixels per second. It's best practice to use
                // VelocityTrackerCompat where possible.
                Log.d("", "X velocity: " + mVelocityTracker.getXVelocity(pointerId));
                Log.d("", "Y velocity: " + mVelocityTracker.getYVelocity(pointerId));
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                // Return a VelocityTracker object back to be re-used by others.
                mVelocityTracker.recycle();
                break;
        }
        return true;
    }
}

포인터 캡처 사용하기

게임, 원격 데스크톱, 가상화 클라이언트와 같은 일부 앱은 마우스 포인터를 제어하는 것이 좋습니다. 포인터 캡처는 Android 8.0 (API 수준 26) 이상에서 사용할 수 있는 기능으로, 모든 마우스 이벤트를 앱의 포커스 뷰로 전달하여 이 컨트롤을 제공합니다.

포인터 캡처 요청하기

앱의 뷰는 뷰가 포함된 뷰 계층 구조에 포커스가 있을 때만 포인터 캡처를 요청할 수 있습니다. 따라서 onClick() 이벤트 중에 또는 활동의 onWindowFocusChanged() 이벤트 핸들러에서와 같이 뷰에 특정 사용자 작업이 있을 때 포인터 캡처를 요청합니다.

포인터 캡처를 요청하려면 뷰에서 requestPointerCapture() 메서드를 호출합니다. 다음 코드 예는 사용자가 뷰를 클릭할 때 포인터 캡처를 요청하는 방법을 보여줍니다.

Kotlin

fun onClick(view: View) {
    view.requestPointerCapture()
}

Java

@Override
public void onClick(View view) {
    view.requestPointerCapture();
}

포인터 캡처 요청이 성공하면 Android는 onPointerCaptureChange(true)를 호출합니다. 캡처를 요청한 뷰와 동일한 뷰 계층 구조에 있는 경우 시스템은 마우스 이벤트를 앱의 포커스가 있는 뷰로 전달합니다. 다른 앱은 ACTION_OUTSIDE 이벤트를 포함하여 캡처가 해제될 때까지 마우스 이벤트 수신을 중지합니다. Android는 마우스 이외의 소스에서 발생한 포인터 이벤트를 정상적으로 전달하지만, 마우스 포인터는 더 이상 표시되지 않습니다.

캡처한 포인터 이벤트 처리하기

뷰가 포인터 캡처를 획득하면 Android는 마우스 이벤트를 전달합니다. 포커스 뷰는 다음 작업 중 하나를 실행하여 이벤트를 처리할 수 있습니다.

다음 코드 예는 onCapturedPointerEvent(MotionEvent)를 구현하는 방법을 보여줍니다.

Kotlin

override fun onCapturedPointerEvent(motionEvent: MotionEvent): Boolean {
    // Get the coordinates required by your app.
    val verticalOffset: Float = motionEvent.y
    // Use the coordinates to update your view and return true if the event is
    // successfully processed.
    return true
}

Java

@Override
public boolean onCapturedPointerEvent(MotionEvent motionEvent) {
  // Get the coordinates required by your app.
  float verticalOffset = motionEvent.getY();
  // Use the coordinates to update your view and return true if the event is
  // successfully processed.
  return true;
}

다음 코드 예는 OnCapturedPointerListener를 등록하는 방법을 보여줍니다.

Kotlin

myView.setOnCapturedPointerListener { view, motionEvent ->
    // Get the coordinates required by your app.
    val horizontalOffset: Float = motionEvent.x
    // Use the coordinates to update your view and return true if the event is
    // successfully processed.
    true
}

Java

myView.setOnCapturedPointerListener(new View.OnCapturedPointerListener() {
  @Override
  public boolean onCapturedPointer (View view, MotionEvent motionEvent) {
    // Get the coordinates required by your app.
    float horizontalOffset = motionEvent.getX();
    // Use the coordinates to update your view and return true if the event is
    // successfully processed.
    return true;
  }
});

맞춤 뷰를 사용하든 리스너를 등록하든 뷰는 트랙볼 기기에서 전달한 좌표와 유사한 상대적 이동(예: X 또는 Y 델타)을 지정하는 포인터 좌표와 함께 MotionEvent를 수신합니다. getX()getY()를 사용하여 좌표를 가져올 수 있습니다.

포인터 캡처 해제하기

앱의 뷰는 다음 코드 예에서와 같이 releasePointerCapture()를 호출하여 포인터 캡처를 해제할 수 있습니다.

Kotlin

override fun onClick(view: View) {
    view.releasePointerCapture()
}

Java

@Override
public void onClick(View view) {
    view.releasePointerCapture();
}

일반적으로 캡처를 요청하는 뷰가 포함된 뷰 계층 구조에서 포커스를 잃기 때문에 시스템은 개발자가 명시적으로 releasePointerCapture()를 호출하지 않아도 캡처를 뷰에서 해제할 수 있습니다.