맞춤 뷰의 접근성 높이기

애플리케이션에 맞춤 뷰 구성요소가 필요한 경우 뷰의 접근성을 높여야 합니다. 이 페이지에서 설명하는 대로 다음 단계에 따라 맞춤 뷰의 접근성을 개선할 수 있습니다.

방향 컨트롤러 클릭 처리

대부분의 기기에서 방향 컨트롤러를 사용하여 뷰를 클릭하면 KEYCODE_DPAD_CENTER와 함께 KeyEvent가 현재 포커스된 뷰에 전송됩니다. 모든 표준 Android 뷰는 KEYCODE_DPAD_CENTER를 적절하게 처리합니다. 맞춤 View 컨트롤을 빌드할 때는 이 이벤트에 터치스크린에서 뷰를 터치하는 것과 동일한 효과가 있도록 해야 합니다.

맞춤 컨트롤은 KEYCODE_ENTER 이벤트를 KEYCODE_DPAD_CENTER와 동일하게 처리해야 합니다. 이렇게 하면 사용자가 전체 키보드를 더 쉽게 사용할 수 있습니다.

접근성 API 메서드 구현

접근성 이벤트는 앱의 시각적 인터페이스 구성요소와 사용자 간의 상호작용에 관한 메시지입니다. 이 메시지는 접근성 서비스에서 처리하고, 이 접근성 서비스는 이러한 이벤트의 정보를 사용해 보충 의견과 메시지를 생성합니다. 접근성 메서드는 ViewView.AccessibilityDelegate 클래스의 일부입니다. 이러한 메서드는 다음과 같습니다.

dispatchPopulateAccessibilityEvent()
맞춤 뷰에서 접근성 이벤트를 생성할 때 시스템이 호출하는 메서드입니다. 이 메서드의 기본 구현은 이 뷰에 onPopulateAccessibilityEvent()를 호출한 후 이 뷰의 각 하위 요소에 dispatchPopulateAccessibilityEvent() 메서드를 호출하는 것입니다.
onInitializeAccessibilityEvent()
텍스트 콘텐츠 이상의 뷰 상태에 관한 추가 정보를 가져오기 위해 시스템에서 호출하는 메서드입니다. 맞춤 뷰가 간단한 TextViewButton을 넘어 양방향 컨트롤을 제공하는 경우 이 메서드를 재정의하고 이 메서드를 사용하여 뷰에 관한 추가 정보(예: 비밀번호 입력란 유형, 체크박스 유형, 사용자 상호작용 또는 이벤트에 보내는 의견)를 설정합니다. 이 메서드를 재정의한 경우 메서드의 슈퍼 구현을 호출하고 슈퍼 클래스에서 설정하지 않은 속성만 수정합니다.
onInitializeAccessibilityNodeInfo()
접근성 서비스에 뷰 상태에 관한 정보를 제공하는 메서드입니다. 기본 View 구현에는 일련의 뷰 표준 속성이 포함되어 있지만, 맞춤 뷰에서 TextViewButton 이상의 양방향 컨트롤을 제공하는 경우 이 메서드를 재정의하고 뷰에 관한 추가 정보를 이 메서드에서 처리하는 AccessibilityNodeInfo 객체에 설정하면 됩니다.
onPopulateAccessibilityEvent()
뷰에 AccessibilityEvent의 음성 텍스트 메시지를 설정하는 메서드입니다. 이 메서드는 뷰가 접근성 이벤트를 생성하는 뷰의 하위 뷰인 경우에도 호출됩니다.
onRequestSendAccessibilityEvent()
뷰의 하위 요소가 AccessibilityEvent를 생성할 때 시스템에서 호출하는 메서드입니다. 이 단계를 통해 상위 뷰는 추가 정보를 사용하여 접근성 이벤트를 수정할 수 있습니다. 맞춤 뷰가 하위 뷰를 포함할 수 있고 상위 뷰가 컨텍스트 정보를 접근성 서비스에 유용한 접근성 이벤트에 제공할 수 있는 경우에만 이 메서드를 구현합니다.
sendAccessibilityEvent()
사용자가 뷰에서 작업할 때 시스템에서 호출하는 메서드입니다. 이벤트는 TYPE_VIEW_CLICKED와 같은 사용자 작업 유형으로 분류됩니다. 일반적으로 맞춤 뷰의 콘텐츠가 변경될 때마다 AccessibilityEvent를 전송해야 합니다.
sendAccessibilityEventUnchecked()
기기에 사용 설정되어 있는지 확인하는 접근성 검사(AccessibilityManager.isEnabled())를 호출 코드에서 직접 제어해야 할 때 사용하는 메서드입니다. 이 메서드를 구현하면 시스템 설정과 관계없이 접근성이 사용 설정된 것처럼 호출을 실행하면 됩니다. 일반적으로 맞춤 뷰에는 이 메서드를 구현하지 않아도 됩니다.

