إنشاء تنقّل متجاوب

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

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

ينفِّذ مكوّن التنقّل في Jetpack مبادئ التنقّل ويسهّل تطوير التطبيقات التي تتضمّن واجهات مستخدم متجاوبة/متكيّفة.

الشكل 1. شاشات موسّعة ومتوسطة وصغيرة مع درج التنقّل وشريط التنقل والشريط السفلي

واجهة مستخدم تتيح التنقّل بسلاسة

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

فئة حجم النافذة عناصر قليلة عناصر متعددة
العرض المكثّف شريط التنقّل السفلي لائحة التنقّل (الحافة الأمامية أو أسفل الشاشة)
عرض متوسط شريط التنقّل لائحة التنقّل (الحافة الأمامية)
العرض الموسّع شريط التنقّل لائحة التنقّل الثابتة (الحافة الأمامية)

يمكن أن تكون ملفات موارد التنسيق مؤهَّلة من خلال نقاط التوقف لفئة حجم النافذة لاستخدام عناصر تنقّل مختلفة لأبعاد عرض مختلفة.

<!-- res/layout/main_activity.xml -->

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        ... />

    <!-- Content view(s) -->
</androidx.constraintlayout.widget.ConstraintLayout>

<!-- res/layout-w600dp/main_activity.xml -->

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.navigationrail.NavigationRailView
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        ... />

    <!-- Content view(s) -->
</androidx.constraintlayout.widget.ConstraintLayout>

<!-- res/layout-w1240dp/main_activity.xml -->

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.navigation.NavigationView
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        ... />

    <!-- Content view(s) -->
</androidx.constraintlayout.widget.ConstraintLayout>

وجهات المحتوى المتجاوبة

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

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

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

يؤدي الانتقال إلى وجهات المحتوى كأثر جانبي لتغييرات حجم النافذة إلى حدوث المشاكل التالية:

  • قد تظهر الوجهة القديمة (لحجم النافذة السابق) لعدة ثوانٍ قبل الانتقال إلى الوجهة الجديدة.
  • للحفاظ على إمكانية العكس (على سبيل المثال، عند طي الجهاز وتفكيكه)، يجب توفير إمكانية التنقّل في كل حجم نافذة.
  • قد يكون من الصعب الحفاظ على حالة التطبيق بين الوجهات، لأنّه يمكن أن يؤدي التنقّل إلى فقدان الحالة عند إزالة الحزمة من "المخطّط الخلفي".

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

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

وجهات المحتوى التي تتضمّن تنسيقات بديلة

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

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

يتيح لك SlidingPaneLayout إنشاء وجهة تنقّل واحدة تعرض لوحتَي محتوى جنبًا إلى جنب على الشاشات الكبيرة، ولكن لوحة واحدة فقط في كل مرة على الشاشات الصغيرة، مثل الهواتف العادية.

<!-- Single destination for list and detail. -->

<navigation ...>

    <!-- Fragment that implements SlidingPaneLayout. -->
    <fragment
        android:id="@+id/article_two_pane"
        android:name="com.example.app.ListDetailTwoPaneFragment" />

    <!-- Other destinations... -->
</navigation>

اطّلِع على إنشاء تصميم من لوحتَين لمعرفة تفاصيل عن تنفيذ SlidingPaneLayout لعرض التنسيق على شكل قائمة مع تفاصيل.

رسم بياني واحد للتنقّل

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

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

مضيف التنقّل المُدمَج

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

لتنفيذ هذا النوع من التنقّل الفرعي، اجعل لوحة التفاصيل مضيفًا لتنقّل متداخل مع رسم بياني للتنقّل يحدّد الوجهات التي يتم الوصول إليها من لوحة التفاصيل:

<!-- layout/two_pane_fragment.xml -->

<androidx.slidingpanelayout.widget.SlidingPaneLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/sliding_pane_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/list_pane"
        android:layout_width="280dp"
        android:layout_height="match_parent"
        android:layout_gravity="start"/>

    <!-- Detail pane is a nested navigation host. Its graph is not connected
         to the main graph that contains the two_pane_fragment destination. -->
    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/detail_pane"
        android:layout_width="300dp"
        android:layout_weight="1"
        android:layout_height="match_parent"
        android:name="androidx.navigation.fragment.NavHostFragment"
        app:navGraph="@navigation/detail_pane_nav_graph" />
