Android 공부

ANDROID ACCESSIBILITY 더 잘 사용하기

서론

 안녕하세요. 최근에 저는 신입사원으로 안드로이드 개발을 시작했습니다.
 회사에서는 가장 먼저 진행하게 된 것은 ACCESSIBILITY 작업을 맡아서 진행을 하고 있고, 조금 더 잘 사용하기 위해서 해당 기술들을 정리해서 쉽게 사용할 수 있게 하려고 블로그 글을 작성하고 있습니다.

 ACCESSIBILITY는 시력이 불편하신 분들이 talkback이라는 안드로이드에서 스크린을 읽어주는 스크린리더 기능을 잘 활용할 수 있도록 각종 스크린 값들을 지정하는 것을 말합니다.

0. 백문이불여일도큐먼트

링크

 

앱 접근성 향상 원칙  |  Android 개발자  |  Android Developers

Android 기기를 사용하는 일부 사용자는 여타 사용자와는 다른 접근성 기능을 필요로 합니다. 동일한 접근성 기능을 필요로 하는 특정 사용자 그룹을 지원하기 위해 Android 프레임워크는 개발자가

developer.android.com

Focus 편

 안드로이드의 접근성 작업은 크게 두 가지를 알면, 어느 정도 문제들을 해결할 수 있을 것이라고 생각을 합니다. 그 두 가지는 Focus와 HintMessage입니다. 그 중에 Focus는 talkback을 사용하는 사용자에게 어떤 뷰를 읽어야하는 지, 가르키는 것을 의미합니다.

 Focus의 문제들은 'Focus를 하나로 합쳐주세요.', 'Focus를 나누어 주세요.', 'Focus를 막아주세요.', 'Focus를 이동시켜주세요.', 'Focus를 제거해주세요.'와 같은 작업들이라고 생각을 한다.

Focus-1. Label For는 다른 뷰의 contentDescription을 활용할 수 있다!

label for는 여러 개의 뷰가 있을 때, A의 뷰에서 B의 뷰에 대한 내용이 필요할 때 사용하는 것 입니다.

아래에 대한 코드를 통해서 자세히 알아보도록 하겠습니다.

 <TextView
            android:id="@+id/username_textview"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="2"
            android:gravity="center"
            android:labelFor="@id/username_edittext"
            android:text="새우버거"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

<EditText
            android:id="@+id/username_edittext"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="2"
            android:hint="input username"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

TextView에 labelFor를 등록해서 EditText의 id를 등록하게 되면, TextView에 있는 내용이 EditText로 전달되게 됩니다.

EditText의 input username이 발화 후에, TextView의 user가 발화됩니다.

(input username 입력창) + (새우버거) 가 발화되어 두 가지의 라벨이 이어지게 됩니다.

Focus-2. Focusable을 통해서 focus를 합치거나 나누자!

 여러 개의 talkback 포커스를 하나로 묶기 위해서는 xml 상에서 focusable을 계층 지어 하나로 합쳐주면 이를 해결할 수 있습니다. 말 뜻이 어렵지만, 아래의 코드처럼 컨테이너의 역할을 하는 LinearLayout은 focusable을 true로 하고, 하위 역할을 하는 뷰는 false로 줍니다.  

 <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:focusable="true">

        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:focusable="false"
            android:importantForAccessibility="no"
            android:text="label(O)">

        </TextView>

        <TextView
            android:id="@+id/username_textview"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="2"
            android:focusable="false"
            android:gravity="center"
            android:labelFor="@id/username_edittext"
            android:text="새우버거"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <EditText
            android:id="@+id/username_edittext"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="2"
            android:focusable="false"
            android:hint="input username"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    </LinearLayout>

 

 뷰 자체가 focus를 갖지 않다보니까, 기존의 talkback이 해당 뷰에 접근을 하지 못 하는 현상이 일어난다. 그래서 해당 focus는 컨테이너 뷰가 갖게 되므로, talkback의 스코프가 하나로 합쳐지게 된다. 이를 이용하게 되면, 거꾸로 focus를 잘게잘게 나누는 것까지 가능하기 때문에 focusable을 통해서 focus를 관리할 수 있다는 사실에 명심을 하고 작업을 진행하면 많은 것이 수월하게 풀릴 수 있다.

 

