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

تجربة طريقة Compose
‫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 وحدة بكسل مستقلة على الأقل.

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

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

لمعرفة المزيد عن تصميم التنسيقات لمختلف أشكال الأجهزة، اطّلِع على المستندات التالية:

مراجع إضافية