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

تجربة طريقة الإنشاء
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);

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

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

مراجع إضافية