접근성을 지원하려면 맞춤 뷰 클래스에서 위에 있는 접근성 메서드를 직접 재정의하고 구현하면 됩니다.

맞춤 뷰 클래스에는 적어도 다음과 같은 접근성 메서드를 구현합니다.

  • dispatchPopulateAccessibilityEvent()
  • onInitializeAccessibilityEvent()
  • onInitializeAccessibilityNodeInfo()
  • onPopulateAccessibilityEvent()

이러한 메서드 구현에 관한 자세한 내용은 접근성 이벤트 채우기 섹션을 참고하세요.

접근성 이벤트 전송

맞춤 뷰의 구체적인 특성에 따라 AccessibilityEvent 객체를 다른 시간에 또는 기본 구현에서 처리하지 않은 이벤트를 위해 전송해야 할 수 있습니다. View 클래스는 다음 이벤트 유형을 위한 기본 구현을 제공합니다.

일반적으로 맞춤 뷰의 콘텐츠가 변경될 때마다 AccessibilityEvent를 전송해야 합니다. 예를 들어 사용자가 왼쪽 또는 오른쪽 화살표 키를 눌러 숫자 값을 선택할 수 있는 맞춤 슬라이더 막대를 구현하는 경우 맞춤 뷰는 슬라이더 값이 변경될 때마다 항상 TYPE_VIEW_TEXT_CHANGED 이벤트를 내보냅니다. 다음 코드 샘플은 sendAccessibilityEvent() 메서드를 사용하여 이 이벤트를 보고하는 방법을 보여줍니다.

Kotlin

override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
    return when(keyCode) {
        KeyEvent.KEYCODE_DPAD_LEFT -> {
            currentValue--
            sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED)
            true
        }
        ...
    }
}

Java

@Override
public boolean onKeyUp (int keyCode, KeyEvent event) {
    if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
        currentValue--;
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
        return true;
    }
    ...
}

접근성 이벤트 채우기

AccessibilityEvent에는 뷰의 현재 상태를 설명하는 필수 속성 집합이 있습니다. 이러한 속성에는 뷰의 클래스 이름, 콘텐츠 설명, 선택 상태 등이 포함됩니다. 각 이벤트 유형에 필요한 특정 속성은 AccessibilityEvent 참조 문서에 설명되어 있습니다.

View 구현은 이 필수 속성의 기본값을 제공합니다. 클래스 이름 및 이벤트 타임스탬프를 비롯하여 이러한 대부분의 값은 자동으로 제공됩니다. 맞춤 뷰 구성요소를 만드는 경우 뷰의 콘텐츠와 특성에 관한 몇 가지 정보를 제공해야 합니다. 이 정보는 버튼 라벨처럼 간단할 수 있으며 이벤트에 추가하려는 추가 상태 정보를 포함할 수 있습니다.

onPopulateAccessibilityEvent()onInitializeAccessibilityEvent() 메서드를 사용하여 AccessibilityEvent에 정보를 채우거나 수정합니다. 특별히 이벤트의 텍스트 콘텐츠를 추가하거나 수정하는 데 onPopulateAccessibilityEvent() 메서드를 사용합니다. 이러한 텍스트 콘텐츠는 TalkBack 기능과 같은 접근성 서비스에 의해 음성 메시지로 변환됩니다. 뷰의 선택 상태와 같은 이벤트 관련 추가 정보를 채우려면 onInitializeAccessibilityEvent() 메서드를 사용합니다.

