يجب أن تكون كل شاشة في تطبيقك سريعة الاستجابة وتتكيف مع المساحة المتاحة.
يمكنك إنشاء واجهة مستخدم متجاوبة باستخدام ConstraintLayout
تسمح بتوسيع نهج مكوّن من جزء واحد ليتناسب مع العديد من الأحجام، ولكن قد تستفيد الأجهزة الأكبر حجمًا من تقسيم التنسيق إلى أجزاء متعددة. على سبيل المثال، قد ترغب في شاشة تعرض قائمة بالعناصر
بجوار قائمة تفاصيل العنصر المحدد.
يتيح
مكوِّن
SlidingPaneLayout
عرض جزأين جنبًا إلى جنب على الأجهزة الكبيرة
والأجهزة القابلة للطي مع تعديله تلقائيًا لعرض جزء واحد فقط في كل مرة على
الأجهزة الأصغر حجمًا، مثل الهواتف.
للحصول على إرشادات خاصة بالجهاز، يمكنك الاطّلاع على نظرة عامة على توافق الشاشة.
إعداد
لاستخدام SlidingPaneLayout
، يجب تضمين الاعتمادية التالية في ملف build.gradle
الخاص بتطبيقك:
رائع
dependencies { implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0" }
Kotlin
dependencies { implementation("androidx.slidingpanelayout:slidingpanelayout:1.2.0") }
إعدادات تنسيق XML
توفّر SlidingPaneLayout
تصميمًا أفقيًا من جزأين للاستخدام في المستوى الأعلى من واجهة المستخدم. يستخدم هذا التخطيط الجزء الأول كقائمة محتوى أو متصفح،
غير تابع لعرض التفاصيل الأساسية لعرض المحتوى في الجزء الآخر.
![صورة تعرض مثالاً على SlidingPaneLayout](https://developer.android.com/static/images/ui/slidingpanelayout-example.png?authuser=5&hl=ar)
SlidingPaneLayout
يستخدم SlidingPaneLayout
عرض الجزءَين لتحديد ما إذا كان سيتم عرض
الأجزاء جنبًا إلى جنب. على سبيل المثال، إذا تم قياس لوحة القائمة بحيث لا يقل حجمها عن 200 بكسل مستقل الكثافة، ويحتاج جزء التفاصيل إلى 400 وحدة بكسل مستقلة الكثافة (dp)، حينئذٍ
يعرض SlidingPaneLayout
تلقائيًا اللوحَين جنبًا إلى جنب ما دام لا يقل عن عرضه 600 وحدة بكسل مستقلة الكثافة (dp).
تتداخل طرق العرض الثانوية إذا تجاوز عرضها المجمّع العرض المتاح في SlidingPaneLayout
. في هذه الحالة، يتم توسيع طرق العرض الفرعية لملء العرض
المتاح في SlidingPaneLayout
. يمكن للمستخدم تمرير العرض العلوي خارج
الطريقة عن طريق سحبه مرة أخرى من حافة الشاشة.
إذا لم تتداخل طرق العرض، تتيح SlidingPaneLayout
استخدام معلَمة التنسيق layout_weight
في الملفات الشخصية الثانوية لتحديد كيفية تقسيم المساحة المتبقية بعد اكتمال القياس. هذه المعلمة مرتبطة بالعرض فقط.
على جهاز قابل للطيّ يتضمّن مساحة على الشاشة لإظهار كلا العرضَين جنبًا إلى جنب، يضبط "SlidingPaneLayout
" حجم لوحَين تلقائيًا بحيث يتم وضعهما على أيّ من جانبَي الطي أو المفصّلة المتداخلة. في هذه الحالة، تعتبر قيم العرض المحددة الحد الأدنى للعرض الذي يجب أن يكون على كل جانب من جانب ميزة الطي. إذا لم تتوفّر مساحة كافية للاحتفاظ بالحد الأدنى للحجم،
سينتقل SlidingPaneLayout
مرة أخرى إلى مشاهد متداخلة.
إليك مثال على استخدام SlidingPaneLayout
الذي يحتوي على
RecyclerView
كالجزء الأيمن
وFragmentContainerView
كعرض تفصيلي أساسي لعرض المحتوى من اللوحة اليمنى:
<!-- two_pane.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">
<!-- The first child view becomes the left pane. When the combined needed
width, expressed using android:layout_width, doesn't fit on-screen at
once, the right pane is permitted to overlap the left. -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list_pane"
android:layout_width="280dp"
android:layout_height="match_parent"
android:layout_gravity="start"/>
<!-- The second child becomes the right (content) pane. In this example,
android:layout_weight is used to expand this detail pane to consume
leftover available space when the entire window is wide enough to fit
the left and right pane.-->
<androidx.fragment.app.FragmentContainerView
android:id="@+id/detail_container"
android:layout_width="300dp"
android:layout_weight="1"
android:layout_height="match_parent"
android:background="#ff333333"
android:name="com.example.SelectAnItemFragment" />
</androidx.slidingpanelayout.widget.SlidingPaneLayout>
في هذا المثال، تضيف السمة android:name
على FragmentContainerView
الجزء الأولي إلى جزء التفاصيل، ما يضمن عدم ظهور جزء يسار فارغ لمستخدمي الأجهزة ذات الشاشات الكبيرة عند تشغيل التطبيق لأول مرة.
تبديل جزء التفاصيل آليًا
في مثال XML السابق، يؤدي النقر على عنصر في RecyclerView
إلى حدوث تغيير في جزء التفاصيل. عند استخدام الأجزاء، يتطلّب ذلك استخدام
FragmentTransaction
يحلّ محلّ الجزء الأيسر، ويطلب
open()
على SlidingPaneLayout
للتبديل إلى الجزء الذي أصبح مرئيًا حديثًا:
Kotlin
// A method on the Fragment that owns the SlidingPaneLayout,called by the // adapter when an item is selected. fun openDetails(itemId: Int) { childFragmentManager.commit { setReorderingAllowed(true) replace<ItemFragment>(R.id.detail_container, bundleOf("itemId" to itemId)) // If it's already open and the detail pane is visible, crossfade // between the fragments. if (binding.slidingPaneLayout.isOpen) { setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) } } binding.slidingPaneLayout.open() }
Java
// A method on the Fragment that owns the SlidingPaneLayout, called by the // adapter when an item is selected. void openDetails(int itemId) { Bundle arguments = new Bundle(); arguments.putInt("itemId", itemId); FragmentTransaction ft = getChildFragmentManager().beginTransaction() .setReorderingAllowed(true) .replace(R.id.detail_container, ItemFragment.class, arguments); // If it's already open and the detail pane is visible, crossfade // between the fragments. if (binding.getSlidingPaneLayout().isOpen()) { ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); } ft.commit(); binding.getSlidingPaneLayout().open(); }
لا يستدعي هذا الرمز تحديدًا
addToBackStack()
في FragmentTransaction
. هذا يتجنب بناء تكديس خلفية في
جزء التفاصيل.
تنفيذ مكوِّن التنقل
تستخدم الأمثلة في هذه الصفحة SlidingPaneLayout
مباشرةً، وتتطلّب منك إدارة معاملات التجزئة يدويًا. ومع ذلك، يوفّر
مكوِّن التنقّل تنفيذًا مُعَدًّا مسبقًا
لتنسيق من جزأين من خلال
AbstractListDetailFragment
،
وهي فئة من واجهات برمجة التطبيقات تستخدم SlidingPaneLayout
في الخيارات المتقدمة لإدارة القائمة
وأجزاء التفاصيل.
يتيح لك ذلك تبسيط ضبط تنسيق XML. بدلاً من الإعلان صراحةً عن SlidingPaneLayout
وكلتا الجزأين، يحتاج التنسيق فقط إلى FragmentContainerView
للاحتفاظ بعملية تنفيذ AbstractListDetailFragment
:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/two_pane_container"
<!-- The name of your AbstractListDetailFragment implementation.-->
android:name="com.example.testapp.TwoPaneFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
<!-- The navigation graph for your detail pane.-->
app:navGraph="@navigation/two_pane_navigation" />
</FrameLayout>
نفِّذ السمة
onCreateListPaneView()
وonListPaneViewCreated()
لتوفير طريقة عرض مخصّصة لجزء القائمة. بالنسبة إلى جزء التفاصيل،
يستخدم AbstractListDetailFragment
NavHostFragment
.
وهذا يعني أنّه يمكنك تحديد رسم بياني للتنقّل يحتوي فقط على الوجهات التي سيتم عرضها في جزء التفاصيل. بعد ذلك، يمكنك استخدام
NavController
لتبديل جزء التفاصيل بين الوجهات في الرسم البياني المستقل للتنقّل:
Kotlin
fun openDetails(itemId: Int) { val navController = navHostFragment.navController navController.navigate( // Assume the itemId is the android:id of a destination in the graph. itemId, null, NavOptions.Builder() // Pop all destinations off the back stack. .setPopUpTo(navController.graph.startDestination, true) .apply { // If it's already open and the detail pane is visible, // crossfade between the destinations. if (binding.slidingPaneLayout.isOpen) { setEnterAnim(R.animator.nav_default_enter_anim) setExitAnim(R.animator.nav_default_exit_anim) } } .build() ) binding.slidingPaneLayout.open() }
Java
void openDetails(int itemId) { NavController navController = navHostFragment.getNavController(); NavOptions.Builder builder = new NavOptions.Builder() // Pop all destinations off the back stack. .setPopUpTo(navController.getGraph().getStartDestination(), true); // If it's already open and the detail pane is visible, crossfade between // the destinations. if (binding.getSlidingPaneLayout().isOpen()) { builder.setEnterAnim(R.animator.nav_default_enter_anim) .setExitAnim(R.animator.nav_default_exit_anim); } navController.navigate( // Assume the itemId is the android:id of a destination in the graph. itemId, null, builder.build() ); binding.getSlidingPaneLayout().open(); }
يجب عدم أن تكون الوجهات المدرجة في الرسم البياني للتنقل في لوحة التفاصيل معروضة في أي رسم بياني خارجي أو على مستوى التطبيق. مع ذلك، يجب إرفاق أيّ روابط لصفحات معيّنة ضمن الرسم البياني للتنقّل في جزء التفاصيل بالوجهة التي تستضيف SlidingPaneLayout
. يساعد ذلك في ضمان انتقال الروابط الخارجية لصفحات في التطبيق أولاً إلى وجهة SlidingPaneLayout
، ثم الانتقال إلى وجهة لوحة التفاصيل الصحيحة.
راجِع مثال TwoPaneFragment للتنفيذ الكامل لتنسيق من جزأين باستخدام مكوِّن التنقل.
الدمج مع زر الرجوع في النظام
على الأجهزة الأصغر حجمًا التي تتداخل فيها أجزاء القائمة وأجزاء التفاصيل، تأكَّد من أنّ زر الرجوع في النظام يعيد المستخدم من جزء التفاصيل إلى جزء القائمة. يمكنك إجراء ذلك من خلال توفير معلومات مخصّصة للرجوع إلى الصفحة السابقة وربط OnBackPressedCallback
بالحالة الحالية لـ SlidingPaneLayout
:
Kotlin
class TwoPaneOnBackPressedCallback( private val slidingPaneLayout: SlidingPaneLayout ) : OnBackPressedCallback( // Set the default 'enabled' state to true only if it is slidable, such as // when the panes overlap, and open, such as when the detail pane is // visible. slidingPaneLayout.isSlideable && slidingPaneLayout.isOpen ), SlidingPaneLayout.PanelSlideListener { init { slidingPaneLayout.addPanelSlideListener(this) } override fun handleOnBackPressed() { // Return to the list pane when the system back button is tapped. slidingPaneLayout.closePane() } override fun onPanelSlide(panel: View, slideOffset: Float) { } override fun onPanelOpened(panel: View) { // Intercept the system back button when the detail pane becomes // visible. isEnabled = true } override fun onPanelClosed(panel: View) { // Disable intercepting the system back button when the user returns to // the list pane. isEnabled = false } }
Java
class TwoPaneOnBackPressedCallback extends OnBackPressedCallback implements SlidingPaneLayout.PanelSlideListener { private final SlidingPaneLayout mSlidingPaneLayout; TwoPaneOnBackPressedCallback(@NonNull SlidingPaneLayout slidingPaneLayout) { // Set the default 'enabled' state to true only if it is slideable, such // as when the panes overlap, and open, such as when the detail pane is // visible. super(slidingPaneLayout.isSlideable() && slidingPaneLayout.isOpen()); mSlidingPaneLayout = slidingPaneLayout; slidingPaneLayout.addPanelSlideListener(this); } @Override public void handleOnBackPressed() { // Return to the list pane when the system back button is tapped. mSlidingPaneLayout.closePane(); } @Override public void onPanelSlide(@NonNull View panel, float slideOffset) { } @Override public void onPanelOpened(@NonNull View panel) { // Intercept the system back button when the detail pane becomes // visible. setEnabled(true); } @Override public void onPanelClosed(@NonNull View panel) { // Disable intercepting the system back button when the user returns to // the list pane. setEnabled(false); } }
يمكنك إضافة الاستدعاء إلى
OnBackPressedDispatcher
باستخدام
addCallback()
:
Kotlin
class TwoPaneFragment : Fragment(R.layout.two_pane) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val binding = TwoPaneBinding.bind(view) // Connect the SlidingPaneLayout to the system back button. requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, TwoPaneOnBackPressedCallback(binding.slidingPaneLayout)) // Set up the RecyclerView adapter. } }
Java
class TwoPaneFragment extends Fragment { public TwoPaneFragment() { super(R.layout.two_pane); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { TwoPaneBinding binding = TwoPaneBinding.bind(view); // Connect the SlidingPaneLayout to the system back button. requireActivity().getOnBackPressedDispatcher().addCallback( getViewLifecycleOwner(), new TwoPaneOnBackPressedCallback(binding.getSlidingPaneLayout())); // Set up the RecyclerView adapter. } }
وضع القفل
يتيح لك SlidingPaneLayout
دائمًا الاتصال يدويًا بـ open()
وclose()
للانتقال بين جزء القائمة وعرض التفاصيل على الهواتف. ليس لهذه الطرق أي تأثير
إذا كان كلا الجزأين مرئيين ولا يتداخلان.
عندما تتداخل القائمة مع جزء التفاصيل، يمكن للمستخدمين التمرير سريعًا في كلا الاتجاهين تلقائيًا، والتبديل بين الجزأين بحرية حتى في حال عدم استخدام التنقُّل بالإيماءات. يمكنك التحكم في اتجاه التمرير السريع من خلال
ضبط وضع القفل في SlidingPaneLayout
:
Kotlin
binding.slidingPaneLayout.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED
Java
binding.getSlidingPaneLayout().setLockMode(SlidingPaneLayout.LOCK_MODE_LOCKED);
مزيد من المعلومات
لمعرفة المزيد حول تصميم التخطيطات لمختلف أشكال الأجهزة، راجع الوثائق التالية: