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

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

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

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

مصادر إضافية