또한 onInitializeAccessibilityNodeInfo() 메서드를 구현합니다. 접근성 서비스는 이 메서드로 채워진 AccessibilityNodeInfo 객체를 사용하여 접근성 이벤트를 수신한 후에 이 이벤트를 생성하는 뷰 계층 구조를 조사하고 사용자에게 적절한 의견을 제공합니다.

다음 코드 예는 뷰에서 이러한 세 개의 메서드를 재정의하는 방법을 보여줍니다.

Kotlin

override fun onPopulateAccessibilityEvent(event: AccessibilityEvent?) {
    super.onPopulateAccessibilityEvent(event)
    // Call the super implementation to populate its text for the
    // event. Then, add text not present in a super class.
    // You typically only need to add the text for the custom view.
    if (text?.isNotEmpty() == true) {
        event?.text?.add(text)
    }
}

override fun onInitializeAccessibilityEvent(event: AccessibilityEvent?) {
    super.onInitializeAccessibilityEvent(event)
    // Call the super implementation to let super classes
    // set appropriate event properties. Then, add the new checked
    // property that is not supported by a super class.
    event?.isChecked = isChecked()
}

override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo?) {
    super.onInitializeAccessibilityNodeInfo(info)
    // Call the super implementation to let super classes set
    // appropriate info properties. Then, add the checkable and checked
    // properties that are not supported by a super class.
    info?.isCheckable = true
    info?.isChecked = isChecked()
    // You typically only need to add the text for the custom view.
    if (text?.isNotEmpty() == true) {
        info?.text = text
    }
}

Java

@Override
public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
    super.onPopulateAccessibilityEvent(event);
    // Call the super implementation to populate its text for the
    // event. Then, add the text not present in a super class.
    // You typically only need to add the text for the custom view.
    CharSequence text = getText();
    if (!TextUtils.isEmpty(text)) {
        event.getText().add(text);
    }
}

@Override
public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
    super.onInitializeAccessibilityEvent(event);
    // Call the super implementation to let super classes
    // set appropriate event properties. Then, add the new checked
    // property that is not supported by a super class.
    event.setChecked(isChecked());
}

@Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
    super.onInitializeAccessibilityNodeInfo(info);
    // Call the super implementation to let super classes set
    // appropriate info properties. Then, add the checkable and checked
    // properties that are not supported by a super class.
    info.setCheckable(true);
    info.setChecked(isChecked());
    // You typically only need to add the text for the custom view.
    CharSequence text = getText();
    if (!TextUtils.isEmpty(text)) {
        info.setText(text);
    }
}

이러한 메서드는 맞춤 뷰 클래스에 직접 구현할 수 있습니다.

맞춤설정된 접근성 컨텍스트 제공

접근성 서비스는 접근성 이벤트를 생성하는 사용자 인터페이스 구성요소가 포함하는 뷰 계층 구조를 검사할 수 있습니다. 이렇게 하면 접근성 서비스에서 더 풍부한 컨텍스트 정보를 제공하여 사용자에게 도움이 될 수 있습니다.

접근성 서비스가 뷰 계층 구조에서 적절한 정보를 가져오지 못하는 경우도 있습니다. 이와 관련한 예로 캘린더 컨트롤과 같이 두 개 이상의 별도 클릭 가능한 영역이 있는 맞춤 인터페이스 컨트롤이 있습니다. 이 경우 클릭 가능한 하위 섹션은 뷰 계층 구조에 속하지 않으므로 서비스에서 적절한 정보를 얻을 수 없습니다.

그림 1. 선택 가능한 날짜 요소가 있는 맞춤 캘린더 뷰

