반응형

[Snippet]

일정 시간 후에 실행하기

#1 메인 스레드(UI 스레드)인 경우
Handler().postDelayed(new Runnable() {
	@Override
	public void run() {
		//실행할 코드
	}
}, 1000);

 

#2 메인 스레드가 아닌 경우

 

메인 스레드가 아닌 곳에서 사용시 해당 스레드에서 looper가 구현되어 있지 않으면 위의 코드 실행시 아래와 같은 에러를 만나게 됩니다.

01-24 13:06:48.589 E 32489    32729    AndroidRuntime: FATAL EXCEPTION: Thread-4274

01-24 13:06:48.589 E 32489    32729    AndroidRuntime:  Process: kr.happydev.snippettest, PID: 32489
01-24 13:06:48.589 E 32489    32729    AndroidRuntime: java.lang.RuntimeException: Cant create handler inside thread that has not called Looper.prepare()

 

이때는 Handler 객체 생성자의 인수로 looper를 구현해서 넣어주어야 합니다.

또는 메인 스레드의 루퍼를 사용할 경우는 아래와 같이 해주면 됩니다.

new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
    @Override
    public void run() {
        //실행할 코드
    }
}, 1000);

참고로 메인 스레드의 경우 #1 코드의 생성자 인수로 looper를 전달하지 않아도 RuntimeException이 발생하지 않는 것은 메인 스레드의 looper를 따로 생성하지 않아도 이미 존재하기 때문에 문제가 되지 않습니다.

 

 

[Sample]

#1 Back 키를 연속해서 두 번 누를 경우 종료하도록 처리하는 예

private boolean backKeyPressedTwice = false;

@Override
public void onBackPressed() {
    if (backKeyPressedTwice) {
        super.onBackPressed();
        return;
    }

    backKeyPressedTwice = true;
    Toast.makeText(this, "Back 키을 한번 더 누르시면 종료됩니다", Toast.LENGTH_SHORT).show();

    new Handler().postDelayed(new Runnable() {
        @Override
        public void run() {
            backKeyPressedTwice = false;
        }
    }, 2000);
}

 (1) 처음에 Back 키를 누르면  backKeyPressedTwice 변수는 true, "Back 키를 한번 더 누르시면 종료됩니다" 토스트 발생

 (2) 2초 후에 backKeyPressedTwice 변수는 false로 초기화하도록 생성한 Runnable 객체를 핸들러에 전달 

 (3-1) 2초 전에 Back 키를 한번 더 누르면 backKeyPressedTwice 변수는 아직 true이므로 super.onBackPressed()가 호출되면서 종료처리 됨

 (3-2) 2초 후에 Back 키를 한번 더 누르면 (1)과 같이 실행됨

 

 

#2 일정 시간 후에 종료하도록 처리하는 예

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH)) {
        Toast.makeText(getApplicationContext(), "There is no camera flash.\n
                        The app will finish!", Toast.LENGTH_LONG).show();
        delayedFinish();
        return;
    }
    ...
}

private void delayedFinish() {
    new Handler().postDelayed(new Runnable() {
        @Override
        public void run() {
            finish();
        }
    }, 3000);
}

앱 실행후 카메라 플래시 Feature가 존재하지 않는 경우 "There is no camera flash. The app will finish!" 문구의 토스트를 띄우고 토스트가 사라질 때쯤에 종료(finish)하도록 만든 것입니다.

 

위의 예는 아랫글에서 실제 사용하고 있는 코드입니다. 

 

2018/10/15 - 손전등 앱 만들기 #3 - Flashlight API 이용방법 (Flashlight app using Flashlight API)

 

[실행결과]

 

 

 

 

반응형

안녕하세요. Simple& Happy Dev입니다.

 

앱 개발할 때 ANR, App crash, Native crash 등 많은 오류를 만납니다.

 

오류 발생할 때 빠른 로그 확보는 유의미한 오류 발생 직전 로그를 많이 확보할 수 있도록 합니다.

 

이번 글에서는 오류 발생할 때 빠른 버그 리포팅을 위해서 어떤 준비를 하면 좋은지와 이전 오류 정보들을 보관하고 있는 tombstone과 dropbox를 알아보도록 하겠습니다.

 

빠른 버그 리포팅을 위한 준비

 

- 전원 메뉴에 [버그 신고 버튼 추가] 하기

"설정 - 개발자 옵션 - 버그 신고 바로가기"를 활성화하면 전원 버튼을 누르면 메뉴에 버그 리포트 생성할 수 있는 메뉴가 생깁니다.

 

오류 발생할 때 전원버튼을 누르고, "버그 보고서 작성 또는 버그 신고"를 누르면, 버그 리포트가 생성됩니다.

 

 

 

이는 오류 발생할 때 버그 리포트 생성 메뉴 이동 단계를 줄여줍니다.

 


 

- 삼성 단말의 경우 SYSDUMP로 버그 리포트 생성할 때 [NOTIBAR RUN DUMPSTATE: ON] 하기

 

 

오류 발생할 때 알림창을 내려서 "Run DumpState"를 누르면, 버그 리포트가 생성됩니다.

 

 

이것도 오류 발생시 버그 리포트 생성 메뉴 이동 단계를 줄여줍니다.

 


 

- 삼성 단말의 경우 SYSDUMP로 버그 리포트 생성할 때 [Modem Log 제외] 하기

 

 

일반적으로 앱 개발에서는 Modem 로그가 필요하지 않습니다. 불필요한 시간을 줄일 수 있습니다.

 

Sysdump에 관한 추가 설명은 아래 참고하세요.

2018/11/30 - Bugreport 및 Sysdump (Samsung) 사용 방법

 

 

이전 오류 정보가 기록된 tombstone과 dropbox

 

오류가 발생했으나 버그 리포트를 생성하지 않고 넘어간 경우라도 이후에 확인하는 방법이 있습니다.

 

[tombstone]

tombstone의 경우에 주로 native crash 발생할 때 정보(abort message, register, backtrace, stack, memory map)가 저장됩니다.

 

※native crash가 tombstone에 기록되는 과정은 아래 dropbox의 native crash 내용을 참조하시길 바랍니다.

 

#define MAX_TOMBSTONES  10
#define TOMBSTONE_DIR   "/data/tombstones"
#define TOMBSTONE_TEMPLATE (TOMBSTONE_DIR"/tombstone_%02d")

 

