مبادئ تحسين إمكانية الوصول إلى التطبيق (طرق العرض)

مفاهيم وتنفيذ Jetpack Compose

لمساعدة المستخدمين الذين لديهم احتياجات متعلّقة بتسهيل الاستخدام، يتيح لك إطار عمل Android إنشاء خدمة مخصّصة لتسهيل الاستخدام يمكنها عرض محتوى من التطبيقات للمستخدمين وتشغيل التطبيقات أيضًا نيابةً عنهم.

توفّر Android عدة خدمات تسهيل استخدام على مستوى النظام، بما في ذلك ما يلي:

لمساعدة المستخدمين الذين لديهم احتياجات متعلّقة بإمكانية الوصول على استخدام تطبيقك بنجاح، يجب أن يتّبع تطبيقك أفضل الممارسات الموضّحة في هذه الصفحة، والتي تستند إلى الإرشادات الموضّحة في مقالة جعل التطبيقات أكثر سهولة في الاستخدام.

تصنيف العناصر

من المهم تزويد المستخدمين بتصنيفات مفيدة وواصفة لكل عنصر في واجهة المستخدم التفاعلي في تطبيقك. يجب أن يوضّح كل تصنيف معنى عنصر معيّن والغرض منه. يمكن لقارئات الشاشة، مثل TalkBack، قراءة هذه التصنيفات للمستخدمين.

في معظم الحالات، يمكنك تحديد وصف لعنصر واجهة مستخدم في ملف مورد التنسيق الذي يحتوي على العنصر. عادةً، يمكنك إضافة تصنيفات باستخدام السمة contentDescription، كما هو موضّح في دليل جعل التطبيقات أكثر سهولة في الاستخدام. هناك عدة طرق أخرى لإضافة التصنيفات موضّحة في الأقسام التالية.

العناصر القابلة للتعديل

عند تصنيف العناصر القابلة للتعديل، مثل EditText الكائنات، من المفيد عرض نص يقدّم مثالاً على الإدخال الصالح في العنصر نفسه، بالإضافة إلى إتاحة نص المثال هذا لقارئات الشاشة. في هذه الحالات، يمكنك استخدام السمة android:hint، كما هو موضّح في المقتطف التالي:

<!-- The hint text for en-US locale would be
     "Apartment, suite, or building". -->
<EditText
   android:id="@+id/addressLine2"
   android:hint="@string/aptSuiteBuilding" ... />

في هذه الحالة، يجب ضبط السمة android:labelFor لكائن View على رقم تعريف عنصر EditText لمزيد من التفاصيل، اطّلِع على القسم التالي.

أزواج العناصر التي يصف أحدها الآخر

من الشائع أن يكون لعنصر EditText كائن مطابق View يصف ما يجب على المستخدمين إدخاله في عنصر EditText. يمكنك الإشارة إلى هذه العلاقة من خلال ضبط السمة android:labelFor لكائن View.

يظهر مثال على تصنيف أزواج العناصر هذه في المقتطف التالي:

<!-- Label text for en-US locale would be "Username:" -->
<TextView
   android:id="@+id/usernameLabel" ...
   android:text="@string/username"
   android:labelFor="@+id/usernameEntry" />

<EditText
   android:id="@+id/usernameEntry" ... />

<!-- Label text for en-US locale would be "Password:" -->
<TextView
   android:id="@+id/passwordLabel" ...
   android:text="@string/password
   android:labelFor="@+id/passwordEntry" />

<EditText
   android:id="@+id/passwordEntry"
   android:inputType="textPassword" ... />

العناصر في مجموعة

عند إضافة تصنيفات إلى عناصر مجموعة، يجب أن يكون كل تصنيف فريدًا. بهذه الطريقة، يمكن لخدمات تسهيل الاستخدام في النظام الإشارة إلى عنصر واحد فقط على الشاشة عند قراءة تصنيف. يتيح هذا التطابق للمستخدمين معرفة متى يتنقّلون بين عناصر واجهة المستخدم أو عندما ينقلون التركيز إلى عنصر سبق لهم اكتشافه.

