개발일지

Android in A..Z - OnTouchListener, GestureEvent 본문

Android (안드로이드)

Android in A..Z - OnTouchListener, GestureEvent

강태종 2021. 7. 18. 00:18

OnTouchListener

OnTouchListener는 터치가 발생했을 때 이벤트를 수신한다. 터치가 발생했을 때 MotionEvent가 발생하고 MotionEvent를 통해 터치한 손가락의 수, 각 터치별 위치, 손가락이 터치할 때, 손가락을 뗐을 때 등의 이벤트를 수신할 수 있다.

이러한 이벤트를 바탕으로 View를 드래그, 축소/확대, 회전등을 할 수 있다.


Action

MotionEvent의 action에는 비트 마스크 형식으로 Action, Touch Index 등 여러 정보를 저장하고 있다. action과 MotionEvent.ACTION_MASK를 통해 터치의 ACTION을 구할 수 있고, actionMasked를 통해 바로 ACTION을 구할 수 있다.

 

  • ACTION_DOWN : 처음으로 터치했을 경우
  • ACTION_POINTER_DOWN : 추가로 터치한 경우 (여러 손가락)
  • ACTION_UP : 모든 손가락을 뗐을 때
  • ACTION_POINTER_UP : 손가락을 뗐을 때
  • ACTION_MOVE : 터치한 손가락을 움직일 때

https://developer.android.com/reference/android/view/MotionEvent

 

MotionEvent  |  Android 개발자  |  Android Developers

 

developer.android.com


Drag

한 손가락으로 터치를 하고, 움직이는 경우. ACTION_DOWN으로 처음으로 터치했을 경우 MotionEvent를 통해 좌표 정보를 얻고, ACTION_MOVE를 통해 움직이는 경로를 얻을 수 있다.

  1. ACTION_DOWN일 때 mode를 DRAG로 바꾸고, point의 좌표를 설정한다.
  2. ACTION_MOVE일 때 event의 좌표와 point의 좌표를 통해 움직인 거리를 계산한다.
    private fun onActionDown(view: View, event: MotionEvent) {
        mode = Mode.DRAG
        point.set(event.x, event.y)
        onDragStart(view, event)
    }
    
    private fun onActionDrag(view: View, event: MotionEvent) {
        val distanceX = event.x - point.x
        val distanceY = event.y - point.y

        onDrag(view, event, distanceX, distanceY)
    }
    
    private fun onActionPointerUp(view: View, event: MotionEvent) {
        when (mode) {
            Mode.DRAG -> {
                mode = Mode.NONE
                onDragEnd(view, event)
            }
            Mode.SCALE -> {
                mode = Mode.DRAG
                val index = if (event.actionIndex == 0) 1 else 0
                point.set(event.getX(index), event.getY(index))
                onDragStart(view, event)
                onScaleEnd(view, event)
                onRotateEnd(view, event)
            }
            else -> {
                mode = Mode.NONE
            }
        }
    }

    private fun onActionCancel(view: View, event: MotionEvent) {
        if (mode == Mode.DRAG) {
            onDragEnd(view, event)
        } else if (mode == Mode.SCALE) {
            onScaleEnd(view, event)
            onRotateEnd(view, event)
        }

        mode = Mode.NONE
    }

Scale

두 손가락으로 움직이면서 확대/축소하는 경우. ACTION_POINTER_DOWN 했을 때 MotionEvent를 통해 두 점에 대한 정보를 얻고 ACTION_MOVE가 발생할 때 MotionEvent로 정보를 얻어 Scale값을 얻는다.

  1. ACTION_POINTER_DOWN이 발생했을 때 mode를 SCALE로 바꾸고, event를 통해 두 손가락 사이의 거리를 계산하여 distance에 저장한다.
  2. ACTION_POINTER_MOVE가 발생했을 때 event로 두 손가락 사이의 거리를 계산하여 기존의 distance와 새로운 거리를 통해 scale값을 얻는다.
    private fun onActionPointerDown(view: View, event: MotionEvent) {
        mode = Mode.SCALE
        vector.set(event)
        distance = distance(event)

        onDragEnd(view, event)
        onScaleStart(view, event)
        onRotateStart(view, event)
    }
    
    private fun onActionMove(view: View, event: MotionEvent) {
        if (mode == Mode.DRAG) {
            onActionDrag(view, event)
        } else if (mode == Mode.SCALE) {
            onActionScale(view, event)
            onActionRotate(view, event)
        }
    }
    
    private fun onActionScale(view: View, event: MotionEvent) {
        onScale(view, event, distance(event) / distance)
    }

    private fun distance(event: MotionEvent): Float {
        val x = event.getX(0) - event.getX(1)
        val y = event.getY(0) - event.getY(1)

        return sqrt(x*x + y*y)
    }
    
    private fun onActionPointerUp(view: View, event: MotionEvent) {
        when (mode) {
            Mode.DRAG -> {
                mode = Mode.NONE
                onDragEnd(view, event)
            }
            Mode.SCALE -> {
                mode = Mode.DRAG
                val index = if (event.actionIndex == 0) 1 else 0
                point.set(event.getX(index), event.getY(index))
                onDragStart(view, event)
                onScaleEnd(view, event)
                onRotateEnd(view, event)
            }
            else -> {
                mode = Mode.NONE
            }
        }
    }

    private fun onActionCancel(view: View, event: MotionEvent) {
        if (mode == Mode.DRAG) {
            onDragEnd(view, event)
        } else if (mode == Mode.SCALE) {
            onScaleEnd(view, event)
            onRotateEnd(view, event)
        }

        mode = Mode.NONE
    }

