안드로이드에서는 onClickListener
, onLongClickListener
, onTouchListener
의 구현 유무에 따라 이벤트 발생순서가 달라지게 된다.
1. 특정 뷰에 클릭이벤트만 구현한 상황
binding.button.setOnTouchListener { _, event ->
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
println("ACTION DOWN")
false
}
MotionEvent.ACTION_UP -> {
println("ACTION UP")
false
}
else -> false
}
}
binding.button.setOnClickListener {
println("onClick")
}
👉 Action Down -> Action Up -> OnClick
순으로 동작
2. 특정 뷰에 클릭과 롱클릭이벤트를 동시에 구현한 상황
binding.button.setOnTouchListener { _, event ->
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
println("ACTION DOWN")
false
}
MotionEvent.ACTION_UP -> {
println("ACTION UP")
false
}
else -> false
}
}
binding.button.setOnClickListener {
println("onClick")
}
binding.button.setOnLongClickListener {
println("onLongClick")
false // 직후 click event 를 받기 위해 false 반환
}
👉 Action Down -> OnLongClick -> Action Up -> OnClick
순으로 동작
LongClick이 추가되면 onClick 보다 먼저 호출되기 때문에, 두 개의 클릭 이벤트를 동시에 핸들링해야하는 경우에는 순서를 주의해야한다. 또한, onClick 과 onLongClick 을 동시에 사용해야하는 상황일 때는 onLongClickListener 의 반환값을 true 로 주게주면 해결된다.
하지만 단지 onLongClickListener, onClickListener 를 구현해줄 뿐만 아니라 LongClick(혹은 Click) 직후에 등장하는 Action Up 이벤트를 Trigger 해야하는 경우가 생길 것이다. 예를들어, Activity에 하나의 버튼이 있다고 가정해보자. 나는 이 버튼에 onClickListener를 이용해 Toast를 띄우는 코드를 만들었다. 그런데 1달 뒤, 이런 요구사항이 들어왔다.
- 버튼에 클릭을 한 경우에는 기존처럼 Toast가 나와야해요.
- 버튼을 꾸욱 눌렀을 때는 버튼의 색깔이 바뀌어야 해요.
- 꾸욱 누른 상태에서 버튼을 떼면 색이 원래대로 돌아와야해요.
이 경우, LongClickEvent 직후 Action Up 시점을 알아야 한다. 보통 위 요구사항을 구현하기 위해선 OnTouchListener, OnClickListener, OnLongClickListener 세개를 전부 구현한 후 LongClick 시점을 Flag 변수로 담아 Trigger하는 방법이 일반적이지만, 최대한 View Listener 없이 구현할 수 있는 방법을 생각해보았다.
1. Long Press 시간 설정
companion object {
/** Long Press 판단 기준 시간 */
private const val LONG_PRESSED_TIME = 2L
}
뷰를 얼마나 오래 눌러야 Long Press로 인식할지 시간을 설정한다. 나의 경우, 우선 2초로 설정했다.
2. Timer 설정
private fun startTimer() {
touchTimer = kotlin.concurrent.timer(period = MILLIS_OF_SECOND) {
elapsedSecond++
checkUserLongPressed()
}
}
private fun cancelTimer() {
touchTimer.cancel()
touchUpEventDetector.onNext(Unit)
checkUserSinglePressed()
elapsedSecond = 0
}
사용자가 뷰를 누른 시간을 측정할 수 있도록 타이머 설정. 2초 미만은 Default로 판단하고, 2초이상 누른 경우 LongPress로 판단한다.
3. bindTargetViewEvent
fun bindTargetViewEvent(
view: View,
) {
targetView = view
targetView.setOnTouchListener { _, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
startTimer()
true
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_CANCEL,
-> {
cancelTimer()
true
}
else -> true
}
}
}
TouchEvent를 설정할 targetView 를 주입한다. ACTION_DOWN
시 사용자가 누르고 있는 시간을 측정하기 위해 Timer가 시작되고, ACTION_UP
or ACTION_CANCEL
시 Timer 도 취소된다.
4. clickEventDetector
private fun clickEventDetector(
singleClickListener: () -> Unit,
longClickListener: () -> Unit,
touchUpListener: () -> Unit
) {
initState {
touchUpListener()
}
compositeDisposable += singleClickDetector
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
singleClickListener()
}
compositeDisposable += longClickDetector
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
longClickListener()
}
compositeDisposable += Observable.zip(
longClickDetector,
touchUpEventDetector
) { _, _ -> }
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
touchUpListener()
}
}
singleClick, longClick, touchUp 시 기대되는 값을 서비스 레이어에 셋팅할 수 있도록 구성한다.
5. 결과
ViewTouchEventDetector().apply {
bindTargetViewEvent(binding.button)
setViewClickEvent(
touchUpListener = {
binding.button.setBackgroundResource(R.color.teal_200)
},
singleClickListener = {
Toast.makeText(this@MainActivity, "Toast!", Toast.LENGTH_SHORT).show()
},
longClickListener = {
binding.button.setBackgroundResource(R.color.purple_200)
}
)
}
위 세팅으로 아래와 같은 조건을 구현했다.
Click | Long Click |
---|---|
![]() | ![]() |
소스코드는 이 곳에서 볼 수 있다 : 소스코드 보기
6. 회고
안드로이드에서 제공하는 것(Click, Touch Listener)을 사용하는것이 제일 좋을 것 같다. 내가 구현한 것들이 안드로이드에서 제공하는 기능들로 충분히 대체할 수 있으며, 사이드이펙트가 없다. 또한 LONG_PRESSED_TIME
을 직접 설정한다는 것 자체가 인위적인 느낌이다.
클릭 이벤트를 구현하기 위해 Timer를 사용하는 것은 배보다 배꼽이 큰 것 같다. 🤧