[삽질] 버튼의 클릭 터치 영역 커스텀 해보기

[삽질] 버튼의 클릭 터치 영역 커스텀 해보기

Sep 4, 2022. | By: pluulove

이번 글에서 언급하는 삽질은 특수 상황에서의 해결 방법을 다룹니다.


샘플 코드 : https://github.com/Pluu/TouchableSample


안드로이드 개발 시에 사용자에게 유효한 터치 영역을 제공하는 것은 UI&UX에서 필수적으로 챙겨야 하는 부분입니다.

터치할 수 있는 크기는 플랫폼마다 다르지만, 모바일 플랫폼에서는 아래처럼 안내하고 있습니다.

  • Android : Material Design Guide / 48x48
  • Apple : Human Interface Guidelines / 44x44

Material Design > Touch targets : https://material.io/design/layout/spacing-methods.html#touch-targets

Human Interface Guidelines > Layout : https://developer.apple.com/design/human-interface-guidelines/foundations/layout/#best-practices

View Component

안드로이드에서는 View를 특정한 위치에 두는 방법으로는 padding/margin/translation 등의 방법을 사용합니다.

Padding을 적용한 ImageButton Margin만 적용한 ImageButton

터치할 수 있는 Component(예: Button/ImageButton)라면, 유효한 터치 영역 지정으로 View에 padding 속성을 지정하는 것이 흔하게 사용되는 방법입니다. 그 외에도 TouchDelegate/HitRect도 존재합니다.

문제는 이것으로 끝나지 않습니다. 회사/프로젝트마다 디자인 가이드에 유효 컴포넌트 영역이 없는 경우도 자주 경험한 사례도 많습니다. 이 경우 개발자 스스로가 View 끼리 위치 조건에 따라 padding과 margin 등을 잘 계산해서 적용합니다.

유효한 터치 영역 만들기 ~ TouchDelegate

먼저 최종 결과물부터 보겠습니다. 클릭 시 좌우 X버튼의 차이를 크게 느끼지 못하실겁니다.

유사하게 보이도록 하기 위해서는 아래와 같이 몇 가지의 작업이 필요합니다.

  • XML 구성
  • TouchDelegate 정의 + Hit 영역 수정
  • Ripple 크기 수정

1) XML

XML에 정의된 좌우 케이스의 다른 점은 padding/margin 사용 여부입니다. 좌측은 Padding을 사용했고, 우측은 터치 영역은 고려하지 않고 위치만 동일하게 맞추기 위해서 margin 속성을 사용했습니다.

<!-- Padding으로 위치+터치 영역 확보 -->
<androidx.cardview.widget.CardView
  ...>
  <androidx.constraintlayout.widget.ConstraintLayout
    ..>
    ...
    <ImageButton
      ...
      android:padding="12dp"
      ... />
  </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>

<!-- Margin만으로 위치 확보 -->
<androidx.cardview.widget.CardView
  ...>
  <androidx.constraintlayout.widget.ConstraintLayout
    ..>
    ...
    <ImageButton
      ...
      android:layout_marginTop="12dp"
      android:layout_marginEnd="12dp"
      ... />
  </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
Padding을 적용한 ImageButton Margin만 적용한 ImageButton

2) HitRect & TouchDelegate 사용

❌ View#getHitRect

뷰에는 Hit 영역을 커스텀하여 반환할 수 있는 View#getHitRect 함수가 존재합니다. 다만, get만 존재하고 set은 존재하지 않습니다. View#getHitRect 함수도 API Level 1부터 존재했던 함수라서, 뷰가 직접 선택 가능 영역을 처리하도록 제공된다는 것을 알 수 있습니다.

자체 커스텀 뷰가 아니라면 유연하게 사용하기 어려우므로 이번 케이스에서는 사용 불가능합니다.

✅ TouchDelegate

Android에는 상위 ViewGroup이 하위 View bounds 밖으로 터치 가능 영역을 확장할 수 있게 하는 TouchDelegate 클래스를 제공합니다. TouchDelegate 클래스는 하위 View의 터치 영역을 확대/축소해야하는 경우에 유용합니다.