Rotate

두 손가락을 움직이면서 회전하는 경우. ACTION_POINTER_DOWN 했을 때 MotionEvent를 통해 Vector값을 얻는다. 그 후 ACTION_POINTER_MOVE가 발생할 때 MotionEvent를 통해 Vector값을 얻고 회전각을 얻는다.

  1. ACTION_POINTER_DOWN이 발생할 때 mode를 SCALE로 바꾸고 Vector값을 vector에 저장한다.
  2. ACTION_MOVE가 발생할 때 새로운 Vector값과 vector를 비교하여 회전각을 얻는다.
    private fun onActionPointerDown(view: View, event: MotionEvent) {
        mode = Mode.SCALE
        vector.set(event)
        distance = distance(event)

        onDragEnd(view, event)
        onScaleStart(view, event)
        onRotateStart(view, event)
    }
    
    private fun onActionMove(view: View, event: MotionEvent) {
        if (mode == Mode.DRAG) {
            onActionDrag(view, event)
        } else if (mode == Mode.SCALE) {
            onActionScale(view, event)
            onActionRotate(view, event)
        }
    }

    private fun onActionRotate(view: View, event: MotionEvent) {
        onRotate(view, event, view.rotation + Vector.getDegree(vector, Vector(event)))
    }
    
    private fun onActionPointerUp(view: View, event: MotionEvent) {
        when (mode) {
            Mode.DRAG -> {
                mode = Mode.NONE
                onDragEnd(view, event)
            }
            Mode.SCALE -> {
                mode = Mode.DRAG
                val index = if (event.actionIndex == 0) 1 else 0
                point.set(event.getX(index), event.getY(index))
                onDragStart(view, event)
                onScaleEnd(view, event)
                onRotateEnd(view, event)
            }
            else -> {
                mode = Mode.NONE
            }
        }
    }

    private fun onActionCancel(view: View, event: MotionEvent) {
        if (mode == Mode.DRAG) {
            onDragEnd(view, event)
        } else if (mode == Mode.SCALE) {
            onScaleEnd(view, event)
            onRotateEnd(view, event)
        }

        mode = Mode.NONE
    }
    
    class Vector(x: Float = 0F, y: Float = 0F) : PointF(x, y) {
        companion object {
            fun getDegree(v1: Vector, v2: Vector): Float {
                return (180.0 / PI * (atan2(v2.y, v2.x) - atan2(v1.y, v1.x))).toFloat()
            }
        }

        constructor(event: MotionEvent) : this() {
            set(event)
        }

        fun set(event: MotionEvent) {
            x = event.getX(1) - event.getX(0)
            y = event.getY(1) - event.getY(0)
            reduction()
        }

        private fun reduction() {
            sqrt(x*x + y*y).also {
                x /= it
                y /= it
            }
        }
    }

코드

open class GestureListener : View.OnTouchListener {
    private val point by lazy { PointF() }
    private val vector by lazy { Vector() }
    private var distance = 0F

    private var mode = Mode.NONE

    override fun onTouch(view: View, event: MotionEvent): Boolean {
        when (event.actionMasked) {
            MotionEvent.ACTION_DOWN -> {
                onActionDown(view, event)
            }
            MotionEvent.ACTION_POINTER_DOWN -> {
                onActionPointerDown(view, event)
            }
            MotionEvent.ACTION_POINTER_UP -> {
                onActionPointerUp(view, event)
            }
            MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
                view.performClick()
                onActionCancel(view, event)
            }
            MotionEvent.ACTION_MOVE -> {
                if (mode == Mode.DRAG || mode == Mode.SCALE) {
                    onActionMove(view, event)
                }
            }
        }

