สร้างเลย์เอาต์แบบ 2 แผง

ลองใช้วิธีแบบ Compose
Jetpack Compose เป็นชุดเครื่องมือ UI ที่แนะนำสำหรับ Android ดูวิธีทำงานกับเลย์เอาต์ใน Compose

ทุกหน้าจอในแอปต้องตอบสนองและปรับให้เข้ากับพื้นที่ที่มี คุณสามารถสร้าง UI ที่ปรับเปลี่ยนตามอุปกรณ์ด้วย ConstraintLayout ซึ่งช่วยให้แนวทางแบบบานหน้าต่างเดียว ปรับขนาดให้มีหลายขนาดได้ แต่อุปกรณ์ขนาดใหญ่อาจได้รับประโยชน์จากการแยก เลย์เอาต์ออกเป็นหลายบานหน้าต่าง ตัวอย่างเช่น คุณอาจต้องการให้หน้าจอแสดง รายการข้างรายการรายละเอียดของรายการที่เลือก

คอมโพเนนต์ SlidingPaneLayout รองรับการแสดง 2 บานหน้าต่างแบบเคียงข้างกันในอุปกรณ์ขนาดใหญ่และ อุปกรณ์พับได้ พร้อมทั้งปรับให้แสดงเพียงบานหน้าต่างเดียวในแต่ละครั้งโดยอัตโนมัติใน อุปกรณ์ขนาดเล็ก เช่น โทรศัพท์

ดูคำแนะนำเฉพาะอุปกรณ์ได้ที่ภาพรวมความเข้ากันได้ของหน้าจอ

ตั้งค่า

หากต้องการใช้ SlidingPaneLayout ให้รวมทรัพยากร Dependency ต่อไปนี้ในไฟล์ build.gradle ของแอป

ดึงดูด

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

Kotlin

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

การกำหนดค่าเลย์เอาต์ XML

SlidingPaneLayout มีเลย์เอาต์ 2 บานหน้าต่างแนวนอนสำหรับใช้ที่ระดับบนสุด ของ UI เลย์เอาต์นี้ใช้บานหน้าต่างแรกเป็นรายการเนื้อหาหรือเบราว์เซอร์ ซึ่งเป็นส่วนย่อยของมุมมองรายละเอียดหลักสำหรับการแสดงเนื้อหาในบานหน้าต่างอื่นๆ

รูปภาพที่แสดงตัวอย่าง SlidingPaneLayout
รูปที่ 1 ตัวอย่างเลย์เอาต์ที่สร้างด้วย SlidingPaneLayout

SlidingPaneLayout ใช้ความกว้างของ 2 บานหน้าต่างเพื่อพิจารณาว่าจะแสดง บานหน้าต่างแบบเคียงข้างกันหรือไม่ เช่น หากวัดแผงรายการแล้วพบว่ามีขนาดขั้นต่ำ 200 dp และแผงรายละเอียดต้องใช้ 400 dp SlidingPaneLayout จะแสดงแผงทั้ง 2 ข้างเคียงกันโดยอัตโนมัติตราบใดที่มีความกว้างอย่างน้อย 600 dp

มุมมองของบุตรหลานจะซ้อนทับกันหากความกว้างรวมเกินความกว้างที่ใช้ได้ใน SlidingPaneLayout ในกรณีนี้ มุมมองย่อยจะขยายเพื่อเติมเต็มความกว้างที่มีใน SlidingPaneLayout ผู้ใช้สามารถเลื่อนมุมมองบนสุดออกไป ได้โดยลากกลับจากขอบของหน้าจอ

หากมุมมองไม่ทับซ้อนกัน SlidingPaneLayout จะรองรับการใช้พารามิเตอร์เลย์เอาต์ layout_weight ในมุมมองย่อยเพื่อกำหนดวิธีแบ่งพื้นที่ที่เหลือ หลังจากวัดเสร็จแล้ว พารามิเตอร์นี้เกี่ยวข้องกับความกว้างเท่านั้น

ในอุปกรณ์แบบพับได้ที่มีพื้นที่บนหน้าจอเพื่อแสดงทั้ง 2 มุมมองแบบเคียงข้างกัน SlidingPaneLayout จะปรับขนาดของ 2 บานหน้าต่างโดยอัตโนมัติเพื่อให้วางอยู่ทั้ง 2 ด้านของรอยพับหรือบานพับที่ทับซ้อนกัน ในกรณีนี้ ความกว้างที่ตั้งไว้จะถือเป็นความกว้างขั้นต่ำที่ต้องมีในแต่ละ ด้านของฟีเจอร์การพับ หากมีพื้นที่ไม่เพียงพอที่จะรักษามิติข้อมูล ขั้นต่ำดังกล่าว 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 จะเพิ่ม Fragment เริ่มต้นลงในแผงรายละเอียด เพื่อให้มั่นใจว่าผู้ใช้ในอุปกรณ์หน้าจอขนาดใหญ่ จะไม่เห็นแผงด้านขวาว่างเปล่าเมื่อเปิดแอปเป็นครั้งแรก