</androidx.slidingpanelayout.widget.SlidingPaneLayout>

يختلف ذلك عن مخطّط التنقّل المُدمَج لأنّ مخطّط التنقّل لNavHost المُدمَج غير متصل بمخطّط التنقّل الرئيسي، أي أنّه لا يمكنك الانتقال مباشرةً من الوجهات في مخطّط إلى الوجهات في المخطّط الآخر.

لمزيد من المعلومات، يُرجى الاطّلاع على الرسوم البيانية المتداخلة للتنقّل.

الحالة المحفوظة

لتقديم وجهات محتوى متجاوبة، يجب أن يحتفظ تطبيقك بحالة التطبيق عند تدوير الجهاز أو طيّه أو تغيير حجم نافذة التطبيق. بشكلٍ تلقائي، يؤدي تغيير الإعدادات إلى إعادة إنشاء أنشطة التطبيق وأجزاءه وعرضه الهرمي. إنّ الطريقة المقترَحة لحفظ حالة واجهة المستخدم هي باستخدام علامة ViewModel، والتي تظل محفوظة عند تغيير الإعدادات. (اطّلِع على حفظ حالات واجهة المستخدِم ).

يجب أن يكون بالإمكان التراجع عن تغييرات الحجم، على سبيل المثال، عندما يدير المستخدم الجهاز ثم يعيد توجيهه.

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

نطاقات ViewModel

يحدّد دليل المطوّر نقل البيانات إلى مكوّن التنقّل بنية نشاط واحد يتمّ فيه تنفيذ الوجهات ك شرائح ويتمّ تنفيذ نماذج بياناتها باستخدام ViewModel.

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

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

لمشاركة حالة واجهة المستخدم بين الأجزاء، حدِّد نطاق ViewModel للنشاط من خلال استدعاء activityViewModels() في الأجزاء (العنصر المكافئ لمحاولة Activity هو viewModels() فقط). يتيح ذلك للنشاط وأي أجزاء مرتبطة به مشاركة مثيل ViewModel. ومع ذلك، في بنية النشاط الواحد، يستمر نطاق ViewModel بشكل فعّال طوال مدة التطبيق، لذا يبقى ViewModel في الذاكرة حتى إذا لم يكن هناك شرائح تستخدمه.

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

بدلاً من ذلك، يمكنك حصر ViewModel في رسم بياني للتنقّل في NavController الحالية. أنشئ رسمًا بيانيًا مدمجًا للتنقّل لتضمين الوجهات التي تشكّل جزءًا من مسار الدفع. بعد ذلك، في كلّ من وجهة المقاطع هذه، استخدِم وكيل الموقع navGraphViewModels()، وأرسِل رقم تعريف الرسم البياني للتنقّل للحصول على ViewModel المشترَك. يضمن ذلك أنّه بعد خروج المستخدم من مسار الدفع وخروج الرسم البياني المتداخل للتنقّل من النطاق، يتم تجاهل المثيل المقابل من ViewModel ولن يتم استخدامه في عملية الدفع التالية.

المجال وكيل الموقع يمكن مشاركة ViewModel مع
الجزء Fragment.viewModels() المقتطف فقط
النشاط Activity.viewModels() أو Fragment.activityViewModels() النشاط وجميع الأجزاء المرتبطة به
الرسم البياني للتنقّل Fragment.navGraphViewModels() جميع الأجزاء في الرسم البياني نفسه للتنقّل

يُرجى العلم أنّه في حال استخدام مضيف تنقّل متداخل (راجِع قسم مضيف التنقّل المتداخل)، لا يمكن للوجهات في هذا المضيف مشاركة مثيلات ViewModel مع الوجهات خارج المضيف عند استخدام navGraphViewModels() لأنّ الرسوم البيانية غير متصلة. في هذه الحالة، يمكنك استخدام نطاق النشاط بدلاً من ذلك.

مصادر إضافية