على وجه الخصوص، عليك تضمين نص إضافي أو معلومات سياقية في العناصر ضمن التنسيقات التي تتم إعادة استخدامها، مثل RecyclerView الكائنات، حتى يتم تحديد كل عنصر فرعي بشكلٍ فريد.

لإجراء ذلك، اضبط وصف المحتوى كجزء من عملية تنفيذ المحوّل، كما هو موضّح في مقتطف الرمز التالي:

Kotlin

data class MovieRating(val title: String, val starRating: Integer)

class MyMovieRatingsAdapter(private val myData: Array<MovieRating>):
        RecyclerView.Adapter<MyMovieRatingsAdapter.MyRatingViewHolder>() {

    class MyRatingViewHolder(val ratingView: ImageView) :
            RecyclerView.ViewHolder(ratingView)

    override fun onBindViewHolder(holder: MyRatingViewHolder, position: Int) {
        val ratingData = myData[position]
        holder.ratingView.contentDescription = "Movie ${position}: " +
                "${ratingData.title}, ${ratingData.starRating} stars"
    }
}

Java

public class MovieRating {
    private String title;
    private int starRating;
    // ...
    public String getTitle() { return title; }
    public int getStarRating() { return starRating; }
}

public class MyMovieRatingsAdapter
        extends RecyclerView.Adapter<MyAdapter.MyRatingViewHolder> {
    private MovieRating[] myData;


    public static class MyRatingViewHolder extends RecyclerView.ViewHolder {
        public ImageView ratingView;
        public MyRatingViewHolder(ImageView iv) {
            super(iv);
            ratingView = iv;
        }
    }

    @Override
    public void onBindViewHolder(MyRatingViewHolder holder, int position) {
        MovieRating ratingData = myData[position];
        holder.ratingView.setContentDescription("Movie " + position + ": " +
                ratingData.getTitle() + ", " + ratingData.getStarRating() +
                " stars")
    }
}

مجموعات المحتوى ذي الصلة

إذا كان تطبيقك يعرض عدة عناصر في واجهة المستخدم تشكّل مجموعة طبيعية، مثل تفاصيل أغنية أو سمات رسالة، رتِّب هذه العناصر داخل حاوية، وهي عادةً فئة فرعية من ViewGroup. اضبط السمة لكائن الحاوية android:screenReaderFocusable على true، واضبط السمة لكل كائن داخلي android:focusable على false. بهذه الطريقة، يمكن لخدمات تسهيل الاستخدام عرض أوصاف محتوى العناصر الداخلية، واحدًا تلو الآخر، في إعلان واحد. يساعد هذا التجميع للعناصر ذات الصلة مستخدمي التكنولوجيا المساعدة في اكتشاف المعلومات على الشاشة بكفاءة أكبر.

android:focusable

يحتوي المقتطف التالي على أجزاء من المحتوى ذات صلة ببعضها البعض، لذا تم ضبط السمة android:screenReaderFocusable لعنصر الحاوية، وهو مثيل من ConstraintLayout، على true، وتم ضبط السمة android:focusable لكل عنصر من عناصر TextView الداخلية على false:

<!-- In response to a single user interaction, accessibility services announce
     both the title and the artist of the song. -->
<ConstraintLayout
    android:id="@+id/song_data_container" ...
    android:screenReaderFocusable="true">

    <TextView
        android:id="@+id/song_title" ...
        android:focusable="false"
        android:text="@string/my_song_title" />
    <TextView
        android:id="@+id/song_artist"
        android:focusable="false"
        android:text="@string/my_songwriter" />
</ConstraintLayout>

بما أنّ خدمات تسهيل الاستخدام تقرأ أوصاف العناصر الداخلية في عبارة واحدة، من المهم أن يكون كل وصف قصيرًا قدر الإمكان مع الحفاظ على معنى العنصر.

