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

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

يجب أن تكون كل شاشة في تطبيقك متجاوبة وتتكيّف مع المساحة المتاحة. يمكنك إنشاء واجهة مستخدم سريعة الاستجابة باستخدام 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 dp وكانت مساحة عرض التفاصيل تحتاج إلى 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. ويؤدي ذلك إلى تجنُّب إنشاء حزمة خلفية في pane.

تستخدِم الأمثلة الواردة في هذه الصفحة 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 للتنفيذ الكامل لتنسيق نوافذ متعدّدة باستخدام مكوّن Navigation.

الدمج مع زر الرجوع في النظام

على الأجهزة الأصغر حجمًا التي تتداخل فيها لوحتا القائمة والتفاصيل، تأكَّد من أنّ زر الرجوع في النظام ينقل المستخدم من لوحة التفاصيل إلى لوحة القائمة. يمكنك إجراء ذلك من خلال توفير ميزة مخصّصة للتنقّل للخلف وربط 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);

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

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

مصادر إضافية