아티클

안드로이드 앱 접근성 포커스 관리

엔비전스 접근성 2024-11-07 10:24:58

소개

안녕하세요, 엔비전스입니다. 스크린 리더 사용자가 모바일에서 콘텐츠를 탐색할 때의 기본 방식은 한 손가락 오른쪽 혹은 왼쪽 쓸기를 해서 순차적으로 접근성 포커스를 이동하며 탐색을 하는 것입니다. 물론 화면의 레이아웃에 익숙해지고 콘텐츠가 많은 경우에는 순차적으로 탐색을 하는 것이 시간이 오래 걸리기 때문에 화면의 특정 지점을 임의 터치하는 동작을 순차 탐색과 함께 사용하거나 보이스오버나 톡백에서 제공하는 컨트롤, 컨테이너와 같은 특정 요소 단위 이동 기능을 사용하기도 합니다.

그래서 만약 순차 탐색 시의 초점 순서가 화면의 레이아웃과 맞지 않거나 콘텐츠 이동 순서가 논리적이지 못할 경우에는 각 플랫폼의 API를 활용해서 접근성 초점 순서를 재조정하는 방법을 여러 아티클을 통해 설명드렸습니다.

그런데 안드로이드 앱을 개발하다보면 초점이 어떻게 관리되는지 궁금해할 수 있습니다. Screen-reader-focusable 과 같은 초점 합치기 속성을 사용하지 않았음에도 어떤 경우에 초점이 자동으로 하나로 합쳐지는지, 그리고 초점이 그렇게 합쳐졌을 때 텍스트를 읽는 순서가 논리적이지 않을 때는 어떻게 해야 하는지 고민될 수 있습니다.

따라서 1부에서는 레거시 뷰 시스템을 기준으로 초점이 어떻게 관리되는지, 하나의 초점에서 텍스트를 논리적으로 읽지 못할 때 대체 텍스트를 어떻게 주는 것이 좋은지에 대해 설명하고 2부에서는 젯팩 컴포즈를 기준으로 설명하려고 합니다.

기본 포커스 관리

  • 클릭 속성이 없는 레이아웃 하위의 모든 TextView들은 별도의 초점으로 분리됩니다. 초점을 하나로 합치려면 레이아웃에 screen-reader-focusable true와 importantForAccessibility yes 속성을 주면 됩니다.
  • 레이아웃 자체에 클릭 속성이 붙는 순간 하위의 모든 TextView들은 초점이 하나로 합쳐집니다.
  • RecyclerView, GridView의 경우 각 아이템이 기본적으로 클릭 속성이 없더라도 하나의 초점으로 관리됩니다. 초점을 분리하려면 분리하고 싶은 TextView에 focusable true 속성을 주면 됩니다.

포커스가 합쳐진 요소에 대체 텍스트 주기

기본적으로 포커스가 합쳐지면 톡백에서는 하위의 모든 텍스트를 하나로 묶어서 읽어줍니다. 그러나 상황에 따라 읽어주는 순서가 논리적이지 못할 수 있습니다.

날짜 날씨 기온
오늘 맑음 25°C
내일 맑음 27°C

위의 표는 웹에서는 테이블 관련 접근성 속성들이 있어서 표라 하더라도 HTML 마크업만 잘 하면 스크린 리더 사용자도 해당 콘텐츠를 읽고 이해하는데 문제가 없지만 안드로이드에서는 이러한 표와 관련된 접근성 속성이 없기 때문에 텍스트를 다시 묶어 주지 않으면 오늘, 내일, 맑음, 맑음 과 같이 읽어주어 맥락을 파악하기 어렵게 됩니다.

이런 경우에는 무조건 텍스트뷰들을 포함하는 상위 레이아웃에 contentDescription 속성을 주어 이슈를 해결할 수 있습니다. 다만 하위 텍스트뷰들을 톡백에서 논리적으로 잘 읽을 수 있도록 재정렬해 주어야 합니다. 클릭 속성이 붙은 레이아웃의 경우에는 클릭 속성이 있는 곳에 대체 텍스트를 추가하면 됩니다.

하위 뷰들을 원하는대로 정렬하여 대체 텍스트로 추가해 주는 메서드

앞서 소개한 내용에 이어, 하위 뷰들을 원하는 순서로 정렬하여 대체 텍스트로 추가해주는 메서드를 소개해 드리겠습니다. 이 메서드는 앞서 설명한 문제를 해결하고 스크린 리더 사용자에게 더 논리적이고 이해하기 쉬운 정보를 제공하는 데 도움이 될 것입니다.

                
inline fun <reified T : View> setAccessibleContentDescription(
    targetView: Any,
    vararg sourceViews: Any
) {
    val target = when (targetView) {
        is Int -> try {
            findViewById<T>(targetView)
        } catch (e: Exception) {
            null
        }
        is T -> targetView
        else -> null
    } ?: throw IllegalArgumentException("targetView must be a valid Int (resource ID) or a View of type ${T::class.java.simpleName}")

    val sources = sourceViews.mapNotNull { view ->
        when (view) {
            is Int -> try {
                findViewById<View>(view)
            } catch (e: Exception) {
                null
            }
            is View -> view
            else -> null
        }
    }

    if (sources.isEmpty()) {
        throw IllegalArgumentException("No valid source views provided")
    }

    val contentDescription = sources.mapNotNull { view ->
        when (view) {
            is TextView -> view.text?.toString()
            else -> view.contentDescription?.toString()
        }
    }.joinToString(" ").takeIf { it.isNotBlank() }

    contentDescription?.let {
        target.contentDescription = it
    }
}
                
            

이 메서드는 대상 뷰와 소스 뷰들을 받아 논리적인 순서로 대체 텍스트를 구성합니다. 유연성, 안전성, 논리적 순서, 텍스트 추출 등의 특징을 가지고 있어 복잡한 레이아웃이나 데이터 구조를 가진 화면에서도 접근성을 크게 향상시킬 수 있습니다.

예를 들어, 날씨 정보 테이블의 경우 다음과 같이 사용할 수 있습니다:

                
setAccessibleContentDescription<LinearLayout>(
    weatherTableLayout,
    dayTextView1,
    weatherTextView1,
    dayTextView2,
    weatherTextView2
)
                
            

이렇게 하면 "오늘 맑음, 내일 맑음"과 같이 논리적인 순서로 정보가 읽히게 됩니다. 또한, 이 메서드는 RecyclerView나 GridView의 각 아이템에도 적용할 수 있어, 복잡한 레이아웃이나 데이터 구조를 가진 화면에서도 스크린 리더 사용자들이 정보를 더 쉽게 이해하고 앱을 더 효율적으로 사용할 수 있게 됩니다.

© 2024 엔비전스. All rights reserved.

댓글 0
댓글을 작성하려면 해주세요.