สลับแผงรายละเอียดโดยใช้โปรแกรม

ในตัวอย่าง XML ก่อนหน้า การแตะองค์ประกอบใน RecyclerView จะทําให้เกิดการเปลี่ยนแปลงในแผงรายละเอียด เมื่อใช้ Fragment คุณจะต้องมี FragmentTransaction ที่แทนที่แผงด้านขวา โดยเรียกใช้ open() ใน SlidingPaneLayout เพื่อสลับไปยัง Fragment ที่เพิ่งแสดง

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 โดยเฉพาะ ซึ่งจะช่วยหลีกเลี่ยงการสร้าง Back Stack ในแผงรายละเอียด

ตัวอย่างในหน้านี้ใช้ SlidingPaneLayout โดยตรงและกำหนดให้คุณ จัดการธุรกรรมของ Fragment ด้วยตนเอง อย่างไรก็ตาม Navigation Component มีการติดตั้งใช้งานเลย์เอาต์แบบ 2 บานหน้าต่างที่สร้างไว้ล่วงหน้าผ่าน AbstractListDetailFragment ซึ่งเป็นคลาส API ที่ใช้ SlidingPaneLayout เบื้องหลังเพื่อจัดการบานหน้าต่างรายการและรายละเอียด

ซึ่งจะช่วยให้คุณลดความซับซ้อนของการกำหนดค่าเลย์เอาต์ XML ได้ แทนที่จะประกาศ SlidingPaneLayout และทั้ง 2 บานหน้าต่างอย่างชัดเจน เลย์เอาต์ของคุณเพียงแค่ต้องมี 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();
}

ปลายทางในกราฟการนำทางของแผงรายละเอียดต้องไม่มีอยู่ในกราฟการนำทางภายนอกหรือกราฟการนำทางทั่วทั้งแอป อย่างไรก็ตาม Deep Link ใดๆ ภายในกราฟการนำทางของแผงรายละเอียดต้องแนบไปกับปลายทางที่โฮสต์ SlidingPaneLayout ซึ่งจะช่วยให้มั่นใจได้ว่า Deep Link ภายนอกจะนำทางไปยังSlidingPaneLayoutปลายทางก่อน แล้วจึงนำทางไปยังปลายทางของแผงรายละเอียดที่ถูกต้อง

ดูตัวอย่างTwoPaneFragment เพื่อดูการติดตั้งใช้งานเลย์เอาต์แบบ 2 บานหน้าต่างทั้งหมดโดยใช้คอมโพเนนต์การนำทาง

ผสานรวมกับปุ่มย้อนกลับของระบบ

ในอุปกรณ์ขนาดเล็กที่แผงรายการและแผงรายละเอียดซ้อนทับกัน ให้ตรวจสอบว่าปุ่มย้อนกลับของระบบ จะนำผู้ใช้จากแผงรายละเอียดกลับไปยังแผงรายการ ทำได้โดยระบุการนำทางย้อนกลับที่กำหนดเองและเชื่อมต่อ 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() เพื่อเปลี่ยนระหว่างบานหน้าต่างรายการและบานหน้าต่างรายละเอียดในโทรศัพท์ วิธีเหล่านี้จะไม่มีผลหากทั้ง 2 บานหน้าต่างมองเห็นได้และไม่ซ้อนทับกัน

เมื่อบานหน้าต่างรายการและรายละเอียดทับซ้อนกัน ผู้ใช้จะปัดได้ทั้ง 2 ทิศทางโดยค่าเริ่มต้น ซึ่งจะสลับระหว่าง 2 บานหน้าต่างได้อย่างอิสระแม้ว่าจะไม่ได้ใช้การนำทางด้วยท่าทางสัมผัสก็ตาม คุณควบคุมทิศทางการปัดได้ โดยตั้งค่าโหมดล็อกของSlidingPaneLayoutดังนี้

Kotlin

binding.slidingPaneLayout.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED

Java

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

ดูข้อมูลเพิ่มเติม

ดูข้อมูลเพิ่มเติมเกี่ยวกับการออกแบบเลย์เอาต์สำหรับอุปกรณ์รูปแบบต่างๆ ได้ในเอกสารประกอบต่อไปนี้

แหล่งข้อมูลเพิ่มเติม