그림 1의 예에서 전체 캘린더는 단일 뷰로 구현되므로 접근성 서비스는 개발자가 추가 정보를 제공하지 않는 한 뷰 콘텐츠와 뷰 내부에서 사용자가 선택한 사항에 관한 충분한 정보를 받지 못합니다. 예를 들어 사용자가 17로 라벨이 지정된 날짜를 클릭하면 접근성 프레임워크는 전체 캘린더 컨트롤에 관한 설명 정보만 수신합니다. 이 경우 TalkBack 접근성 서비스에서 '캘린더' 또는 '4월 캘린더'라고 알려주지만, 사용자는 어떤 날짜가 선택되었는지 알 수 없습니다.

이와 같은 상황에서 접근성 서비스에 적절한 컨텍스트 정보를 제공하기 위해 접근성 프레임워크는 가상 뷰 계층 구조를 지정하는 방법을 제공합니다. 가상 뷰 계층 구조는 앱 개발자가 접근성 서비스에 화면에 표시되는 정보와 좀 더 근접하게 일치하는 보완적인 뷰 계층 구조를 제공하는 방법입니다. 접근성 서비스는 이러한 접근 방식을 통해 사용자에게 더 유용한 컨텍스트 정보를 제공할 수 있습니다.

가상 뷰 계층 구조가 필요할 수 있는 다른 상황으로는 밀접하게 관련된 함수가 있는 일련의 View 컨트롤을 포함하는 사용자 인터페이스입니다. 여기서 한 컨트롤의 작업은 하나 이상의 요소(예: 별도의 위, 아래 버튼이 있는 숫자 선택 도구)의 콘텐츠에 영향을 미칩니다. 이 경우 한 컨트롤의 작업이 다른 컨트롤의 콘텐츠를 변경하고 이러한 컨트롤 간의 관계가 서비스에 명확하지 않을 수 있으므로 접근성 서비스가 적절한 정보를 얻을 수 없습니다.

이 상황을 처리하려면 포함 뷰로 관련 컨트롤을 그룹화하고 이 컨테이너의 가상 뷰 계층 구조를 제공하여 컨트롤에서 제공하는 정보와 동작을 명확하게 나타냅니다.

뷰의 가상 뷰 계층 구조를 제공하려면 맞춤 뷰 또는 뷰 그룹에서 getAccessibilityNodeProvider() 메서드를 재정의하고 AccessibilityNodeProvider의 구현을 반환합니다. 지원 라이브러리를 ViewCompat.getAccessibilityNodeProvider() 메서드와 함께 사용하여 가상 뷰 계층 구조를 구현하고 AccessibilityNodeProviderCompat을 사용하여 구현을 제공할 수 있습니다.

접근성 서비스에 정보를 제공하고 접근성 포커스를 관리하는 작업을 단순화하려면 ExploreByTouchHelper를 구현하면 됩니다. 이 클래스는 AccessibilityNodeProviderCompat을 제공하고 setAccessibilityDelegate를 호출하여 뷰의 AccessibilityDelegateCompat으로 연결될 수 있습니다. ExploreByTouchHelperActivity 예를 참고하세요. ExploreByTouchHelper는 하위 뷰인 SimpleMonthView를 통해 CalendarView와 같은 프레임워크 위젯에서도 사용합니다.

맞춤 터치 이벤트 처리

다음 예에 설명한 것처럼, 맞춤 뷰 컨트롤에는 비표준 터치 이벤트 동작이 필요할 수도 있습니다.

클릭 기반 작업 정의

위젯에서 OnClickListener 또는 OnLongClickListener 인터페이스를 사용하는 경우 시스템은 자동으로 ACTION_CLICKACTION_LONG_CLICK 작업을 처리합니다. 앱에서 OnTouchListener 인터페이스를 사용하는 더 맞춤설정된 위젯을 사용하는 경우 클릭 기반 접근성 작업의 맞춤 핸들러를 정의합니다. 이렇게 하려면 다음 코드 스니펫과 같이 각 작업에 replaceAccessibilityAction() 메서드를 호출합니다.

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    ...

    // Assumes that the widget is designed to select text when tapped, and selects
    // all text when tapped and held. In its strings.xml file, this app sets
    // "select" to "Select" and "select_all" to "Select all".
    ViewCompat.replaceAccessibilityAction(
        binding.textSelectWidget,
        ACTION_CLICK,
        getString(R.string.select)
    ) { view, commandArguments ->
        selectText()
    }

    ViewCompat.replaceAccessibilityAction(
        binding.textSelectWidget,
        ACTION_LONG_CLICK,
        getString(R.string.select_all)
    ) { view, commandArguments ->
        selectAllText()
    }
}

