반응형

안녕하세요. 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개 검토항목이 요소별로 나열되어 나옵니다. 그리고, 주황색 사각형으로 표시된 걸 누르면 아래 오른쪽과 같이 그 위치의 검토할 내용이 표시되며, 해결방법에 대한 설명이 나옵니다.

     

     

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

     

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

     

    읽어주셔서 감사합니다.

    반응형

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

     

    접근성(Accessibility)이라는 말을 들어본 적 있을 겁니다.

     

    위키피디아에 보면 아래와 같이 정의되어 있습니다.

     

     

    많은 사용자가 편하게 이용할 수 있도록 제공하는 것을 말합니다.

     

    이 글에서는 시각 장애인이나 약시 사용자들을 위한 접근성에 대해서 다룰 것입니다.

     

    시각 장애인들의 경우 시각을 대신해서 촉각이나 청각을 많이 이용합니다.

     

    화면을 가진 디바이스의 경우 스크린 리더(Screen Reader)라고 해서 청각을 이용해서 시각 장애인들에게 도움을 주고 있습니다.

     

    윈도우즈의 경우 제어판의 접근성 센터 - 디스플레이가 없는 컴퓨터 사용 메뉴에서 설정할 수 있습니다.

     

     

     

    여기에 "텍스트 소리내어 읽기"라고 있습니다. 이것을 활성화해주면 마우스가 있는 위치에 텍스트를 읽어주게 됩니다.

     

    기본으로 영어에 대한 텍스트 음성 변환해주며 다른 언어의 경우 추가 설치가 필요합니다.

     


     

    안드로이드의 경우 설정 - 접근성에 가면 시각 장애인 및 약시 사용자를 위한 기능들이 있습니다.

     

    제조회사마다 조금씩 커스터마이징해주기에 하위 메뉴 이름이 조금씩 틀릴 수 있습니다.

     

    아래 설명하는 것은 삼성 단말기의 메뉴 기준으로 설명합니다. 다른 제조회사에서도 비슷한 메뉴가 존재할 것입니다.

     

    약시 사용자를 위해서 "고대비 글자(High contrast fonts)", "고대비 키보드 설정(High contrast keyboard)"을 하면 글자 색상과 윤곽이 좀 더 강조되고 키보드의 경우 눈에 띄는 색상으로 변경되고 크기도 조절됩니다.

     

    "버튼 강조(Show button shape)" 설정하면 버튼을 좀 더 잘 구분할 수 있도록 배경이나 윤곽 표시를 해줍니다.

     

    고대비 글자를 설정하면 아래와 같이 달라집니다.

    진해진 색상과 윤곽의 변화를 확인하실 수 있습니다.

    "고대비 글자" 설정 전/후

     

     

     

    이번에는 고대비 글자버튼 강조를 동시에 설정해 보겠습니다.

    왼쪽보다 오른쪽이 약시 사용자들에게는 잘 보일 것입니다.

    "고대비 글자" 설정 및 "버튼 강조" 설정 전/후 

     

    마지막으로 고대비 키보드를 설정해 보겠습니다.
    크기뿐만 아니라 키의 색상도 배경과 구분이 되어서 키를 누르기 쉽도록 바뀌었습니다. 

    "고대비 키보드" 설정 전/후 

     

     

    시각 장애인들의 경우 앞서 말했듯이 청각을 이용한 기능을 사용합니다. 보통 구글에서 제공하는 토크백(TalkBack)이 제공됩니다. 접근성 메뉴에 토크백이 보이지 않으면, Play 스토어에 들어가서 토크백을 검색해서 설치하면 됩니다.

     

    토크백의 경우 윈도우즈의 "텍스트 소리내어 읽기"처럼 현재 위치하는 위젯의 텍스트를 읽어 줍니다.

     

    시각 장애인들은 보통 화면에 손가락을 대고 떼지 않은 상태에서 이동합니다. 이동하다가 아이콘이나 버튼 같은 위젯을 만나게 되면, 손가락 위치에 무엇인가 있다는 것을 알려주기 위해서 알림 소리와 햅틱이 동작합니다. 햅틱을 지원하지 않으면 알림 소리만 나옵니다. 이때 소리가 나와서 현재 위젯의 텍스트를 읽어줍니다.

     

    아래 토크백 동작하는 것을 확인해보세요. 소리를 키우시고 들어보세요.

     

     

    토크백을 처음 사용하시는 분들은 좀 당황스러울 수 있습니다.

     

    일단 기본적으로 해당 위치의 앱이나 위젯을 실행하기 위해서는 두 번 터치를 해야합니다.

    그리고, 좌우/상하 스크롤의 경우 두 손가락을 화면에 댄 상태에서 스크롤을 해야 동작합니다.

     

    위의 동영상을 보시면 Play 스토어에서 스크롤을 하기 위해서 두 손가락을 화면에 댄 상태에서 스크롤하는 모습을 확인하실 수 있습니다.

     

    시각 장애인 또는 약시 사용자를 위한 접근성에 대해서 알아보았습니다.

     

    읽어주셔서 감사합니다.

     

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

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

     

    반응형

     

     

    Build type(user/userdebug/eng)과 루트 권한에 대해서 알아봅니다.

    아래는 Build type의 용도에 관한 간단한 설명입니다.

      Buildtype

      Use

      user

      limited access; suited for production 

      userdebug

      like "user" but with root access and debuggability; preferred for debugging

      eng

      development configuration with additional debugging tools

     

    각 Build type 별 보안 및 디버그 관련 설정되는 System Property는 다음과 같습니다.

    Buildtype

    System Property

       user

      ro.secure = 1 , ro.debuggable = 0

       userdebug

      ro.secure = 1 , ro.debuggable = 1

       eng

      ro,secure = 0 , ro.debuggable = 1

     

    System Property는 개별 또는 두 개 조합으로 Framework와 Application에서 Buildtype에 따른 로직 처리를 위해서 사용됩니다. 

    대표적으로 Framework에서 Debug 모드 시 분기 조건으로 많이 사용되는 IS_DEBUGGABLE은 아래와 같이 정의되어 있습니다.

    /frameworks/base/core/java/android/os/Build.java

     

    /** * Returns true if we are running a debug build such as "user-debug" or "eng". * @hide */ public static final boolean IS_DEBUGGABLE = SystemProperties.getInt("ro.debuggable", 0) == 1;

     

     

    안드로이드 단말기는 양산할 때 user 바이너리(Build type: user)가 들어갑니다.

    물론 시중에 나와 있는 단말기 중에는 userdebug 바이너리가 들어간 것도 있습니다.

     

    아래에서 다루겠지만, userdebug인 경우 쉽게 루트 권한을 가질 수 있고[각주:1] 커널 로그도 확인할 수 있어서 개발자 입장에서 선호하는 Build type입니다.

    양산단계에 안드로이드 단말기에 user 바이너리가 들어가는 이유

     - 기본탑재 앱(Preload app)의 보호 (기본탑재 앱의 삭제 또는 변형에 따른 오동작 방지)

     - A/S 이슈 (루트 권한을 이용해서 시스템에 리스크를 줄 수 있는 변경에 따른 고장 유발)

     - 보안 이슈

    단말기 제조업체에서는 개발 중에는 eng 또는 userdebug 바이너리로 단말기를 개발 또는 테스트합니다.

    개발단계에 단말기는 eng / userdebug 바이너리를 사용하는 경우

     - 단말기 브링업/셋업시는 eng 바이너리가 작업에 유리한 점이 많습니다.

     - 단말기 안정화된 후 검증에는 user 바이너리와 가까운 userdebug 바이너리가 적합합니다.

        (예로들면 eng 바이너리는 상대적으로 로드가 있기 때문에 성능관련한 검증 및 사용성 검증 등에는 적합하지 않습니다.)

    안드로이드 초기버전에는 user 바이너리 단말에서도 안드로이드 취약점을 이용한 루팅이 가능했지만, 현재는 안드로이드 보안패치로 인하여 막혔습니다. 단, 리커버리를 통한 커스텀 바이너리 업데이트로 루팅은 여전히 가능합니다.

     

    userdebug 바이너리 단말기에서는 ADB(Android Debug Bridge) 데몬을 루트 권한으로 동작하게끔 명령(adb root)을 날려줌으로써 루트 권한을 가진 ADB로 restart 하는 방법과 ADB restart 없이 쉘에서 su 명령에 의한 루트 권한으로 해당 명령을 동작하게 하는 방법이 있습니다.

     

    [userdebug 바이너리 단말기에서 su 명령에 의한 루트 권한으로 동작하게 하는 방법]

    아래와 같이 커널로그(kmsg 로그)는 루트 권한이 없으면 명령이 먹히지 않습니다. 

     

    그러나, su 명령으로 루트 권한을 주면 명령이 먹힙니다.

     

     

    [userdebug 바이너리 단말기에서 루트 권한을 가진 ADB로 restart 하는 방법]

    - adb root 명령 실행 후 uid/gid 모두 root로 변경된 것을 확인할 수 있습니다.

    - adb root 명령 실행 후 adb는 루트 권한을 가지고, 이후 su 명령없이도 동작하는 것을 확인할 수 있습니다.

     

     

    이상으로 Build type(user/userdebug/eng)과 루트 권한에 대해서 알아보았습니다.

     

    조금이나마 도움이 되셨으면 아래 공감 버튼을 눌러주세요.

    1. 루팅(rooting) [본문으로]

    + Recent posts