tombstones 디렉터리(/data/tombstones)는 eng 또는 userdebug 빌드된 단말에서만 접근이 가능합니다.

tombstones 디렉터리를 열어보면 tombstone_xx 파일들을 확인할 수 있습니다.

기본적으로 10개 파일이 저장되게 되어 있고, 10개 이후는 가장 오래된 파일부터 지워지면서 추가됩니다.

 

아래는 userdebug 빌드된 단말에서 보이는 tombstone 디렉터리입니다. user 빌드된 단말에서는 아래가 보이지 않습니다.

  

 

tombstone_xx 파일을 열면 아래처럼 abort message 및 register, backtrace, stack 등의 정보를 확인할 수 있습니다.

pid: 8671, tid: 8785, name: RenderThread  >>> com.sec.android.app.sbrowser <<<
signal 6 (SIGABRT), code -6 (SI_TKILL), fault addr --------
Abort message: 'Failed to set damage region on surface 0xcefee7e0, error=EGL_BAD_ACCESS'
    r0 00000000  r1 00002251  r2 00000006  r3 00000008
    r4 000021df  r5 00002251  r6 cbdb8f08  r7 0000010c
    r8 cbdb97d0  r9 00003e98  sl cbdb97f0  fp cbdb97e0
    ip 00000000  sp cbdb8ef8  lr f12a44d7  pc f12d580c  cpsr 200f0010
    d0  5f4441425f4c4741  d1  6765722065676143
    d2  0000000000000043  d3  000003e800000045
    d4  0000000000000008  d5  0000000000000000
    d6  00000000d1946020  d7  00000000d1946028
    d8  0000000000000000  d9  0000000000000000
    d10 0000000000000000  d11 0000000000000000
    d12 0000000000000000  d13 0000000000000000
    d14 0000000000000000  d15 0000000000000000
    d16 2e74736973726570  d17 2e6761742e676f6c
    d18 0000000000956d20  d19 00000000005d11e8
    d20 0000000000000000  d21 0000000000000000
    d22 0000000000000000  d23 0000000000000000
    d24 0000005b0000001b  d25 0000005b000000db
    d26 0000000000000000  d27 0000000000000000
    d28 0000000000000000  d29 0000000000000000
    d30 0000000000000000  d31 0000000000000000
    scr 20000013

backtrace:
    #00 pc 0004b80c  /system/lib/libc.so (tgkill+12)
    #01 pc 0001a4d3  /system/lib/libc.so (abort+54)
    #02 pc 00006683  /system/lib/liblog.so (__android_log_assert+154)
    #03 pc 0003a37d  /system/lib/libhwui.so (_ZN7android10uirenderer12renderthread10EglManager11damageFrameERKNS1_5FrameERK6SkRect+204)
    #04 pc 00038405  /system/lib/libhwui.so (_ZN7android10uirenderer12renderthread14OpenGLPipeline4drawERKNS1_5FrameERK6SkRectS8_RKNS0_12FrameBuilder13LightGeometry

EPNS0_16LayerUpdateQueueERKNS0_4RectEbRKNS0_15BakedOpRenderer9LightInfoERKNSt3__16vectorINS_2spINS0_10RenderNodeEEEN

SM_9allocatorISQ_EEEEPNS0_19FrameInfoVisualizerE+32)
    #05 pc 00036dd5  /system/lib/libhwui.so (_ZN7android10uirenderer12renderthread13CanvasContext4drawEv+144)
    #06 pc 00039457  /system/lib/libhwui.so (_ZN7android10uirenderer12renderthread13DrawFrameTask3runEv+138)
    #07 pc 0003e0a3  /system/lib/libhwui.so (_ZN7android10uirenderer12renderthread12RenderThread10threadLoopEv+166)
    #08 pc 0000d479  /system/lib/libutils.so (_ZN7android6Thread11_threadLoopEPv+144)
    #09 pc 000a2c69  /system/lib/libandroid_runtime.so (_ZN7android14AndroidRuntime15javaThreadShellEPv+80)
    #10 pc 00048a89  /system/lib/libc.so (_ZL15__pthread_startPv+24)
    #11 pc 0001b34f  /system/lib/libc.so (__start_thread+32)

stack:
         cbdb8eb8  ffffffdf
         cbdb8ebc  00000000
         cbdb8ec0  cbdb97e0
         cbdb8ec4  76d298d5  /dev/ashmem/dalvik-non moving space_4285_4285 (deleted)
         cbdb8ec8  000021df
         cbdb8ecc  f2ceeaab  /system/bin/app_process32 (sigprocmask+182)
         cbdb8ed0  c60348a0  [anon:libc_malloc]

 

tombstone 추출하는 방법

eng 또는 userdebug 빌드된 단말에서는 adb pull 명령으로 파일을 꺼낼 수 있습니다. (adb pull /data/tombstones)

 

user 빌드된 단말(양산 단말)에서는 "버그 리포트 생성"하면 아래처럼 버그 리포트에 포함돼 나옵니다.

 

[dropbox]

보통 logcat 같은 경우 정해진 크기의 링 버퍼에 로그가 저장되지만, dropbox는 파일로 저장되는 로그입니다.

 

주로 오류나 시스템에 영향을 줄 수 있는 상황을 기록하는 용도로 많이 사용됩니다.

strictmode violation, watchdog, wtf(What a Terrible Failure), netstats_error, lowmem 정보도 기록되지만 여기에서는 ANR과 Crash에 대한 것만 확인하도록 하겠습니다.

 

dropbox 디렉토리(/data/system/dropbox)도 eng 또는 userdebug 빌드된 단말에서만 접근이 가능합니다.

 

아래는 userdebug 빌드된 단말에서 보이는 dropbox 디렉터리입니다. user 빌드된 단말에서는 아래가 보이지 않습니다.

 

ANR

inputsystem에서는 입력 시 5초내 응답이 없을 경우 ANR 트리거 동작해서 AMS(ActivityManagerService)의 inputDispatchingTimeOut을 호출하고, 여기서 다시 AppErrors를 호출해서 ANR 다이얼로그를 화면에 표시하기 전에 Process 정보, ANR reason, 각 프로세스의 CPU 사용량, stack trace 등은 dropbox에 기록(addErrorToDropBox)하고 일부 정보는 로그로 출력합니다.

 

AppErrors.java

 

App Crash

Exception이 Throw 된 후 catch 되지 않으면 Runtime에서 uncaught exception으로 처리합니다.

uncaughtException()에서 AMS의 handleApplicationCrash() -> handleApplicationCrashInner() 이벤트 로그에 Crash 정보 출력 후 dropbox에 기록(addErrorToDropBox) 후 해당 앱을 강제 종료 처리합니다.

 

RuntimeInit.java

 

ActivityManagerService.java

 

 

Native crash

Native crash가 발생하면 커널은 해당 시그널을 보내고, linker 모듈의 debugger(클라이언트)에서 시그널(SIGABRT, SIGSEGV, SIGFPE 등)를 잡아서 debuggerd(데몬 서버)로 DEBUGGER_ACTION_CRASH 메시지를 보냅니다.

 

데몬 서버인 debuggerd에서는 DEBUGGER_ACTION_CRASH 메시지를 받아서 Native에서 보낸 정보를 읽어오고, AMS(ActivityManagerService) NativeCrashListener에 소켓 연결을 설정하고 tombstone를 호출해서 tombstone에 native crash에 대해서 덤프하도록 요청합니다. 이후 dump thread에 의해서 tombstone에 backtrace 외 정보들이 저장됩니다.

 

한편, debuggerd와 소켓 연결된 NativeCrashListener는 스레드를 상속받은 클래스인데, 애초부터 SystemServer에서 AMS로 NativeCrashListener의 인스턴스를 생성해서 스레드로 동작중인 상태였으며, debuggerd에서는 tombstone에 dump를 요청한 이후 NativeCrashListener에도 시그널을 전달해서 아래와 같이 dropbox에 기록하도록 요청합니다.

 

NativeCrashListener.java

 

ActivityManagerService.java

 

dropbox 추출하는 방법

eng 또는 userdebug 빌드된 단말에서는 adb pull 명령으로 파일을 꺼낼 수 있습니다. (adb pull /data/system/dropbox)

 

user 빌드된 단말에서는 tombstone에서 "버그 리포트 생성"을 해서 꺼낸 것처럼 dropbox를 꺼낼 수 없습니다.

단, 삼성 단말의 경우 "Sysdump로 버그 리포트 생성"하면 dropbox 디렉터리에 있는 파일들을 압축해서 내보내줍니다.

 

직전의 오류의 경우에 대해서는 아래 명령으로 꺼낼 수는 있습니다.

 

ANR

adb shell dumpsys dropbox --print system_app_anr > system_app_anr.txt

adb shell dumpsys dropbox --print data_app_anr > data_app_anr.txt

 

APP Crash

adb shell dumpsys dropbox --print system_app_crash > system_app_crash.txt 

adb shell dumpsys dropbox --print data_app_crash > data_app_crash.txt 

 

Native Crash

adb shell dumpsys dropbox --print system_app_native_crash > system_app_native_crash.txt 

adb shell dumpsys dropbox --print data_app_native_crash > data_app_native_crash.txt

 

여기까지입니다. 읽어주셔서 감사합니다.

반응형

안녕하세요. Simple& Happy Dev입니다.

 

커스텀 리스트뷰(Custom ListView)를 가지고 "접근성 검사기를 이용한 접근성 개선 예제"를 만드는 과정에서 리스트뷰의 아이템를 클릭했지만 동작하지 않는 이슈가 있었습니다.

 

2018/12/20 - 접근성 검사기(Accessibility Scanner)를 이용한 접근성 개선 예제

 

아래와 같이 ImageView + TextView + Switch 버튼으로 이루어진 아이템을 가진 커스텀 리스트뷰입니다.

 

아이템 클릭 시 리스너 코드는 아래와 같습니다.

ListView colorListView = findViewById(R.id.lvColor);
colorListView.setAdapter(mColorListAdapter);
colorListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
ColorItem selectedItem = (ColorItem) mColorListAdapter.getItem(position);
AlertDialog.Builder builder = new AlertDialog.Builder(new ContextThemeWrapper(MainActivity.this, R.style.AlertDialogCustom));
AlertDialog alert = builder.setMessage(selectedItem.getColorName() + getResources().getString(R.string.dialog_msg) + (selectedItem.getColorUse() ? getResources().getString(R.string.yes) : getResources().getString(R.string.no)))
.setPositiveButton(getResources().getString(android.R.string.ok), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int whichButton) {
dialog.cancel();
}
}).create();
alert.show();
}
});

그런데, 아이템 클릭 시 동작을 하지 않아서 디버깅해보니 onItemClick()이 호출되지 않았습니다.

 

몇 가지 테스트를 해보니, 일단 아이템을 TextView 하나로만 구성한 커스텀 리스트뷰에서는 정상 동작합니다.

그 경우에 onItemClick이 호출될 때의 콜스택은 아래와 같이 나옵니다.

 

