당신은 Android 개발자로서 사진에 보이는 UI를 구현해야 합니다. UI 요구사항 중 왼쪽 아이콘을 클릭 액션이 존재하는데 클릭 영역을 뷰 사이즈보다 크게 잡아야 합니다. 어떻게 구현하면 좋을까요?
비교적 연령대가 높은 유저가 사용하는 서비스를 개발하다 보니 위 요구사항을 구현할 일이 종종 있었습니다. 그때마다 빈 레이아웃을 만들어서 해당 레이아웃에 클릭 리스너를 설정하는 방식을 사용했었는데요. 깨끗한 코드를 작성하겠다는 양심을 버려가며 레거시 코드를 마구마구 만드는 기분이었습니다..
그러다 최근 위 요구사항에 알맞게 사용할 수 있는 touchDelegate라는 기능을 알게 되어서 소개해보려고 합니다.
TouchDelegate
TouchDelegate는 뷰가 가지고 있는 경계 너머로까지 클릭 반경을 넓히고 싶을 때 사용하는 클래스입니다. 이름의 Delegate라는 단어에 맞게 클릭 반경을 넓히고자 하는 뷰 경계 너머에 있는 클릭을 탐지하는 대리자가 존재합니다.
심지어 Android 1부터 추가된 기능이라고 하는데요. 이번 기회로 아직 접하지 못한 좋은 기능들이 찾아봐야겠다는 생각이 들었어요.
findViewById<ConstraintLayout>(R.id.layoutChild).setOnClickListener {
Toast.makeText(this@TouchExpandActivity, "자식 클릭", Toast.LENGTH_LONG).show()
}
예제 코드는 Child 뷰를 감싸는 Parent 뷰를 가지고 있습니다. 현재 Child의 뷰 경계 밖에서의 클릭은 탐지하지 못하는 모습을 볼 수 있는데요. 이제 touchDelegate를 이용해 child의 클릭 영역을 키워보겠습니다.
touchDelegate를 사용은 클릭 영역을 키우려는 뷰의 setTouchDelegate 함수만 호출해 주면 됩니다. 해당 함수는 매개변수인 TouchDelefate 타입의 변수를 자신의 property로 설정하는 코드입니다. 해당 property가 어디서 어떻게 쓰이는지 코드를 따라가 보면
View의 onTouchEvent에서 가장 상위 조건으로 사용이 되며 TouchDelegate가 null이 아니면 TouchDelegate의 onTouchEvent로 touch 이벤트를 넘기는 것을 확인할 수 있었습니다. 그리고 함수의 결과가 true이면 그대로 TouchEvent 함수가 종료가 되는데요. 어떤 경우에 true를 반환하는지 또 한 번 내부를 보겠습니다.
TouchDelegate의 onTouchEvent의 코드를 보면 이벤트가 발생한 지점 x, y가 TouchDelegate가 정의하는 Bounds에 포함되는지 확인하고 포함이 되어 있다면 밑에 event.setLocation 함수를 통해 DelegateView에서 발생했다고 재정의하고 있는 것을 확인할 수 있습니다.
따라서 여기서 알 수 있는 점은 Bounds는 확장된 뷰의 영역, DelegateView는 확장하고자 하는 뷰임을 알 수 있습니다. 그럼 해당 변수는 어디서 설정이 되는지 찾아보면
/**
* Constructor
*
* @param bounds Bounds in local coordinates of the containing view that should be mapped to
* the delegate view
* @param delegateView The view that should receive motion events
*/
public TouchDelegate(Rect bounds, View delegateView) {
mBounds = bounds;
mSlop = ViewConfiguration.get(delegateView.getContext()).getScaledTouchSlop();
mSlopBounds = new Rect(bounds);
mSlopBounds.inset(-mSlop, -mSlop);
mDelegateView = delegateView;
}
TouchDelegate의 생성자를 통해 설정이 됨을 최종적으로 확인했습니다. 자! 그러면 touchDelegate 사용을 위한 지식들은 모두 알아본 것 같으니 코드 구현으로 넘어가겠습니다.
코드 구현하기
val childView = findViewById<ConstraintLayout>(R.id.layoutChild).apply {
setOnClickListener {
Toast.makeText(this@TouchExpandActivity, "자식 클릭", Toast.LENGTH_LONG).show()
}
}
val parent = findViewById<ConstraintLayout>(R.id.layoutParent)
parent.post {
val newHitRect = Rect()
childView.getHitRect(newHitRect)
newHitRect.top -= 100 // 100px
newHitRect.left -= 100
newHitRect.right += 100
newHitRect.bottom += 100
parent.touchDelegate = TouchDelegate(newHitRect, childView)
}
getHitRect로 Child 기본 Hit Rect 구하기
childView.getHitRect(newHitRect)
클릭 영역을 확장하기 위해 반드시 함수는 아니지만 기존 뷰 경계에서 일정 px만큼 확장하려고 한다면 기존 뷰의 HitRect를 구하는 것이 구현하기에 편리합니다. 기존 뷰의 HitRect는 getHitRect 함수를 통해 가져올 수 있습니다.
주의할 점으로 코드에서도 이 과정을 post 함수를 통한 Runnable 내에서 진행하고 있는데요. getHitRect 함수 호출 직전에 하위 요소를 배치해서 Child 뷰의 width, height 값을 특정하기 위함입니다.
HiltRect 확장하기
newHitRect.top -= 100 // 100px
newHitRect.left -= 100
newHitRect.right += 100
newHitRect.bottom += 100
Rect의 left, top, right, bottom의 값을 조정해서 touchDelegate의 클릭 영역 기준이 될 Bounds를 설정하는 코드입니다. 주의할 점으로 뷰의 좌표계는 왼쪽 상단이 0,0이기 때문에 left와 top은 확장하려면 음수의 값을, right와 bottom은 양수의 값을 사용해야 합니다.
추가로 저는 예시를 위해 직접 100px를 추가하는 코드를 사용했지만 실제 개발에서는 해상도 대응을 위해 dp에 해당하는 px을 사용하는 것을 고려하면 좋습니다.
TouchDelegate 등록하기
parent.touchDelegate = TouchDelegate(newHitRect, childView)
touchDelegate를 등록하는 마지막 코드입니다. 여기서 주의할 점은 Touch 클릭을 대신 전달해 줄 대리자의 touchDelegate를 설정해야 한다는 점입니다. 값으로 등록되는 TouchDelegate 객체는 확장된 영역을 의미하는 Rect 객체와 클릭 영역을 확장하고자 하는 뷰를 전달합니다.
결과 보기
Child 뷰 외부를 클릭해도 클릭 리스너가 동작하는 것을 확인할 수 있었습니다. 그리고 자세히 보면 확장된 클릭 영역으로 터치했을 때 Child 뷰에 Ripple 효과가 표시된다는 것을 확인할 수 있습니다
주의할 점
TouchDelegate는 기본적으로 View 객체마다 한 개의 TouchDelegate만 등록이 가능합니다. 따라서 복잡한 뷰의 경우 여러 개의 TouchDelegate를 등록하고자 하는 요구사항이 생길 수도 있는데요. stackoverflow 글에 나와있는 방식을 참고하시면 좋을 것 같습니다. TouchDelegate를 상속해서 Collection으로 TouchDelegate를 관리해 여러 개의 TouchDelegate를 사용하는 예제입니다.
예제 코드는 깃허브 저장소에서 확인할 수 있습니다.
'Android > Common' 카테고리의 다른 글
[Android] Notification MessageStyle에 이미지 표시하기 (2) | 2025.01.01 |
---|---|
[Android] InAppUpdate 기능 알아보기 (1) | 2023.11.09 |
[Android] 앱 성능 개선 - 앱 시작 시간 (1) | 2023.10.10 |
[Android] minSdk, compileSdk, targetSdk (0) | 2023.09.25 |
[Android] Radius 올바르게 적용하기 (0) | 2023.05.24 |