إنشاء تنسيق من لوحتين

تجربة طريقة الإنشاء
إنّ Jetpack Compose هي مجموعة أدوات واجهة المستخدم المقترَحة لنظام التشغيل Android. تعرَّف على كيفية استخدام التنسيقات في Compose.

يجب أن تكون كل شاشة في تطبيقك سريعة الاستجابة وأن تتكيّف مع المساحة المتاحة. يمكنك إنشاء واجهة مستخدم سريعة الاستجابة باستخدام 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
الشكل 1. مثال على تنسيق تم إنشاؤه باستخدام SlidingPaneLayout.

يستخدم SlidingPaneLayout عرض الجزءين لتحديد ما إذا كان سيتم عرض الأجزاء جنبًا إلى جنب. على سبيل المثال، إذا تم قياس جزء القائمة بحد أدنى للحجم يبلغ 200 وحدة بكسل مستقلة الكثافة ويحتاج جزء التفاصيل إلى 400 بكسل مستقل الكثافة، يعرض SlidingPaneLayout تلقائيًا الجزءين جنبًا إلى جنب ما دام العرض متاحًا لا يقل عن 600 بكسل مستقل الكثافة.

يتداخل طرق العرض الثانوية إذا كان عرضها المجمّع يتجاوز العرض المتاح في 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);

مزيد من المعلومات

لمعرفة المزيد حول تصميم التخطيطات لعوامل الشكل المختلفة، راجع الوثائق التالية:

مراجع إضافية