parentView.touchDelegate = TouchDelegate(/** Hit size */, /** Child View */)

실제 사용도 하위 View가 아니라 상위 ViewGroup의 touchDelegate 함수를 사용합니다.

이어서 코드를 살펴보겠습니다.

// 샘플
private fun setUpViews() {
  // Expand Hit Size
  val addHitSize = 12.dp2px() // 추가로 Hit하려는 크기 (기존 Padding으로 추가한 사이즈)
  val targetView = binding.changeButton
  val parentView = targetView.parent as View

  parentView.doOnPreDraw { parent ->
    val updateHitRect = Rect().also { r ->
      targetView.getHitRect(r)
      r.inset(-addHitSize, -addHitSize)
    }
    parent.touchDelegate = TouchDelegate(updateHitRect, targetView) // <-- 핵심 코드
  }
}

중요한 것은 하위 View의 HitRect를 가져와서 원하는 크기만큼 변경 후, 상위 ViewGroup의 touchDelegate에 TouchDelegate 클래스의 인스턴스를 설정하면 됩니다. 뷰의 크기를 알아야 하므로 위치/크기를 알 수 있는 시점에서 HitRect를 취득하면 됩니다.

다만, TouchDelegate는 XML로 적용은 되지 않으며 코드에서만 적용할 수 있습니다.

여기까지 작업 시 하위 View 영역 밖에서 클릭하더라도 클릭 효과를 얻을 수 있습니다.

하지만 클릭 시 Ripple 효과가 일어나는 영역이 달라서 어색하게 보입니다.

Padding만 적용 시 클릭 효과 TouchDelegate 적용 시 클릭 효과

3) Ripple 크기 수정

대부분의 동작은 해결했으니, 이제 남은 것은 Ripple 크기입니다. 샘플에서는 단순하게 RippleDrawable#setRadius으로 기본 Riadius보다 더 크게 수정해줍니다.

private fun setUpViews() {
  val targetView = binding.changeButton
  ...
  targetView.ripple(/** Update size */)
}

// Ripple 사이즈 수정
fun ImageButton.ripple(addSize: Int) {
    doOnPreDraw {
        val drawable = background
        if (drawable is RippleDrawable) {
          	// (샘플) 버튼 크기 + 추가 크기 기준으로 반지름 계산
            drawable.radius = (width + addSize * 2) / 2
        }
        background = drawable
    }
}
RippleDrawable 수정 전 RippleDrawable 수정 후 기존 Padding만 적용

지금까지 언급한 내용을 적용하면 Padding과 유사한 모습을 볼 수 있습니다.

결론

HitRect와 TouchDelegate 커스텀은 몇 가지의 제약은 존재하지만 잘 사용한다면 꽤 괜찮은 결과물을 만들어 낼 수 있습니다.

위와 같은 작업을 여러 곳에서 유연하게 적용하기에는 추가로 고려해야 할 사항이 있습니다.

고려 1) 여러 개의 하위 View Hit를 변경하고 싶다면?

ViewGroup에 TouchDelegate를 적용하는 것은 하나만 가능하므로, 여러 View들은 별도 처리가 필요합니다.

Github 샘플에서는 다음 Stackoverflow 해결법을 사용했습니다.

https://stackoverflow.com/a/20051314

고려 2) XML에서 전부 풀고 싶다면?

TouchDelegate는 XML에서 적용할 수 있는 수단은 없으므로, DataBinding으로 해결할 수 있습니다. 또한, 여전히 하위 View 처리 이슈도 남아 있습니다.

  • DataBinding
  • 복수의 하위 View를 XML로 TouchDelegate 처리

참고

  • Extend a child view’s touchable area : https://developer.android.com/develop/ui/views/touch-and-input/gestures/viewgroup#delegate

comments powered by Disqus

Currnte Pages Tags

Android

About

Pluu, Android Developer Blog Site

이전 블로그 링크 :네이버 블로그

Using Theme : SOLID SOLID Github

Social Links