        return true
    }

    private fun onActionDown(view: View, event: MotionEvent) {
        mode = Mode.DRAG
        point.set(event.x, event.y)
        onDragStart(view, event)
    }

    private fun onActionPointerDown(view: View, event: MotionEvent) {
        mode = Mode.SCALE
        vector.set(event)
        distance = distance(event)

        onDragEnd(view, event)
        onScaleStart(view, event)
        onRotateStart(view, event)
    }

    private fun onActionPointerUp(view: View, event: MotionEvent) {
        when (mode) {
            Mode.DRAG -> {
                mode = Mode.NONE
                onDragEnd(view, event)
            }
            Mode.SCALE -> {
                mode = Mode.DRAG
                val index = if (event.actionIndex == 0) 1 else 0
                point.set(event.getX(index), event.getY(index))
                onDragStart(view, event)
                onScaleEnd(view, event)
                onRotateEnd(view, event)
            }
            else -> {
                mode = Mode.NONE
            }
        }
    }

    private fun onActionCancel(view: View, event: MotionEvent) {
        if (mode == Mode.DRAG) {
            onDragEnd(view, event)
        } else if (mode == Mode.SCALE) {
            onScaleEnd(view, event)
            onRotateEnd(view, event)
        }

        mode = Mode.NONE
    }

    private fun onActionMove(view: View, event: MotionEvent) {
        if (mode == Mode.DRAG) {
            onActionDrag(view, event)
        } else if (mode == Mode.SCALE) {
            onActionScale(view, event)
            onActionRotate(view, event)
        }
    }

    private fun onActionDrag(view: View, event: MotionEvent) {
        val distanceX = event.x - point.x
        val distanceY = event.y - point.y

        onDrag(view, event, distanceX, distanceY)
    }

    private fun onActionScale(view: View, event: MotionEvent) {
        onScale(view, event, distance(event) / distance)
    }

    private fun onActionRotate(view: View, event: MotionEvent) {
        onRotate(view, event, view.rotation + Vector.getDegree(vector, Vector(event)))
    }

    private fun distance(event: MotionEvent): Float {
        val x = event.getX(0) - event.getX(1)
        val y = event.getY(0) - event.getY(1)

        return sqrt(x*x + y*y)
    }

    open fun onDrag(view: View, event: MotionEvent, distanceX: Float, distanceY: Float) {
        val array = arrayOf(distanceX, distanceY).toFloatArray()

        view.matrix.mapVectors(array)
        view.translationX += array.first()
        view.translationY += array.last()
    }

    open fun onDragStart(view: View, event: MotionEvent) {

    }

    open fun onDragEnd(view: View, event: MotionEvent) {

    }

    open fun onScaleStart(view: View, event: MotionEvent) {

    }

    open fun onScale(view: View, event: MotionEvent, scale: Float) {
        view.scaleX *= scale
        view.scaleY *= scale
    }

    open fun onScaleEnd(view: View, event: MotionEvent) {

    }

    open fun onRotateStart(view: View, event: MotionEvent) {

    }

    open fun onRotate(view: View, event: MotionEvent, rotation: Float) {
        view.rotation = rotation
    }

    open fun onRotateEnd(view: View, event: MotionEvent) {

    }

    enum class Mode {
        NONE, DRAG, SCALE
    }

    class Vector(x: Float = 0F, y: Float = 0F) : PointF(x, y) {
        companion object {
            fun getDegree(v1: Vector, v2: Vector): Float {
                return (180.0 / PI * (atan2(v2.y, v2.x) - atan2(v1.y, v1.x))).toFloat()
            }
        }

        constructor(event: MotionEvent) : this() {
            set(event)
        }

        fun set(event: MotionEvent) {
            x = event.getX(1) - event.getX(0)
            y = event.getY(1) - event.getY(0)
            reduction()
        }

        private fun reduction() {
            sqrt(x*x + y*y).also {
                x /= it
                y /= it
            }
        }
    }
}

Git

https://github.com/KangTaeJong98/GestureLayout

 

KangTaeJong98/GestureLayout

GestureLayout and GestureListener like Instagram Story. - KangTaeJong98/GestureLayout

github.com

 

'Android (안드로이드)' 카테고리의 다른 글

Android in A..Z - Context  (0) 2021.08.05
Android in A..Z - Constraint Layout  (0) 2021.08.04
Android in A..Z - ActivityResultContract  (0) 2021.05.23
Android in A..Z - DataStore  (0) 2021.03.30
Android in A..Z - Dialog  (0) 2021.03.22
Comments