یک طرح دو صفحه ای ایجاد کنید

روش نوشتن را امتحان کنید
Jetpack Compose ابزار رابط کاربری پیشنهادی برای اندروید است. یاد بگیرید که چگونه با طرح‌بندی‌ها در Compose کار کنید.

هر صفحه در برنامه شما باید واکنش‌گرا باشد و با فضای موجود سازگار شود. شما می‌توانید با ConstraintLayout یک رابط کاربری واکنش‌گرا بسازید که به یک رویکرد تک‌صفحه‌ای اجازه می‌دهد تا در اندازه‌های مختلف مقیاس‌پذیر باشد، اما دستگاه‌های بزرگتر ممکن است از تقسیم طرح‌بندی به چندین صفحه بهره‌مند شوند. به عنوان مثال، ممکن است بخواهید یک صفحه فهرستی از موارد را در کنار فهرستی از جزئیات مورد انتخاب شده نشان دهد.

کامپوننت SlidingPaneLayout از نمایش دو پنل در کنار هم در دستگاه‌های بزرگتر و تاشو پشتیبانی می‌کند، در حالی که به طور خودکار طوری تنظیم می‌شود که در دستگاه‌های کوچکتر مانند تلفن‌ها فقط یک پنل را در یک زمان نشان دهد.

برای راهنمایی‌های مربوط به دستگاه، به نمای کلی سازگاری صفحه نمایش مراجعه کنید.

راه‌اندازی

برای استفاده از SlidingPaneLayout ، وابستگی زیر را در فایل build.gradle برنامه خود وارد کنید:

شیار

dependencies {
    implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0"
}

کاتلین

dependencies {
    implementation("androidx.slidingpanelayout:slidingpanelayout:1.2.0")
}

پیکربندی طرح‌بندی XML

SlidingPaneLayout یک طرح‌بندی افقی دو قسمتی برای استفاده در سطح بالای رابط کاربری ارائه می‌دهد. این طرح‌بندی از اولین قسمت به عنوان یک لیست محتوا یا یک مرورگر استفاده می‌کند و تابع نمای جزئیات اصلی برای نمایش محتوا در قسمت دیگر است.

تصویری که نمونه‌ای از SlidingPaneLayout را نشان می‌دهد
شکل ۱. نمونه‌ای از طرح‌بندی ایجاد شده با SlidingPaneLayout .

SlidingPaneLayout از عرض دو صفحه برای تعیین اینکه آیا صفحه‌ها را در کنار هم نشان دهد یا خیر، استفاده می‌کند. برای مثال، اگر صفحه فهرست حداقل اندازه ۲۰۰ dp داشته باشد و صفحه جزئیات به ۴۰۰ dp نیاز داشته باشد، SlidingPaneLayout به طور خودکار دو صفحه را در کنار هم نشان می‌دهد، البته تا زمانی که حداقل ۶۰۰ dp عرض داشته باشد.

اگر عرض ترکیبی نماهای فرزند از عرض موجود در SlidingPaneLayout بیشتر شود، همپوشانی ایجاد می‌شود. در این حالت، نماهای فرزند گسترش می‌یابند تا عرض موجود در SlidingPaneLayout را پر کنند. کاربر می‌تواند با کشیدن بالاترین نما از لبه صفحه به عقب، آن را از سر راه خود کنار بزند.

اگر نماها همپوشانی نداشته باشند، SlidingPaneLayout از استفاده از پارامتر layout 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 باعث ایجاد تغییر در صفحه جزئیات می‌شود. هنگام استفاده از fragmentها، این امر به یک FragmentTransaction نیاز دارد که صفحه سمت راست را جایگزین کند و تابع open() را در SlidingPaneLayout فراخوانی کند تا به قطعه جدید قابل مشاهده تغییر کند:

کاتلین

// 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()
}

جاوا

// 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 فراخوانی نمی‌کند. این کار از ساخت یک back stack در صفحه جزئیات جلوگیری می‌کند.

مثال‌های این صفحه مستقیماً SlidingPaneLayout استفاده می‌کنند و شما را ملزم می‌کنند که تراکنش‌های fragment را به صورت دستی مدیریت کنید. با این حال، کامپوننت Navigation یک پیاده‌سازی از پیش ساخته شده از یک طرح دو قسمتی را از طریق AbstractListDetailFragment ، یک کلاس API که از یک 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 برای جابجایی صفحه جزئیات خود بین مقاصد موجود در گراف ناوبری مستقل استفاده کنید:

کاتلین

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()
}

جاوا

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 و سپس به مقصد صحیح پنل جزئیات هدایت شوند.

برای پیاده‌سازی کامل یک طرح‌بندی دو قسمتی با استفاده از کامپوننت Navigation، به مثال TwoPaneFragment مراجعه کنید.

با دکمه بازگشت سیستم ادغام شوید

در دستگاه‌های کوچک‌تر که پنل‌های لیست و جزئیات با هم همپوشانی دارند، مطمئن شوید که دکمه‌ی بازگشت سیستم، کاربر را از پنل جزئیات به پنل لیست برمی‌گرداند. این کار را با ارائه‌ی ناوبری بازگشت سفارشی و اتصال یک OnBackPressedCallback به وضعیت فعلی SlidingPaneLayout انجام دهید:

کاتلین

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
    }
}

جاوا

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);
    }
}

شما می‌توانید با استفاده از addCallback() ‎، فراخوانی برگشتی را به OnBackPressedDispatcher اضافه کنید:

کاتلین

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.
    }
}

جاوا

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 ، جهت سوایپ را کنترل کنید:

کاتلین

binding.slidingPaneLayout.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED

جاوا

binding.getSlidingPaneLayout().setLockMode(SlidingPaneLayout.LOCK_MODE_LOCKED);

بیشتر بدانید

برای کسب اطلاعات بیشتر در مورد طراحی طرح‌بندی برای عوامل شکل مختلف، به مستندات زیر مراجعه کنید:

منابع اضافی