ایجاد چیدمان دو قاب

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

کاتلین

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 را ببینید.

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

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

بیشتر بدانید

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

منابع بیشتر