ملاحظة: بشكل عام، عليك تجنُّب إنشاء وصف محتوى لمجموعة من خلال تجميع نص العناصر الفرعية. يؤدي ذلك إلى جعل وصف المجموعة غير دقيق، وعندما يتغيّر نص عنصر فرعي ، قد لا يعود وصف المجموعة مطابقًا للنص المرئي.

في سياق قائمة أو شبكة، قد يجمع قارئ الشاشة نصّ العُقد النصية الفرعية لعنصر قائمة أو شبكة. من الأفضل تجنُّب تعديل هذا الإعلان.

مجموعات مُدمجة

إذا كانت واجهة تطبيقك تعرض معلومات متعددة الأبعاد، مثل قائمة بأحداث مهرجان يومًا بيوم، استخدِم السمة android:screenReaderFocusable في حاويات المجموعات الداخلية. يوفّر مخطط التصنيف هذا توازنًا جيدًا بين عدد الإعلانات اللازمة لاكتشاف محتوى الشاشة وطول كل إعلان.

يوضّح مقتطف الرمز التالي إحدى طرق تصنيف المجموعات داخل مجموعات أكبر:

<!-- In response to a single user interaction, accessibility services
     announce the events for a single stage only. -->
<ConstraintLayout
    android:id="@+id/festival_event_table" ... >
    <ConstraintLayout
        android:id="@+id/stage_a_event_column"
        android:screenReaderFocusable="true">

        <!-- UI elements that describe the events on Stage A. -->

    </ConstraintLayout>
    <ConstraintLayout
        android:id="@+id/stage_b_event_column"
        android:screenReaderFocusable="true">

        <!-- UI elements that describe the events on Stage B. -->

    </ConstraintLayout>
</ConstraintLayout>

العناوين داخل النص

تستخدم بعض التطبيقات عناوين لتلخيص مجموعات النصوص التي تظهر على الشاشة. إذا كان عنصر View معيّن يمثّل عنوانًا، يمكنك الإشارة إلى الغرض منه لخدمات تسهيل الاستخدام من خلال ضبط السمة android:accessibilityHeading للعنصر على true.

يمكن لمستخدمي خدمات تسهيل الاستخدام اختيار التنقّل بين العناوين بدلاً من التنقّل بين الفقرات أو بين الكلمات. تُحسّن هذه المرونة تجربة التنقّل في النص.

عناوين لوحات تسهيل الاستخدام

في Android 9 (مستوى واجهة برمجة التطبيقات 28) والإصدارات الأحدث، يمكنك تقديم عناوين سهلة الاستخدام للوحات الشاشة. لأغراض تسهيل الاستخدام، اللوحة هي جزء مميّز مرئيًا من نافذة، مثل محتويات جزء. لكي تفهم خدمات تسهيل الاستخدام سلوك اللوحة الذي يشبه النافذة، عليك منح عناوين وصفية للوحات تطبيقك. يمكن لخدمات تسهيل الاستخدام بعد ذلك تقديم معلومات أكثر دقة للمستخدمين عند تغيُّر مظهر اللوحة أو محتواها.

لتحديد عنوان لوحة، استخدِم السمة android:accessibilityPaneTitle ، كما هو موضّح في المقتطف التالي:

<!-- Accessibility services receive announcements about content changes
     that are scoped to either the "shopping cart view" section (top) or
     "browse items" section (bottom) -->
<MyShoppingCartView
     android:id="@+id/shoppingCartContainer"
     android:accessibilityPaneTitle="@string/shoppingCart" ... />

<MyShoppingBrowseView
     android:id="@+id/browseItemsContainer"
     android:accessibilityPaneTitle="@string/browseProducts" ... />

العناصر الزخرفية

إذا كان عنصر في واجهة المستخدم مخصّصًا فقط لأغراض التباعد المرئي أو المظهر المرئي ، اضبط السمة android:importantForAccessibility على "no".

