Membuat tata letak dua panel

Coba cara Compose
Jetpack Compose adalah toolkit UI yang direkomendasikan untuk Android. Pelajari cara menggunakan tata letak di Compose.

Setiap layar di aplikasi Anda harus responsif dan beradaptasi dengan ruang yang tersedia. Anda dapat mem-build UI responsif dengan ConstraintLayout yang memungkinkan pendekatan satu panel diskalakan ke berbagai ukuran, tetapi perangkat yang lebih besar mungkin sebaiknya dibagi tata letak menjadi beberapa panel. Misalnya, Anda mungkin ingin layar menampilkan daftar item di samping daftar detail item yang dipilih.

Komponen SlidingPaneLayout mendukung menampilkan dua panel secara berdampingan di perangkat yang lebih besar dan perangkat foldable sekaligus otomatis beradaptasi untuk hanya menampilkan satu panel pada satu waktu di perangkat yang lebih kecil seperti ponsel.

Untuk panduan khusus perangkat, lihat ringkasan kompatibilitas layar.

Penyiapan

Untuk menggunakan SlidingPaneLayout, sertakan dependensi berikut di file build.gradle aplikasi Anda:

Groovy

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

Kotlin

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

Mengonfigurasi tata letak XML

SlidingPaneLayout menyediakan tata letak dua panel horizontal untuk digunakan di bagian atas UI. Tata letak ini menggunakan panel pertama sebagai daftar konten atau browser, di bawah tampilan detail utama guna menampilkan konten di panel lainnya.

Gambar yang menunjukkan contoh SlidingPaneLayout
Gambar 1. Contoh tata letak yang dibuat dengan SlidingPaneLayout.

SlidingPaneLayout menggunakan lebar dua panel untuk menentukan apakah menampilkan panel secara berdampingan atau tidak. Misalnya, jika panel daftar diukur memiliki ukuran minimum 200 dp dan panel detail memerlukan 400 dp, maka SlidingPaneLayout akan otomatis menampilkan dua panel secara berdampingan selama lebar yang tersedia minimal 600 dp.

Tampilan turunan akan tumpang tindih jika lebar gabungannya melebihi lebar yang tersedia di SlidingPaneLayout. Dalam hal ini, tampilan turunan diperluas untuk mengisi lebar yang tersedia di SlidingPaneLayout. Pengguna dapat menggeser keluar tampilan paling atas dengan menariknya kembali dari tepi layar.

Jika tampilan tidak tumpang tindih, SlidingPaneLayout mendukung penggunaan parameter tata letak layout_weight pada tampilan turunan untuk menentukan cara membagi ruang yang tersisa setelah pengukuran selesai. Parameter ini hanya relevan untuk lebar.

Pada perangkat foldable yang memiliki ruang pada layar untuk menampilkan kedua tampilan secara berdampingan, SlidingPaneLayout otomatis menyesuaikan ukuran dua panel sehingga diposisikan di kedua sisi lipatan atau engsel yang tumpang tindih. Dalam hal ini, lebar yang disetel dianggap sebagai lebar minimum yang harus ada di setiap sisi fitur lipat. Jika tidak ada cukup ruang untuk mempertahankan ukuran minimum tersebut, SlidingPaneLayout akan beralih kembali ke tampilan yang tumpang tindih.

Berikut adalah contoh penggunaan SlidingPaneLayout yang memiliki RecyclerView sebagai panel kirinya dan FragmentContainerView sebagai tampilan detail utamanya untuk menampilkan konten dari panel kiri:

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

Dalam contoh ini, atribut android:name pada FragmentContainerView menambahkan fragmen awal ke panel detail, sehingga memastikan pengguna di perangkat layar besar tidak melihat panel kanan yang kosong saat aplikasi pertama kali diluncurkan.

Menukarkan panel detail secara terprogram

Pada contoh XML sebelumnya, mengetuk elemen dalam RecyclerView akan memicu perubahan pada panel detail. Saat menggunakan fragmen, tindakan ini memerlukan FragmentTransaction yang menggantikan panel kanan, dengan memanggil open() di SlidingPaneLayout untuk beralih ke fragmen yang baru saja terlihat:

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

Kode ini secara khusus tidak memanggil addToBackStack() pada FragmentTransaction. Tindakan ini akan menghindari pembuatan data sebelumnya di panel detail.

Contoh di halaman ini menggunakan SlidingPaneLayout secara langsung dan mengharuskan Anda mengelola transaksi fragmen secara manual. Namun, Komponen navigasi menyediakan implementasi bawaan tata letak dua panel melalui AbstractListDetailFragment, class API yang menggunakan SlidingPaneLayout di balik layar untuk mengelola panel daftar dan detail.

Hal ini memungkinkan Anda menyederhanakan konfigurasi tata letak XML. Daripada secara eksplisit mendeklarasikan SlidingPaneLayout dan kedua panel, tata letak Anda hanya memerlukan FragmentContainerView untuk menyimpan implementasi 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>

Implementasikan onCreateListPaneView() dan onListPaneViewCreated() untuk memberikan tampilan kustom bagi panel daftar Anda. Untuk panel detail, AbstractListDetailFragment menggunakan NavHostFragment. Ini berarti Anda dapat menentukan grafik navigasi yang hanya berisi tujuan yang akan ditampilkan di panel detail. Kemudian, Anda dapat menggunakan NavController untuk menukar panel detail antar-tujuan dalam grafik navigasi mandiri:

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

Tujuan di grafik navigasi panel detail tidak boleh ada di grafik navigasi tingkat aplikasi luar. Namun, deep link apa pun dalam grafik navigasi panel detail harus dilampirkan ke tujuan yang menghosting SlidingPaneLayout. Hal ini membantu memastikan bahwa deep link eksternal terlebih dahulu menavigasi ke tujuan SlidingPaneLayout, lalu menavigasi ke tujuan panel detail yang benar.

Lihat contoh TwoPaneFragment untuk mengetahui implementasi lengkap tata letak dua panel menggunakan komponen Navigasi.

Mengintegrasikan dengan tombol kembali sistem

Pada perangkat yang lebih kecil, dengan panel daftar dan detail saling tumpang tindih, pastikan tombol kembali sistem membawa pengguna dari panel detail kembali ke panel daftar. Lakukan hal ini dengan memberikan navigasi kembali kustom dan menghubungkan OnBackPressedCallback ke status SlidingPaneLayout saat ini:

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

Anda dapat menambahkan callback ke OnBackPressedDispatcher menggunakan 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.
    }
}

Mode kunci

SlidingPaneLayout selalu memungkinkan Anda memanggil open() dan close() secara manual untuk transisi antara panel daftar dan detail pada ponsel. Metode tersebut tidak berpengaruh jika kedua panel terlihat dan tidak tumpang tindih.

Saat panel daftar dan detail tumpang tindih, pengguna dapat melakukan gestur geser ke kedua arah secara default, dengan bebas beralih antar dua panel bahkan saat tidak menggunakan navigasi gestur. Anda dapat mengontrol arah gestur geser dengan menyetel mode kunci SlidingPaneLayout:

Kotlin

binding.slidingPaneLayout.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED

Java

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

Pelajari lebih lanjut

Untuk mempelajari cara mendesain tata letak untuk berbagai faktor bentuk lebih lanjut, lihat dokumentasi berikut:

Referensi lainnya