[Kotlin] 부모 View와 자식 View의 TouchEvent 중첩

park_sujeong·2022년 9월 1일
0

Android

목록 보기
5/13
post-thumbnail

부모 View와 자식 View의 eventListener 중첩은 앱을 개발하는 사람들은 한번쯤은 겪어볼만한 일이다. 한번은 무슨... 꽤 자주 겪는다. 그때마다 구글링하기가 귀찮아서 기록한다.




TouchEvent 동작 원리

아래 해결방법을 적용하기 위해서 이 동작 원리를 알아야한다.

사진 출처 - https://suragch.medium.com/how-touch-events-are-delivered-in-android-eee3b607b038



왼쪽 사진을 먼저 보면 Activity안에 ViewGroup A, ViewGroup A 안에 ViewGroup B, ViewGroup B안에 View가 있다. 이럴 경우 TouchEvent는 Activity -> ViewGroup A -> ViewGroup B -> View 순으로 event가 발생함을 알리고, 다시 거꾸로 View -> ViewGroup B -> ViewGroup A -> Activity 순으로 event가 동작하게 된다.

위의 사진으로 알 수 있듯이, TouchEvent가 발생하면 바로 동작이 되는게 아니고 TouchEvent가 발생하였다고 알리는 dispatchTouchEvent() 메소드가 먼저 작동한다. event가 발생했다는 것을 ViewGroup은 onInterceptTouchEvent()로 이 event를 하위 View(자식 뷰)에게 전달할지, 아닐지를 결정할 수 있다. onInterceptTouchEvent()의 반환값이 true면 ViewGroup은 이 event를 intercept(채가다)해간다. 이 말은 하위 View(자식 View)에게 event를 전달하지 않고 바로 event를 동작한다는 의미다.

이 내용을 이해했다면 아래 해결방법에서의 예시가 이해간다.





해결방법

부모 View(ViewGroup)에 onInterceptTouchEvent()를 오버라이딩하여, 어떤 특정 이벤트가 발생 시 자식 View에게 이 이벤트를 전달할 지 안할지 결정한다.
반환값이 false면 자식 View에게 이벤트를 전달하는 것을 의미한다.


아래 코드는 Android Developer에서 onInterceptTouchEvent의 예시를 보여준다.

onInterceptTouchEvent()

    class MyViewGroup @JvmOverloads constructor(
            context: Context,
            private val mTouchSlop: Int = ViewConfiguration.get(context).scaledTouchSlop
    ) : ViewGroup(context) {

        ...

        override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
            /*
             * This method JUST determines whether we want to intercept the motion.
             * If we return true, onTouchEvent will be called and we do the actual
             * scrolling there.
             */
            return when (ev.actionMasked) {
                // Always handle the case of the touch gesture being complete.
                MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
                    // Release the scroll.
                    mIsScrolling = false
                    false // Do not intercept touch event, let the child handle it
                }
                MotionEvent.ACTION_MOVE -> {
                    if (mIsScrolling) {
                        // We're currently scrolling, so yes, intercept the
                        // touch event!
                        true
                    } else {

                        // If the user has dragged her finger horizontally more than
                        // the touch slop, start the scroll

                        // left as an exercise for the reader
                        val xDiff: Int = calculateDistanceX(ev)

                        // Touch slop should be calculated using ViewConfiguration
                        // constants.
                        if (xDiff > mTouchSlop) {
                            // Start scrolling!
                            mIsScrolling = true
                            true
                        } else {
                            false
                        }
                    }
                }
                ...
                else -> {
                    // In general, we don't want to intercept touch events. They should be
                    // handled by the child view.
                    false
                }
            }
        }

        override fun onTouchEvent(event: MotionEvent): Boolean {
            // Here we actually handle the touch event (e.g. if the action is ACTION_MOVE,
            // scroll this container).
            // This method will only be called if the touch event was intercepted in
            // onInterceptTouchEvent
            ...
        }
    }
    

위의 예시를 난 아래처럼 적용했다.

모든 코드를 볼 필요는 없다. 수정 전,후로 onInterceptTouchEvent()에 추가된 when 구절이 해결방법이다.


CustomMotionLayout.kt(수정 전)

package com.dn.digitalnutrition.customs

import android.content.Context
import android.graphics.Rect
import android.util.AttributeSet
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import androidx.constraintlayout.motion.widget.MotionLayout
import com.dn.digitalnutrition.R