إضافة إجراءات تسهيل الاستخدام

من المهم السماح لمستخدمي خدمات تسهيل الاستخدام بتنفيذ جميع مسارات المستخدم بسهولة داخل تطبيقك. على سبيل المثال، إذا كان بإمكان المستخدم التمرير سريعًا على عنصر في قائمة، يمكن أيضًا عرض هذا الإجراء لخدمات تسهيل الاستخدام حتى يتمكّن المستخدمون من إكمال مسار المستخدم نفسه بطريقة بديلة.

إتاحة جميع الإجراءات

قد يحتاج مستخدم TalkBack أو الوصول الصوتي، أو الوصول عبر مفتاح تحكّم إلى طرق بديلة لإكمال مسارات مستخدم معيّنة داخل التطبيق. بالنسبة إلى الإجراءات المرتبطة بالإيماءات، مثل السحب والإفلات أو التمرير السريع، يمكن لتطبيقك عرض الإجراءات بطريقة يمكن لمستخدمي خدمات تسهيل الاستخدام الوصول إليها.

باستخدام إجراءات تسهيل الاستخدام، يمكن للتطبيق توفير طرق بديلة للمستخدمين لإكمال إجراء معيّن.

على سبيل المثال، إذا كان تطبيقك يسمح للمستخدمين بالتمرير سريعًا على عنصر، يمكنك أيضًا عرض هذه الوظيفة من خلال إجراء تسهيل استخدام مخصّص، مثل ما يلي:

Kotlin

ViewCompat.addAccessibilityAction(
    // View to add accessibility action
    itemView,
    // Label surfaced to user by an accessibility service
    getText(R.id.archive)
) { _, _ ->
    // Same method executed when swiping on itemView
    archiveItem()
    true
}

Java

ViewCompat.addAccessibilityAction(
    // View to add accessibility action
    itemView,
    // Label surfaced to user by an accessibility service
    getText(R.id.archive),
    (view, arguments) -> {
        // Same method executed when swiping on itemView
        archiveItem();
        return true;
    }
);

With the custom accessibility action implemented, users can access the action through the actions menu.

Make available actions understandable

When a view supports actions such as touch & hold, an accessibility service such as TalkBack announces it as "Double tap and hold to long press."

This generic announcement doesn't give the user any context about what a touch & hold action does.

To make this announcement more descriptive, you can replace the accessibility actions announcement like so:

Kotlin

ViewCompat.replaceAccessibilityAction(
    // View that contains touch & hold action
    itemView,
    AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_LONG_CLICK,
    // Announcement read by TalkBack to surface this action
    getText(R.string.favorite),
    null
)

Java

ViewCompat.replaceAccessibilityAction(
    // View that contains touch & hold action
    itemView,
    AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_LONG_CLICK,
    // Announcement read by TalkBack to surface this action
    getText(R.string.favorite),
    null
);

This results in TalkBack announcing "Double tap and hold to favorite," helping users understand the purpose of the action.

Extend system widgets

Note: When you design your app's UI, use or extend system-provided widgets that are as far down Android's class hierarchy as possible. System-provided widgets that are far down the hierarchy already have most of the accessibility capabilities your app needs. It's easier to extend these system-provided widgets than to create your own from the more generic View, ViewCompat, Canvas, and CanvasCompat classes.

If you must extend View or Canvas directly, which might be necessary for a highly customized experience or a game level, see Make custom views more accessible.

This section uses the example of implementing a special type of Switch called TriSwitch while following best practices around extending system widgets. A TriSwitch object works similarly to a Switch object, except that each instance of TriSwitch allows the user to toggle among three possible states.

Extend from far down the class hierarchy

The Switch object inherits from several framework UI classes in its hierarchy:

View
 TextView
   Button
     CompoundButton
       Switch

