일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- AppBarLayout
- sqlite
- Coroutine
- onMeasure
- DataBinding
- ViewModel
- LiveData
- room
- 알림
- Behavior
- 안드로이드
- BOJ
- lifecycle
- notification
- 알고리즘
- hilt
- 코틀린
- CustomView
- kotlin
- recyclerview
- HTTP
- CoordinatorLayout
- Algorithm
- CollapsingToolbarLayout
- activity
- 백준
- onLayout
- Android
- Navigation
- View
- Today
- Total
개발일지
Android in A..Z - OnTouchListener, GestureEvent 본문
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
Drag
한 손가락으로 터치를 하고, 움직이는 경우. ACTION_DOWN으로 처음으로 터치했을 경우 MotionEvent를 통해 좌표 정보를 얻고, ACTION_MOVE를 통해 움직이는 경로를 얻을 수 있다.
- ACTION_DOWN일 때 mode를 DRAG로 바꾸고, point의 좌표를 설정한다.
- 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값을 얻는다.
- ACTION_POINTER_DOWN이 발생했을 때 mode를 SCALE로 바꾸고, event를 통해 두 손가락 사이의 거리를 계산하여 distance에 저장한다.
- 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값을 얻고 회전각을 얻는다.
- ACTION_POINTER_DOWN이 발생할 때 mode를 SCALE로 바꾸고 Vector값을 vector에 저장한다.
- 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
'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 |