ทุกหน้าจอในแอปต้องตอบสนองและปรับให้เข้ากับพื้นที่ที่มี
คุณสามารถสร้าง 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
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);
ดูข้อมูลเพิ่มเติม
ดูข้อมูลเพิ่มเติมเกี่ยวกับการออกแบบเลย์เอาต์สำหรับอุปกรณ์รูปแบบต่างๆ ได้ในเอกสารประกอบต่อไปนี้