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

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

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

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

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

ضبط إعدادات الجهاز

لاستخدام SlidingPaneLayout، يجب تضمين الاعتمادية التالية في قسم ملف build.gradle:

Groovy

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 وحدة بكسل مستقلة الكثافة (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);

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

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

مصادر إضافية