(좌) 컨테이너가 focusable을 별도로 관리하지 않을 때, (우) 컨테이너가 focusable을 별도로 관리 받을 때 

 

Focus-3. Foucs를 막기 위해서는 android:importantForAccessibility를 사용해라!

 작업을 진행하다보면, talkback을 통해서 읽지 말아야하는 뷰들이 있습니다다. 예를 들면, 단순한 이미지 아이콘이 이와 같은 경우라고 생각을 합니다. 이러한 뷰에게 포커스를 막기 위해서는 xml에서 android:importantForAccessibility의 값을 no로 지정시켜 해당 뷰를 읽지 않도록 레이아웃을 제공할 수 있습니다.

 

<TextView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:focusable="false"
        android:importantForAccessibility="no"
        android:text="label(O)">
</TextView>

Focus-4. Focus를 이동 및 제거 시켜주세요.

 안드로이드에서는 화면을 덮는 동작의 레이아웃들이 많습니다. 다이얼로그, 네비게이션드로어, 커스텀 뷰와 같이 다양한 형태의 뷰들이 화면을 덮게 되는데, 이 때 대부분의 talkback은 클릭했던 뷰의 위치에 멈춰 있을 지도 모릅니다. 우리는 이러한 점을 해결하기 위해서 Focus를 이동시키거나 제거하는 법을 배울 것 입니다.

 

class MoveFocusActivity : AppCompatActivity() {
    var isMovable = true
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = ActivityMoveFocusBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.targetButton.setOnClickListener {
            if (isMovable) {
                binding.targetTextview.text = "ACTION_ACCESSIBILITY_FOCUS"
                binding.targetTextview.performAccessibilityAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null)
            } else {
                binding.targetTextview.text = "ACTION_CLEAR_ACCESSIBILITY_FOCUS"
                binding.targetButton.performAccessibilityAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS, null)
            }
            isMovable = !isMovable
        }
    }
}

 

  • performAccessibilityAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null) : 포커스 이동
  • performAccessibilityAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS, null) : 포커스 제거

 

 

Focus-5. Focus를 이동 및 제거 시켜주세요.(2)

 Focus를 이동시키기 위한 두 번째 방법은 뷰를 기준으로 sendAccessibilityEvent를 제공하는 것입니다.

binding.targetButton2.setOnClickListener {
	binding.targetTextview2.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)
}

HintMessage편

Hint-1. 힌트 메시지는 클릭리스너에서 나온다.

 클릭하거나 길게 클릭하는 행위의 뷰에서는 talkback이 자동적으로 '두 번 탭하여 활성하하세요.'와 같은 힌트 메시지를 통해서 해당 뷰가 어떤 행동을 갖고 있는 지 알려줍니다. 하지만, 프로젝트에는 클릭리스너는 갖고 있지만, 어떤 행위를 하고 있지 않은 구조의 뷰가 존재할 수 있습니다. 이럴 때, 우리는 힌트메시지를 컨트롤 해줘야합니다.

 우선, 앞에서 말한 것처럼 힌트 메시지는 클릭 리스너, 롱클릭리스너와 같이 뷰가 갖고 있는 클릭 리스너에서 힌트 메시지가 발화된다는 것을 알아야합니다.

 

//클릭리스너로 힌트메시지 추가
binding.target1Button.setOnClickListener {

}

 

Hint-2. 힌트 메시지를 제거하기 위해서는 removeAction을 활용해라.

 힌트 메시지를 제거하는 첫 번쨰 방법은 'removeAction을 사용하는 것' 입니다.

removeAction은 AccessibilityNodeInfo가 갖고 있는 메소드이고, 이것을 가져올 수 있는 방법은 view의 accessibilityDelegate에서 onInitializeAccessibilityNodeInfo를 오버라이드하게 되면, 해당 뷰와 NodeInfo를 얻을 수 있습니다.

 그리고, removeAction에서 ACTION_CLICK을 제거시켜주게 되면, 힌트 메시지를 제거할 수 있습니다.

binding.target2Button.accessibilityDelegate = object : View.AccessibilityDelegate() {
	override fun onInitializeAccessibilityNodeInfo(
		host: View?,
        info: AccessibilityNodeInfo?
        ) {
        super.onInitializeAccessibilityNodeInfo(host, info)
        info?.removeAction(AccessibilityNodeInfo.ACTION_CLICK)
        host?.isClickable = false
	}
}

 해당 답변이 가장 이상적으로, talkback source에서 왜 'double on tap'이라는 힌트 메시지가 어떤 이유에 의해서 나오는 지에 대한 답변과 함께, 이를 제거하는 좋은 답변 중 하나라고 생각합니다. 

 

How do it disable message "double on tap" in view using talkback accessibility android?

When a view has event click and enable talkback. I need to disable the audio "double to tap" in the view. I am using accessibility in android development. How can I do this, please?

stackoverflow.com

 

Hint-3. 힌트메시지를 제거하고, 순수하게 뷰의 콘텐츠를 얻어오기 위해서는 뷰를 감싸는 레이아웃을 고민해보자.

 버튼에는 2가지 힌트메시지가 있습니다. 첫 번째로는 '~버튼'이라는 키워드가 자동적으로 붙고, 두 번째로는 '실행하려면 두 번 누르세요.'라는 힌트메시지가 나오게 됩니다. 하지만, 우리는 작업을 하다보면 순수하게 뷰의 contentDescription만 얻어오고 싶을 때가 있는데, 이럴 때는 뷰를 감싸는 레이아웃을 배치해서 해당 문제를 해결할 수 있습니다.

버튼을 깜싸고 있는 컨테이너뷰

 이렇게 했을 때, 얻는 다른 이점은 Hint-2에서는 뷰에 removeAction과 isClickable을 막게되는데, 이게 정석적인 방법이 아니기 때문에, 해당 레이아웃을 통해서 힌트 메시지를 얻어오거나 수정하는 문제를 깔끔하게 해결할 수 있습니다.  

<LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <TextView
            android:id="@+id/target_3_textview"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="2"
            android:text="DISABLE double on tap2"
            android:focusable="true"
            android:gravity="center"
            android:importantForAccessibility="no">

        </TextView>

        <LinearLayout
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="3"
            android:focusable="true">
            <Button
                android:id="@+id/target_3_button"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="DISABLE HINT MESSAGE"
                android:importantForAccessibility="no"
                android:backgroundTint="@color/teal_200"
                />
        </LinearLayout>

</LinearLayout>

Hint-4. 힌트 메시지를 수정해서 사용자에게 다양한 상태를 알려줄 수 있다.

 '두 번 탭하여 활성화해주세요.'와 같은 힌트메시지는 수정할 '주문 하려면 두 번 탭하여 활성화해주세요.' , '예약하려면 두 번 탭하여 활성화해주세요.'와 같은 힌트메시지로 수정할 수 있습니다. 수정 하기 위해서는 AccessibilityNodeInfo에서 별도의 action을 추가해주면 됩니다.

 

binding.target4Button.accessibilityDelegate = object : View.AccessibilityDelegate() {
	override fun onInitializeAccessibilityNodeInfo(
    	host: View?,
		info: AccessibilityNodeInfo?
	) {
		super.onInitializeAccessibilityNodeInfo(host, info)
		val orderAction = AccessibilityNodeInfo.AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, "주문")
		info?.addAction(orderAction)
	}
}

 

 

접근성이라는 작업을 하면서 느낀 점

 어떤 안드로이드 컨퍼런스에서 접근성 작업을 열심히 진행하시고, 이에 대해서 발표를 하시는 발표자님의 모습이 눈에 선합니다. 발표를 들으면서 들었던 생각은 '나도 한 번 해보고 싶다'는 생각이 들었던 영역이었습니다. 그 이유는 앱을 통해서 사회적 약자를 위한 기능을 제공할 수 있으며, 최대한 많은 사람들이 편하게 앱을 사용할 수 있게 고민을 할 수 있는 영역이기 때문입니다. 그래서, 개인 프로젝트에서는 다소 중요시 여기지 않았던 부분이 많았고, 저에게는 작업하는 모든 것들이 새로웠던 영역이었습니다.

 

Github 

 빌드 해보시고, 사용해보시길 권장합니다.

 

 

hakzzang/ACCESSIBILITY

Android ACCESSIBILITY . Contribute to hakzzang/ACCESSIBILITY development by creating an account on GitHub.

github.com