Java

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...

    // Assumes that the widget is designed to select text when tapped, and select
    // all text when tapped and held. In its strings.xml file, this app sets
    // "select" to "Select" and "select_all" to "Select all".
    ViewCompat.replaceAccessibilityAction(
            binding.textSelectWidget,
            ACTION_CLICK,
            getString(R.string.select),
            (view, commandArguments) -> selectText());

    ViewCompat.replaceAccessibilityAction(
            binding.textSelectWidget,
            ACTION_LONG_CLICK,
            getString(R.string.select_all),
            (view, commandArguments) -> selectAllText());
}

맞춤 클릭 이벤트 만들기

맞춤 컨트롤에서 onTouchEvent(MotionEvent) 리스너 메서드를 사용하여 ACTION_DOWNACTION_UP 이벤트를 감지하고 특수 클릭 이벤트를 트리거할 수 있습니다. 접근성 서비스와의 호환성을 유지하려면 이 맞춤 클릭 이벤트를 처리하는 코드에서 다음을 실행해야 합니다.

  1. 해석된 클릭 작업에 관한 적절한 AccessibilityEvent를 생성합니다.
  2. 접근성 서비스를 사용 설정하여 터치스크린을 사용할 수 없는 사용자를 위한 맞춤 클릭 작업을 실행합니다.

이러한 요구사항을 효율적으로 처리하려면 코드에서 performClick() 메서드를 재정의해야 하며 이 메서드는 메서드의 슈퍼 구현을 호출한 후 클릭 이벤트가 요구하는 작업은 무엇이든지 실행해야 합니다. 맞춤 클릭 작업이 감지되면 코드에서 performClick() 메서드를 호출해야 합니다. 다음 코드 예에 이 패턴이 나와 있습니다.

Kotlin

class CustomTouchView(context: Context) : View(context) {

    var downTouch = false

    override fun onTouchEvent(event: MotionEvent): Boolean {
        super.onTouchEvent(event)

        // Listening for the down and up touch events.
        return when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                downTouch = true
                true
            }

            MotionEvent.ACTION_UP -> if (downTouch) {
                downTouch = false
                performClick() // Call this method to handle the response and
                // enable accessibility services to
                // perform this action for a user who can't
                // tap the touchscreen.
                true
            } else {
                false
            }

            else -> false  // Return false for other touch events.
        }
    }

    override fun performClick(): Boolean {
        // Calls the super implementation, which generates an AccessibilityEvent
        // and calls the onClick() listener on the view, if any.
        super.performClick()

        // Handle the action for the custom click here.

        return true
    }
}

Java

class CustomTouchView extends View {

    public CustomTouchView(Context context) {
        super(context);
    }

    boolean downTouch = false;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);

        // Listening for the down and up touch events
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                downTouch = true;
                return true;

            case MotionEvent.ACTION_UP:
                if (downTouch) {
                    downTouch = false;
                    performClick(); // Call this method to handle the response and
                                    // enable accessibility services to
                                    // perform this action for a user who can't
                                    // tap the touchscreen.
                    return true;
                }
        }
        return false; // Return false for other touch events.
    }

    @Override
    public boolean performClick() {
        // Calls the super implementation, which generates an AccessibilityEvent
        // and calls the onClick() listener on the view, if any.
        super.performClick();

        // Handle the action for the custom click here.

        return true;
    }
}

위의 패턴은 접근성 이벤트를 생성하고 접근성 서비스의 진입점을 제공하여 맞춤 클릭 이벤트를 실행하는 이벤트를 대신하는 performClick() 메서드를 사용하여 맞춤 클릭 이벤트가 접근성 서비스와 호환되도록 합니다.