Android - OnTouchListener 로 OnClick, OnLongClick 동시에 구현하기

Sehee Jeong·2021년 10월 10일
5
post-thumbnail

OnClick, OnLongClick, OnTouchListener ...

안드로이드에서는 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달 뒤, 이런 요구사항이 들어왔다.

  1. 버튼에 클릭을 한 경우에는 기존처럼 Toast가 나와야해요.
  2. 버튼을 꾸욱 눌렀을 때는 버튼의 색깔이 바뀌어야 해요.
  3. 꾸욱 누른 상태에서 버튼을 떼면 색이 원래대로 돌아와야해요.

이 경우, LongClickEvent 직후 Action Up 시점을 알아야 한다. 보통 위 요구사항을 구현하기 위해선 OnTouchListener, OnClickListener, OnLongClickListener 세개를 전부 구현한 후 LongClick 시점을 Flag 변수로 담아 Trigger하는 방법이 일반적이지만, 최대한 View Listener 없이 구현할 수 있는 방법을 생각해보았다.


Single, Long Click Event Trigger Util

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)
                }
            )
        }

위 세팅으로 아래와 같은 조건을 구현했다.

  1. 기본 Button 색은 Teal
  2. 클릭 시에는 "Toast!" 라는 토스트 메세지가 나오고,
  3. 롱클릭 시에는 Button의 색이 Purple로 변경
  4. 롱클릭 직후에 버튼을 떼면 Button의 색이 Teal로 변경

ClickLong Click

소스코드는 이 곳에서 볼 수 있다 : 소스코드 보기


6. 회고

  1. 안드로이드에서 제공하는 것(Click, Touch Listener)을 사용하는것이 제일 좋을 것 같다. 내가 구현한 것들이 안드로이드에서 제공하는 기능들로 충분히 대체할 수 있으며, 사이드이펙트가 없다. 또한 LONG_PRESSED_TIME을 직접 설정한다는 것 자체가 인위적인 느낌이다.

  2. 클릭 이벤트를 구현하기 위해 Timer를 사용하는 것은 배보다 배꼽이 큰 것 같다. 🤧

profile
android developer @bucketplace

0개의 댓글