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

روش Compose را امتحان کنید
Jetpack Compose جعبه ابزار UI توصیه شده برای اندروید است. نحوه کار با طرح‌بندی‌ها در 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 را نشان می دهد
شکل 1. نمونه ای از طرح بندی ایجاد شده با SlidingPaneLayout .

SlidingPaneLayout از عرض دو پنجره استفاده می کند تا مشخص کند که آیا پنجره ها در کنار هم نشان داده شوند یا خیر. به عنوان مثال، اگر اندازه صفحه لیست حداقل 200 dp باشد و صفحه جزئیات به 400 dp نیاز دارد، SlidingPaneLayout به طور خودکار دو صفحه را در کنار هم نشان می دهد تا زمانی که حداقل 600 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 باعث تغییر در قسمت جزئیات می شود. هنگام استفاده از قطعات، این به یک 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 فراخوانی نمی کند. این از ایجاد پشته پشتی در قسمت جزئیات جلوگیری می کند.

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

مثال TwoPaneFragment را برای اجرای کامل یک طرح بندی دو صفحه ای با استفاده از مولفه Navigation ببینید.

با دکمه بازگشت سیستم یکپارچه شوید

در دستگاه‌های کوچک‌تری که فهرست و جزییات با هم همپوشانی دارند، مطمئن شوید که دکمه بازگشت سیستم کاربر را از صفحه جزئیات به صفحه فهرست برمی‌گرداند. این کار را با ارائه پیمایش برگشت سفارشی و اتصال 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);

بیشتر بدانید

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

منابع اضافی