من الأفضل أن تمتد فئة TriSwitch الجديدة مباشرةً من فئة Switch. بهذه الطريقة، يوفّر إطار عمل تسهيل الاستخدام في Android معظم إمكانات تسهيل الاستخدام التي تحتاج إليها فئة TriSwitch:

  • إجراءات تسهيل الاستخدام: معلومات للنظام حول كيفية محاكاة خدمات تسهيل الاستخدام لكل بيانات أدخلها المستخدم ممكنة يتم تنفيذه على كائن TriSwitch. (موروثة من View)
  • أحداث تسهيل الاستخدام: معلومات لخدمات تسهيل الاستخدام حول كل طريقة ممكنة لتغيير مظهر عنصر TriSwitch عند إعادة تحميل الشاشة أو تعديلها. (موروثة من View)
  • الخصائص: تفاصيل حول كل كائن TriSwitch، مثل محتويات أي نص يعرضه. (موروثة من TextView)
  • معلومات الحالة: وصف للحالة الحالية لكائن TriSwitch، مثل "تم وضع علامة" أو "لم يتم وضع علامة". (موروثة من CompoundButton)
  • الوصف النصي للحالة: شرح نصي لما تمثله كل حالة. (موروثة من Switch)

يكون سلوك Switch وفئاته الأساسية هو نفسه تقريبًا سلوك كائنات TriSwitch. لذلك، يمكن أن يركّز التنفيذ على توسيع عدد الحالات الممكنة من حالتين إلى ثلاث حالات.

تحديد الأحداث المخصّصة

عند توسيع عنصر واجهة مستخدم على مستوى النظام، من المحتمل أن تغيّر جانبًا من كيفية تفاعل المستخدمين مع عنصر واجهة المستخدم هذا. من المهم تحديد تغييرات التفاعل هذه حتى تتمكّن خدمات تسهيل الاستخدام من تعديل عنصر واجهة المستخدم في تطبيقك كما لو كان المستخدم يتفاعل مع عنصر واجهة المستخدم مباشرةً.

هناك إرشادات عامة تنص على أنّه لكل معاودة اتصال تستند إلى طريقة العرض يتم إلغاؤها، عليك أيضًا إعادة تعريف إجراء تسهيل الاستخدام المقابل من خلال إلغاء ViewCompat.replaceAccessibilityAction(). في اختبارات تطبيقك، يمكنك التحقق من سلوك هذه الإجراءات التي تم إعادة تعريفها من خلال استدعاء ViewCompat.performAccessibilityAction().

كيف يمكن أن يعمل هذا المبدأ مع كائنات TriSwitch؟

على عكس كائن Switch عادي، يؤدي النقر على كائن TriSwitch إلى التنقّل بين ثلاث حالات ممكنة. لذلك، يجب تعديل إجراء تسهيل الاستخدام ACTION_CLICK المقابل:

Kotlin

class TriSwitch(context: Context) : Switch(context) {
    // 0, 1, or 2
    var currentState: Int = 0
        private set

    init {
        updateAccessibilityActions()
    }

    private fun updateAccessibilityActions() {
        ViewCompat.replaceAccessibilityAction(this, ACTION_CLICK,
            action-label) {
            view, args -> moveToNextState()
        })
    }

    private fun moveToNextState() {
        currentState = (currentState + 1) % 3
    }
}

Java

public class TriSwitch extends Switch {
    // 0, 1, or 2
    private int currentState;

    public int getCurrentState() {
        return currentState;
    }

    public TriSwitch() {
        updateAccessibilityActions();
    }

    private void updateAccessibilityActions() {
        ViewCompat.replaceAccessibilityAction(this, ACTION_CLICK,
            action-label, (view, args) -> moveToNextState());
    }

    private void moveToNextState() {
        currentState = (currentState + 1) % 3;
    }
}

مراجع إضافية

لمزيد من المعلومات حول جعل تطبيقك أكثر سهولة في الاستخدام، اطّلِع على المراجع الإضافية التالية:

اختبارات الرموز

منشورات المدوّنات