"main"@10,638 in group "main": RUNNING
  • onItemClick:40, MainActivity$1 {kr.happydev.accessibilitytest}
  • performItemClick:318, AdapterView {android.widget}
  • performItemClick:1159, AbsListView {android.widget}
  • run:3136, AbsListView$PerformClick {android.widget}
  • onTouchUp:4064, AbsListView {android.widget}
  • onTouchEvent:3822, AbsListView {android.widget}
  • dispatchTouchEvent:12513, View {android.view}
  •  

    ImageView + TextView 2개로 구성한 커스텀 리스트뷰에서도 정상 동작하였습니다.

     

    Switch 버튼이 문제인 것 같은데, 프레임워크 레벨에서 왜 호출되지 않는지 확인이 필요했습니다.

     

    다시 ImageView + TextView + Switch 버튼으로 이루어진 아이템으로 소스를 돌려서 확인한 결과 일단 onTouchUp 까지는 정상적으로 호출됩니다.

     

    그런데, onTouchUp 안에서 performClick.run() 이 호출되지 않았습니다.

     

    /android/widget/AbsListView.java

    private void onTouchUp(MotionEvent ev) {
    switch (mTouchMode) {
    case TOUCH_MODE_DOWN:
    case TOUCH_MODE_TAP:
    case TOUCH_MODE_DONE_WAITING:
    final int motionPosition = mMotionPosition;
    final View child = getChildAt(motionPosition - mFirstPosition);
    if (child != null) {
    if (mTouchMode != TOUCH_MODE_DOWN) {
    child.setPressed(false);
    }

    final float x = ev.getX();
    final boolean inList = x > mListPadding.left && x < getWidth() - mListPadding.right;
    if (inList && !child.hasExplicitFocusable()) {
    if (mPerformClick == null) {
    mPerformClick = new PerformClick();
    }

    final AbsListView.PerformClick performClick = mPerformClick;
    performClick.mClickMotionPosition = motionPosition;
    performClick.rememberWindowAttachCount();

    mResurrectToPosition = motionPosition;

    if (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP) {
    removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ?
    mPendingCheckForTap : mPendingCheckForLongPress);
    mLayoutMode = LAYOUT_NORMAL;
    if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
    mTouchMode = TOUCH_MODE_TAP;
    setSelectedPositionInt(mMotionPosition);
    layoutChildren();
    child.setPressed(true);
    positionSelector(mMotionPosition, child);
    setPressed(true);
    if (mSelector != null) {
    Drawable d = mSelector.getCurrent();
    if (d != null && d instanceof TransitionDrawable) {
    ((TransitionDrawable) d).resetTransition();
    }
    mSelector.setHotspot(x, ev.getY());
    }
    if (mTouchModeReset != null) {
    removeCallbacks(mTouchModeReset);
    }
    mTouchModeReset = new Runnable() {
    @Override
    public void run() {
    mTouchModeReset = null;
    mTouchMode = TOUCH_MODE_REST;
    child.setPressed(false);
    setPressed(false);
    if (!mDataChanged && !mIsDetaching && isAttachedToWindow()) {
    performClick.run();
    }
    }
    };
    postDelayed(mTouchModeReset,
    ViewConfiguration.getPressedStateDuration());
    } else {
    mTouchMode = TOUCH_MODE_REST;
    updateSelectorState();
    }
    return;
    } else if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
    performClick.run();
    }
    }
    }
    mTouchMode = TOUCH_MODE_REST;
    updateSelectorState();
    break;

     

    조금 더 디버깅해보니 아래 조건이 맞지 않아서 그런 것이었습니다.

    if (inList && !child.hasExplicitFocusable()) {

    아래 보면 inList는 true이지만, !child.hasExplicitFocusable()이 false라서 동작하지 않게 된 것입니다.

    즉, Switch 버튼으로 인하여 child.hasExpliciFocusable()이 true로 리턴이 되고 있어서 발생한 문제입니다.

     

    이는 커스텀 리스트뷰에서 아이템을 클릭하더라도 이미 switch 버튼이 포커스를 가지고 있기 때문에 동작하지 않았다고 이해하면 됩니다.

     

     

    해결 방법은 switch 버튼을 포커스를 가지고 있지 않도록 처리해주면 됩니다.

     

    java에서 처리할 경우 아래 2라인을 추가하면 해결됩니다. setFocusable(false)만 해줘도 onItemClick()이 호출되지만, setFocusableInTouchMode(false)를 해주지 않으면 switch 상태 변경 후 switch 값을 얻어올 때 변경된 값을 읽지 못합니다.

    public ColorItemView(Context context) {
    super(context);
    LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    inflater.inflate(R.layout.color_item, this, true);

    mColorImage = findViewById(R.id.ivColor);
    mColorName = findViewById(R.id.tvName);
    mColorUse = findViewById(R.id.swUse);

    mColorUse.setFocusable(false);

    mColorUse.setFocusableInTouchMode(false);

    }

    xml에서 처리할 경우에는 android:focusableandroid:focusableInTouchMode를 false로 만들어 줍니다.

    <Switch
    android:id="@+id/swUse"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginTop="8dp"
    android:layout_marginEnd="16dp"
    android:focusable="false"
    android:focusableInTouchMode="false"
    app:layout_constraintBottom_toBottomOf="@+id/ivColor"
    app:layout_constraintEnd_toEndOf="parent"

    app:layout_constraintTop_toTopOf="@+id/ivColor" />

    여기까지입니다. 읽어주셔서 감사합니다.

    반응형

    안녕하세요. Simple& Happy Dev입니다.

     

    앞서 접근성과 접근성 개선을 위한 검토방법에 대해서 알아보았습니다.

     

    접근성에 대한 개요는 아랫 글을 참조하시길 바랍니다.

    2018/11/27 - 시각장애인 또는 약시 사용자를 위한 접근성(Accessibility)에 대하여

     

    아래 좌측과 같이 임의로 만든 "접근성 테스트" 앱에서 접근성 검사기를 사용해서 검토하고 개선해보도록 하겠습니다.

    먼저, "접근성 검사기" 서비스를 활성화해서 아래 우측에 있는 것처럼 "접근성 스캔 시작 버튼"을 나오도록 만듭니다.

    기억이 안 나시는 분들은 아랫 글을 참고하시면 됩니다.

    2018/12/17 - 접근성 지원을 위한 개발 시 검토 항목 / 사전 출시 보고서(접근성) / 접근성 검사기

     

     

    접근성 테스트 소스 (개선 전)

    AccessibilityTest_before.zip

     

     

    "접근성 테스트 앱"에 대해서 접근성 검사기로 스캔을 시작해보니 아래와 같이 결과가 나왔습니다.

     

     

     

     

     

     

    검토된 결과 총 21개의 제안사항이 있으며, "텍스트 대비", "터치할 대상", "항목 설명", "이미지 대비", "항목 라벨" 까지 총 5종류로 분류됩니다.

     

    "텍스트 대비" 항목 검토 및 개선

     

    "빨강"이라는 글자와 나무 질감의 배경 간의 대비율(Contrast ratio)이 4.5 이상되어야 하는데 3.72라서 검출되었습니다.

    참고로 글자의 크기가 큰 경우에 기준 대비율이 3.0이 될 수 있습니다.

     

    약시 사용자에게는 글자 인식에 문제가 될 수 있기 때문입니다.

     

    대비율은 https://contrast-ratio.com/ 사이트에서 확인할 수 있습니다.

    여기에서 대비율이 4.5 이상 나오도록 Text color 값 변경해봅니다. #979797 이상만 넣어주면 4.5가 넘어갑니다. 

     

     

     

    color_item.xml에 있는 id가 tvName인 TextView의 textColor를 #979797 이상이 되는 값으로 넣어줍니다.

    여기서는 white(#ffffffff)로 변경했습니다.

    수정 전

     

    <TextView

    android:id="@+id/tvName"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="12dp"
    android:layout_marginTop="16dp"
    android:textColor="#ff888888"
    app:layout_constraintStart_toEndOf="@+id/ivColor"
    app:layout_constraintTop_toTopOf="@+id/ivColor" />

     


     

    수정 후

     

    <TextView
    android:id="@+id/tvName"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="12dp"
    android:layout_marginTop="16dp"
    android:textColor="#ffffffff"
    app:layout_constraintStart_toEndOf="@+id/ivColor"
    app:layout_constraintTop_toTopOf="@+id/ivColor" />

    "텍스트 대비" 검토 항목을 개선 후 접근성 검사기로 다시 스캔해보면 제안사항이 15개로 줄어든 것을 확인할 수 있습니다.

    수정한 것은 하나인데, 갑자기 제안사항 개수가 많이 줄어든 것은 리스트 뷰에서 공용으로 사용하는 아이템 레이아웃을 수정했기 때문입니다.

     

     

     

    "터치할 대상" 항목 검토 및 개선

    버튼의 경우 너비나 높이가 48dp 이상이 되어야 합니다.

    이 크기는 아이콘과 패딩 크기를 포함한 것입니다. 현재 우측 레이아웃 디자인의 패딩 크기는 0입니다.

    이 스위치 버튼의 아이콘 크기가 47dpx27dp이고, 패딩 포함한 전체 크기도 47dpx27dp라서 검출이 되었습니다.

     

     

     

    두 가지 방법으로 개선할 수 있습니다.

    아이콘을 48dp 이상인 아이콘 이미지로 교체하는 방법과 기존의 아이콘을 사용하면서 패딩크기를 조절하는 방법이 있습니다. 여기서는 편의상 후자로 수정을 해보도록 하겠습니다.

     

    아래처럼 start/end 패딩 크기를 각각 1dp 이상, top/bottom 패딩 크기를 각각 11dp 이상 추가해주어도 됩니다.

    android:paddingStart="1dp"
    android:paddingTop="11dp"
    android:paddingEnd="1dp"
    android:paddingBottom="11dp"

    그러나, 여기서는 아래처럼 minWidth와 minHeight를 48dp로 변경해주도록 하겠습니다.

    결과적으로 비슷하게 보입니다만, 혹시 아이콘이 변경되더라도 스위치 버튼의 크기가 48dp 이상이 되도록 만들어줍니다. 

    수정 전

     

    <Switch
    android:id="@+id/swUse"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginEnd="16dp"
    android:focusable="false"
    android:focusableInTouchMode="false"
    app:layout_constraintBottom_toBottomOf="@+id/ivColor"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintTop_toTopOf="@+id/ivColor" />

     


     

    수정 후 

     

    <Switch
    android:id="@+id/swUse"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginEnd="16dp"
    android:focusable="false"
    android:focusableInTouchMode="false"
    android:minWidth="48dp"
    android:minHeight="48dp"
    app:layout_constraintBottom_toBottomOf="@+id/ivColor"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintTop_toTopOf="@+id/ivColor" />

    앞선 레이아웃 디자인과 비교해보면 패딩 크기가 늘어난 것을 확인하실 수 있습니다.

     

    "터치할 대상" 검토 항목을 개선 후 접근성 검사기로 다시 스캔해보면 제안사항이 8개로 줄어든 것을 확인할 수 있습니다.

     

     

    "항목 설명" 항목 검토 및 개선

    스크린 리더는 위젯에 설정된 "콘텐츠 라벨"을 사용자에게 읽어줍니다. 

     

    그러면, 스크린 리더가 텍스트 뷰, 일반 버튼, 스위치 버튼의 "콘텐츠 라벨"을 어떻게 읽는지부터 설명하겠습니다.

     

    텍스트 뷰의 경우 별도의 콘텐츠 라벨이 필요 없습니다. 텍스트 그 자체를 읽기 때문입니다. 위에서 텍스트 뷰의 "색상 이름"은 콘텐츠 라벨 없이도 "빨강", "주황", "노랑" 등으로 읽혀집니다.

     

    일반 버튼의 경우 스크린 리더는 설정된 "콘텐츠 라벨"+"버튼"이라고 읽습니다. 버튼의 콘텐츠 라벨에 "시작"으로 되어 있으면 스크린 리더는 "시작 버튼"이라고 읽게 됩니다. 만약, 콘텐츠 라벨을 "시작 버튼"으로 해두면 "시작 버튼 버튼"으로 읽기 때문에 문제로 검출됩니다.

     

    스위치 버튼의 경우 스크린 리더는 스위치 상태(On/Off)에 따라서 "사용 스위치" 또는 "해제 스위치"라고 읽습니다.

     

    여기 문제가 된 항목은 리스트 뷰의 경우 아이템이 여러 개 존재하지만, 아이템 레이아웃이 하나만 존재하기 때문에 이 레이아웃에 포함된 스위치 버튼이 다른 아이템들에서도 "콘텐츠 라벨"이 동일하다고 검출하는 것입니다.

     

    즉, 동일한 레이아웃을 계속 사용하기 때문에 리스트 뷰의 각 아이템을 스크린 리더가 읽을 때 스위치 상태가 On인 경우 모두 "사용 스위치"라고 읽고, 스위치가 Off인 경우 모두 "해제 스위치"라고 읽게 됩니다. 이렇게 되면 어떤 아이템의 스위치인지 알 수 없게 됩니다. 아래 "개선 전 동영상"을 보시면 알 수 있습니다.

     

     

    이 경우에 수정을 하려면 레이아웃이 아닌 Java 코드 쪽 수정을 해야합니다.

    View를 반환하는 getView에서 스위치 버튼의 "콘텐츠 라벨"을 지정할 수 있도록 setContentDescription 메서드에 아이템의 "색상 이름"을 넣어줍니다. 이렇게 되면 스크린 리더는 아이템에서 스위치 버튼을 "콘텐츠 라벨"+"사용 스위치"라고 읽게 되며, 여기서 "콘텐츠 라벨"이 아이템마다 해당하는 "색상 이름"이 들어가서 "빨강 사용 스위치", "주황 사용 스위치"라고 읽게 됩니다.즉, 각 아이템의 스위치 버튼마다 "콘텐츠 라벨"이 달라져서 구분이 됩니다.

    다만, 아이템의 텍스트 뷰(id:  tvName)의 텍스트도 "색상 이름"이기에 뒤에 "-"를 추가해주어서 "콘텐츠 라벨"이 텍스트 뷰와 같지 않도록 만들었습니다. "-"를 추가해도 스크린 리더가 읽지는 않습니다.

    수정 전

     

    @Override
    public View getView(final int position, View convertView, ViewGroup parent) {
    ColorItemView colorView;
    if (convertView == null) {
    colorView = new ColorItemView(mContext);
    } else {
    colorView = (ColorItemView) convertView;
    }

    colorView.setColorValue(mItems.get(position).getColorValue());
    colorView.setColorName(mItems.get(position).getColorName());
    colorView.setColorUse(mItems.get(position).getColorUse());

    Switch swUse = colorView.findViewById(R.id.swUse);
    swUse.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
    @Override
    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
    mItems.get(position).setColorUse(isChecked);
    }
    });

    return colorView;
    }

     

     


     

    수정 후

     

    @Override
    public View getView(final int position, View convertView, ViewGroup parent) {
    ColorItemView colorView;
    if (convertView == null) {
    colorView = new ColorItemView(mContext);
    } else {
    colorView = (ColorItemView) convertView;
    }

    colorView.setColorValue(mItems.get(position).getColorValue());
    colorView.setColorName(mItems.get(position).getColorName());
    colorView.setColorUse(mItems.get(position).getColorUse());

    Switch swUse = colorView.findViewById(R.id.swUse);
    swUse.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
    @Override
    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
    mItems.get(position).setColorUse(isChecked);
    }
    });
    swUse.setContentDescription(mItems.get(position).getColorName()+"-");

    return colorView;
    }

    "항목 설명" 검토 항목을 개선 후 접근성 검사기로 다시 스캔해보면 제안사항이 6개로 줄어든 것을 확인할 수 있습니다.

     

    개선 전/후 동영상을 확인해보세요. 스크린 리더가 동작하기에 소리를 키우시고 보세요.

     

    [개선 전]

     

    [개선 후]

     

     

    "항목 라벨" 항목 검토 및 개선

    앞서 버튼에 대해서 스크린 리더는 설정된 "콘텐츠 라벨"+"버튼"이라고 읽습니다. 여기서 "스크린이 읽을 수 있는 라벨이 없다"는 말은 해당 버튼에 "콘텐츠 라벨"이 지정되지 않았다는 의미입니다. 그리고, 스크린 리더는 "버튼 레이블 없음"이라고 읽게 됩니다. 이 경우에 시각장애인은 이 버튼이 어떤 용도인지 알 수 없게 됩니다.

     

     

    두 가지 방법으로 수정할 수 있습니다.

    - 자바에서 수정방법: setContentDescription 메서드에 "콘텐츠 라벨" 입력

    - xml에서 수정방법: android:contentDescription에  "콘텐츠 라벨" 입력

     

    여기서는 후자의 방법으로 수정하도록 하겠습니다.

    수정 전

     

    <ImageButton
    android:id="@+id/ibPrev"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginEnd="8dp"
    android:src="@android:drawable/ic_media_previous"
    app:layout_constraintEnd_toStartOf="@+id/ibPlay"
    app:layout_constraintTop_toTopOf="@+id/ibPlay" />

    <ImageButton
    android:id="@+id/ibPlay"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginBottom="8dp"
    android:src="@android:drawable/ic_media_play"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/lvColor" />

    <ImageButton
    android:id="@+id/ibNext"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="8dp"
    android:src="@android:drawable/ic_media_next"
    app:layout_constraintStart_toEndOf="@+id/ibPlay"
    app:layout_constraintTop_toTopOf="@+id/ibPlay" />

     


     

    수정 후

     

    <ImageButton
    android:id="@+id/ibPrev"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginEnd="8dp"
    android:src="@android:drawable/ic_media_previous"
    android:contentDescription="@string/previous_song"
    app:layout_constraintEnd_toStartOf="@+id/ibPlay"
    app:layout_constraintTop_toTopOf="@+id/ibPlay" />

    <ImageButton
    android:id="@+id/ibPlay"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginBottom="8dp"
    android:src="@android:drawable/ic_media_play"
    android:contentDescription="@string/play"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/lvColor" />

    <ImageButton
    android:id="@+id/ibNext"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="8dp"
    android:src="@android:drawable/ic_media_next"
    android:contentDescription="@string/next_song"
    app:layout_constraintStart_toEndOf="@+id/ibPlay"
    app:layout_constraintTop_toTopOf="@+id/ibPlay" />

    "항목 라벨" 검토 항목을 개선 후 접근성 검사기로 다시 스캔해보면 제안사항이 3개로 줄어든 것을 확인할 수 있습니다.

     

     

    "이미지 대비" 항목 검토 및 개선

    앞서 "텍스트 대비"와 유사하게 전면 이미지와 배경 간의 대비율(Contrast ratio)이 3.0 이상 되어야 하는데, 이에 미치지 못해서 검출되었습니다.

     

     

    여기서는 색조를 조정해서 버튼과 나무 질감의 배경 간의 대비율을 높이도록 해보겠습니다.

    android:backgroundTint에 @color/colorPrimary(=#3F51B5 "    ")를 추가했습니다.

    수정 전

     

    <ImageButton
    android:id="@+id/ibPrev"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginEnd="8dp"
    android:src="@android:drawable/ic_media_previous"
    android:contentDescription="@string/previous_song"
    app:layout_constraintEnd_toStartOf="@+id/ibPlay"
    app:layout_constraintTop_toTopOf="@+id/ibPlay" />

    <ImageButton
    android:id="@+id/ibPlay"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginBottom="8dp"
    android:src="@android:drawable/ic_media_play"
    android:contentDescription="@string/play"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/lvColor" />

    <ImageButton
    android:id="@+id/ibNext"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="8dp"
    android:src="@android:drawable/ic_media_next"
    android:contentDescription="@string/next_song"
    app:layout_constraintStart_toEndOf="@+id/ibPlay"
    app:layout_constraintTop_toTopOf="@+id/ibPlay" />

     


     

    수정 후

     

    <ImageButton
    android:id="@+id/ibPrev"
    android:backgroundTint="@color/colorPrimary"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginEnd="8dp"
    android:src="@android:drawable/ic_media_previous"
    android:contentDescription="@string/previous_song"
    app:layout_constraintEnd_toStartOf="@+id/ibPlay"
    app:layout_constraintTop_toTopOf="@+id/ibPlay" />

    <ImageButton
    android:id="@+id/ibPlay"
    android:backgroundTint="@color/colorPrimary"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginBottom="8dp"
    android:src="@android:drawable/ic_media_play"
    android:contentDescription="@string/play"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/lvColor" />

    <ImageButton
    android:id="@+id/ibNext"
    android:backgroundTint="@color/colorPrimary"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="8dp"
    android:src="@android:drawable/ic_media_next"
    android:contentDescription="@string/next_song"
    app:layout_constraintStart_toEndOf="@+id/ibPlay"
    app:layout_constraintTop_toTopOf="@+id/ibPlay" />

    아래와 같이 이미지와 배경 간의 구분이 좋아졌습니다.

    "이미지 대비" 검토 항목까지 개선 후 접근성 검사기로 다시 스캔해보면 "제안사항이 없음"으로 모두 수정된 것을 확인할 수 있습니다.

     

     

     

     

    리스트 뷰의 아이템을 누르게 되면 아래 왼쪽과 같이 팝업(AlertDialog)이 발생하게 되어있습니다.

    이것도 "접근성 검사기"로 스캔을 해보면 아래 오른쪽처럼 개선해야 할 1건이 확인됩니다.

     

     

    세부내용을 확인해보면, 제일 처음에 다루었던 "텍스트 대비"에 대한 문제점입니다.

     

    그런데, 이 팝업(AlertDialog)은 레이아웃으로 만든 게 아니어서, 테마를 적용해서 스타일에서 텍스트의 색상을 조절하도록 수정하였습니다.

     

    MainActivity.java

     

    수정 전

     

    ListView colorListView = findViewById(R.id.lvColor);
    colorListView.setAdapter(mColorListAdapter);
    colorListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
    ColorItem selectedItem = (ColorItem) mColorListAdapter.getItem(position);
    AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
    AlertDialog alert = builder.setMessage(selectedItem.getColorName() + getResources().getString(R.string.dialog_msg) +
    (selectedItem.getColorUse() ? getResources().getString(R.string.yes) : getResources().getString(R.string.no)))
    .setPositiveButton(getResources().getString(android.R.string.ok), new DialogInterface.OnClickListener() {
    @Override
    public void onClick(DialogInterface dialog, int whichButton) {
    dialog.cancel();
    }
    }).create();
    alert.show();
    }
    });

     


     

     

    수정 후 



    ListView colorListView = findViewById(R.id.lvColor);
    colorListView.setAdapter(mColorListAdapter);
    colorListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
    ColorItem selectedItem = (ColorItem) mColorListAdapter.getItem(position);
    AlertDialog.Builder builder = new AlertDialog.Builder(new ContextThemeWrapper(MainActivity.this, R.style.AlertDialogCustom));
    AlertDialog alert = builder.setMessage(selectedItem.getColorName() + getResources().getString(R.string.dialog_msg) +
    (selectedItem.getColorUse() ? getResources().getString(R.string.yes) : getResources().getString(R.string.no)))
    .setPositiveButton(getResources().getString(android.R.string.ok), new DialogInterface.OnClickListener() {
    @Override
    public void onClick(DialogInterface dialog, int whichButton) {
    dialog.cancel();
    }
    }).create();
    alert.show();
    }
    });

     

    styles.xml

     

    수정 전

     

    <resources>

      <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    <!-- Customize your theme here. -->
    <item name="colorPrimary">@color/colorPrimary</item>
    <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
    <item name="colorAccent">@color/colorAccent</item>
    </style>

    </resources>

     


     

    수정 후

     

    <resources>
    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    <!-- Customize your theme here. -->
    <item name="colorPrimary">@color/colorPrimary</item>
    <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
    <item name="colorAccent">@color/colorAccent</item>
    </style>
    <style name="AlertDialogCustom" parent="Theme.AppCompat.Light.Dialog.Alert">
    <item name="android:textColor">#000000</item>
    <item name="android:typeface">normal</item>
    <item name="android:textSize">20dp</item>
    </style>
    </resources>

    수정된 팝업은 아래와 같이 나옵니다. "확인" 버튼의 색상이 검정으로 변경되었고, 크기도 조금 더 커졌습니다.

     

    접근성 테스트 소스 (개선 후)

    AccessibilityTest_after.zip

     

    결론

    접근성 개선을 위한 도구인 "접근성 검사기"를 이용해서 접근성 개선하는 예제를 확인해보았습니다.

    때로는 너무 접근성 개선을 위해서 수정을 하다가 보면 많이 복잡해질 뿐만 아니라 접근성이 필요 없는 사람에게는 높은 대비로 인하여 시각적인 불편함을 느낄 수도 있습니다.

     

    이와 관련해서 최초 앱 진입 시 접근성이 활성화 되어 있는지를 체크해서 이에 따라서 레이아웃을 다르게 보이도록 처리하는 방법도 하나의 대안이 될 수 있을 것 같습니다.

     

    활성화 여부 체크 관련한 내용은 아래 글에 포함되어 있으니 참조하시길 바랍니다.

     

    2018/12/15 - 앱 진입시 스크린 리더(TalkBack, Voice Assistant) 감지하기

     

    긴 글 읽어주셔서 감사합니다.

    반응형

    안녕하세요. Simple& Happy Dev입니다.

     

    이번 글에서는 접근성 지원을 위해서 어떤 부분을 검토해야 하고 이에 도움을 줄 수 있는 도구들을 소개합니다.

     

    접근성에 대한 기본적인 내용은 아래 글을 참조하시길 바랍니다.

    2018/11/27 - 시각장애인 또는 약시 사용자를 위한 접근성(Accessibility)에 대하여

     

     

    먼저 접근성 지원을 위해서 개발하는 도중에 고려해야 할 내용에 대해서 설명하겠습니다.

     

    [접근성 - 개발 중 검토 방법]

    1. 터치 대상 크기 조절

       화면상의 요소(주로 버튼)에 대해서 사용자가 터치하는데 불편함이 없는지에 대한 너비 및 높이에 대한 기준 크기

       최소 48dp가 되어야 합니다.

     

    2. 콘텐츠 라벨 지정

       토크백과 같은 스크린 리더가 음성 안내를 할 수 있도록 화면상의 요소들에 라벨을 지정이 필요합니다.

       텍스트 뷰(TextView)와 같이 별도 지정이 필요하지 않은 것도 있고, 이미지 뷰(ImageView)나 이미지 버튼(ImageButton)과

       같이 별도로 추가해야 하는 것도 있습니다. 배경 이미지나 화면을 꾸미기 위해서 넣은 이미지의 경우와 같이 라벨이 필요

       하지 않아도 되는 경우 제외하도록 처리할 수 있습니다.

     

    3. 대비(Contrast) 조절

       쉽게 읽을 수 있도록 "텍스트와 배경색상" 또는 "이미지와 배경색상" 간의 명암비가 큰 텍스트는 3.0 이상 작은 텍스트는

       4.5 이상이어야 합니다.

     

    4. 구현 검사

       View 계층구조를 검토하고 사용자가 레이아웃과 상호작용하는 데 문제가 될 수 있는 인스턴스를 검사합니다.

       클릭 가능한 링크 (Clickablespan 대신 Urlspans 사용)

     

    접근성을 지원하기 위해서 개발자는 개발 중에 위의 4가지를 고려해서 작업해야 하겠지만, 일단 고려하지 않고 작업 후 검사하는 방법도 있습니다.

     

    [접근성 - 개발 후 검토 방법]

    ■ 사전 출시 보고서 (접근성 항목)

    플레이 스토어에 앱을 출시하는 경우 Google Play Console에 apk를 등록 후 알파(공개 트랙)나 베타(비공개 트랙)로 릴리즈하면 "사전 출시 보고서"가 생성됩니다.

     

    아래의 경우 접근성 항목에서 해당 apk의 경고성 문제 15개와 사소한 문제 8개를 검출한 것을 확인할 수 있습니다.

     

     

    접근성 항목으로 진입하면 스크린샷과 문제가 된 위치를 확인할 수 있습니다.

    (아래의 경우 앱의 스크린샷을 제대로 못 했는지 일부만 표시되었습니다. 원래는 제대로 표시됩니다)

     

     

    스크린샷 화면을 클릭하면 문제가 된 리소스명, 해결 방법에 대한 설명이 나옵니다.

     

    간혹 문제가 된 위치가 엉뚱한 곳으로 표시되는 경우가 있는데, 이때는 리소스명으로 문제가 된 곳을 확인하시면 됩니다.

     

    ■ 접근성 검사기

    Google Play Console을 이용할 수 없는 경우는 접근성 검사기를 이용해서 문제점을 확인할 수 있습니다.

    접근성 검사기는 구글에서 안드로이드 앱의 접근성 개선을 도와주도록 만든 도구입니다.

     

    사용 방법

    - 플레이 스토어에서 다운로드 후 설치, "설정 - 접근성 - 접근성 검사기 - 서비스 활성화"

       접근성 검사기 서비스가 활성화되면 우측처럼 가운데 오버레이 된 "파란색 체크 버튼"이 나타납니다.

       이 버튼은 "접근성 스캔 시작 버튼"이라고 부르며 다른 앱으로 이동하더라도 계속 표시됩니다.

     

     

     

    - 검사할 앱으로 가서 "접근성 스캔 시작 버튼"을 눌러줍니다. (아래 예로 "11번가" 앱을 검사하였습니다)

      스캔할 때 "찰칵"하는 촬영음 발생되고 잠시후에 오른쪽에 있는 것과 같이 결과가 표시됩니다.

      총 26개의 검토 항목이 검출되었고, 해당 위치에 주황색 사각형으로 표시되어 있습니다.

     

     

    위의 접근성 스캔 결과가 표시된 화면의 상단의 목록 버튼을 누르면 아래 왼쪽과 같이 26개 검토항목이 요소별로 나열되어 나옵니다. 그리고, 주황색 사각형으로 표시된 걸 누르면 아래 오른쪽과 같이 그 위치의 검토할 내용이 표시되며, 해결방법에 대한 설명이 나옵니다.

     

     

    이상으로 접근성 지원을 위한 개발 시 검토 항목과 접근성 검토를 위한 도구들 사용법을 알아보았습니다.

     

    다음에는 실제 예를 들어서 접근성 개선하는 것을 보여드리도록 하겠습니다.

     

    읽어주셔서 감사합니다.

    반응형

    [Snippet]

    앱 진입시 "스크린 리더" 감지하기

    @Override
    protected void onResume() {
    super.onResume();

    AccessibilityManager accessibilityManager = (AccessibilityManager) getSystemService(ACCESSIBILITY_SERVICE);
    boolean isScreenReaderEnabled = accessibilityManager.isEnabled() && accessibilityManager.isTouchExplorationEnabled();

    if (isScreenReaderEnabled) {
    // 스크린 리더 활성화시
    } else {
    // 스크린 리더 비활성화시
    }
    }

     

     

    [Sample]

    "스크린 리더" 활성화에 따른 UI 변경

    public class MainActivity extends AppCompatActivity {
    private TextView mTextView;
    private ImageButton mImageButton;
    private Button mButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    mTextView = findViewById(R.id.textView);
    mImageButton = findViewById(R.id.imageButton);
    mButton = findViewById(R.id.button);
    }

    @Override
    protected void onResume() {
    super.onResume();

    AccessibilityManager accessibilityManager = (AccessibilityManager) getSystemService(ACCESSIBILITY_SERVICE);
    boolean isScreenReaderEnabled = accessibilityManager.isEnabled() && accessibilityManager.isTouchExplorationEnabled();

    if (isScreenReaderEnabled) {
    mTextView.setText(getResources().getString(R.string.screen_reader_enable));
    mButton.setVisibility(View.VISIBLE);
    mImageButton.setVisibility(View.INVISIBLE);
    } else {
    mTextView.setText(getResources().getString(R.string.screen_reader_disable));
    mButton.setVisibility(View.INVISIBLE);
    mImageButton.setVisibility(View.VISIBLE);
    }
    }
    }

    프로젝트 화일
    ScreenReaderDetectTest.zip

     

     

    [실행결과]

     

     

     

    + Recent posts