class CustomMotionLayout(context: Context, attributes: AttributeSet? = null):
MotionLayout(context, attributes){


    private var motionTouchStarted = false // 정확한 위치에서만 true
    private val itemContainerView by lazy {
        findViewById<View>(R.id.item_container)
    }

    private val hitRect = Rect()

    init {
        setTransitionListener(object : TransitionListener {
            override fun onTransitionStarted(
                motionLayout: MotionLayout?,
                startId: Int,
                endId: Int
            ) {}

            override fun onTransitionChange(
                motionLayout: MotionLayout?,
                startId: Int,
                endId: Int,
                progress: Float
            ) {}

            override fun onTransitionCompleted(motionLayout: MotionLayout?, currentId: Int) {
                motionTouchStarted = false
            }

            override fun onTransitionTrigger(
                motionLayout: MotionLayout?,
                triggerId: Int,
                positive: Boolean,
                progress: Float
            ) {}
        })
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.actionMasked) {
            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
                motionTouchStarted = false
                return super.onTouchEvent(event)
            }
        }

        if (!motionTouchStarted) {
            itemContainerView.getHitRect(hitRect)
            motionTouchStarted = hitRect.contains(event.x.toInt(), event.y.toInt())
        }

        return super.onTouchEvent(event) && motionTouchStarted
    }

    private val gestureListener by lazy {
        object : GestureDetector.SimpleOnGestureListener() {
            override fun onScroll(
                e1: MotionEvent,
                e2: MotionEvent,
                distanceX: Float,
                distanceY: Float
            ): Boolean {
                itemContainerView.getHitRect(hitRect)
                return hitRect.contains(e1.x.toInt(), e1.y.toInt())
            }
        }
    }

    private val gestureDetector by lazy {
        GestureDetector(context, gestureListener)
    }

    override fun onInterceptTouchEvent(event: MotionEvent?): Boolean {
        return gestureDetector.onTouchEvent(event)
    }

}

CustomMotionLayout.kt(수정 후)

package com.dn.digitalnutrition.customs

import android.content.Context
import android.graphics.Rect
import android.util.AttributeSet
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import androidx.constraintlayout.motion.widget.MotionLayout
import com.dn.digitalnutrition.R

class CustomMotionLayout(context: Context, attributes: AttributeSet? = null):
MotionLayout(context, attributes){


    private var motionTouchStarted = false // 정확한 위치에서만 true
    private val itemContainerView by lazy {
        findViewById<View>(R.id.item_container)
    }

    private val hitRect = Rect()

    init {
        setTransitionListener(object : TransitionListener {
            override fun onTransitionStarted(
                motionLayout: MotionLayout?,
                startId: Int,
                endId: Int
            ) {}

            override fun onTransitionChange(
                motionLayout: MotionLayout?,
                startId: Int,
                endId: Int,
                progress: Float
            ) {}

            override fun onTransitionCompleted(motionLayout: MotionLayout?, currentId: Int) {
                motionTouchStarted = false
            }

            override fun onTransitionTrigger(
                motionLayout: MotionLayout?,
                triggerId: Int,
                positive: Boolean,
                progress: Float
            ) {}
        })
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {

        when (event.actionMasked) {
            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL, -> {
                motionTouchStarted = false
                return super.onTouchEvent(event)
            }
        }

        if (!motionTouchStarted) {
            itemContainerView.getHitRect(hitRect)
            motionTouchStarted = hitRect.contains(event.x.toInt(), event.y.toInt())
        }

        return super.onTouchEvent(event) && motionTouchStarted
    }

    private val gestureListener by lazy {
        object : GestureDetector.SimpleOnGestureListener() {
            override fun onScroll(
                e1: MotionEvent,
                e2: MotionEvent,
                distanceX: Float,
                distanceY: Float
            ): Boolean {
                itemContainerView.getHitRect(hitRect)
                return hitRect.contains(e1.x.toInt(), e1.y.toInt())
            }
        }
    }

    private val gestureDetector by lazy {
        GestureDetector(context, gestureListener)
    }

    override fun onInterceptTouchEvent(event: MotionEvent): Boolean {

        when (event.action) {
            MotionEvent.ACTION_MOVE, MotionEvent.ACTION_SCROLL->
            return false
        }
        return gestureDetector.onTouchEvent(event)
    }

}





참고

profile
Android Developer

0개의 댓글