Mọi màn hình trong ứng dụng đều phải mang tính phản hồi và thích ứng với không gian hiện có. Thao tác tạo giao diện người dùng thích ứng bằng ConstraintLayout có thể cho phép một phương thức ngăn đơn để mở rộng theo nhiều kích thước, nhưng các thiết bị lớn hơn có thể hưởng lợi khi chia bố cục thành nhiều ngăn. Ví dụ: bạn có thể muốn một màn hình hiện danh sách các mục cạnh nhau kèm theo thông tin chi tiết của mục đang được chọn.
Thành phần SlidingPaneLayout
hỗ trợ hiện hai ngăn cạnh nhau trên các thiết bị lớn hơn có thể gập lại, đồng thời tự động điều chỉnh để chỉ hiện một ngăn tại một thời điểm trên các thiết bị nhỏ hơn (chẳng hạn như điện thoại).
Để biết hướng dẫn dành riêng cho thiết bị, hãy xem nội dung tổng quan về khả năng tương thích với màn hình.
Thiết lập
Để sử dụng SlidingPaneLayout
, hãy đưa phần phụ thuộc sau vào tệp build.gradle
của ứng dụng:
Groovy
dependencies { implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0" }
Kotlin
dependencies { implementation("androidx.slidingpanelayout:slidingpanelayout:1.2.0") }
Cấu hình bố cục XML
SlidingPaneLayout
cung cấp một bố cục theo chiều ngang gồm có hai ngăn để sử dụng cho giao diện người dùng ở cấp cao nhất. Bố cục này sử dụng ngăn đầu tiên làm danh sách nội dung hoặc trình duyệt, phụ thuộc vào thành phần hiển thị chi tiết chính để hiện nội dung trong ngăn khác.

SlidingPaneLayout
sử dụng chiều rộng của hai ngăn để xác định xem các ngăn này có hiện cạnh nhau hay không. Ví dụ: nếu kích thước tối thiểu của ngăn danh sách là 200dp và ngăn chi tiết là 400dp thì SlidingPaneLayout
sẽ tự động hiện hai ngăn cạnh nhau nếu chiều rộng có sẵn tối thiểu cho hai ngăn là 600dp.
Các thành phần hiển thị con sẽ chồng chéo nhau nếu tổng chiều rộng của chúng vượt quá chiều rộng có sẵn trong SlidingPaneLayout
. Trong trường hợp này, các thành phần hiển thị con sẽ mở rộng để lấp đầy chiều rộng có sẵn trong SlidingPaneLayout
. Người dùng có thể trượt thành phần hiển thị trên cùng ra ngoài bằng cách kéo nó trở lại từ cạnh của màn hình.
Nếu các thành phần hiển thị không chồng chéo nhau thì SlidingPaneLayout
sẽ hỗ trợ quá trình sử dụng tham số bố cục layout_weight
trên các thành phần hiển thị con để xác định cách chia không gian còn lại sau khi đo lường xong. Tham số này chỉ liên quan đến chiều rộng.
Trên một thiết bị có thể gập lại và có không gian trên màn hình để cho thấy cả hai thành phần hiển thị cạnh nhau, SlidingPaneLayout
tự động điều chỉnh kích thước của hai ngăn để chúng nằm ở một bên của màn hình hoặc bản lề của thiết bị gập. Trong trường hợp này, chiều rộng được thiết lập được coi là chiều rộng tối thiểu phải có trên mỗi cạnh của tính năng gấp. Nếu không có đủ không gian để duy trì kích thước tối thiểu đó, SlidingPaneLayout
sẽ chuyển về thành phần hiển thị trùng lặp.
Sau đây là ví dụ về cách sử dụng SlidingPaneLayout
có RecyclerView
làm ngăn bên trái và FragmentContainerView
làm thành phần hiển thị chi tiết chính để hiện nội dung của ngăn bên trái:
<!-- 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
desired width (expressed using android:layout_width) would
not 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
the entire window is wide enough to fit both 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>
Trong ví dụ này, thuộc tính android:name
trên FragmentContainerView
sẽ thêm mảnh ban đầu vào ngăn chi tiết, đảm bảo rằng người dùng trên các thiết bị màn hình lớn không nhìn thấy ngăn bên phải trống khi ứng dụng khởi chạy lần đầu tiên.
Hoán đổi ngăn chi tiết theo phương thức lập trình.
Trong ví dụ XML ở trên, thao tác nhấn vào một phần tử trong RecyclerView
sẽ kích hoạt một thay đổi trong ngăn chi tiết. Khi sử dụng mảnh (phải có FragmentTransaction
thay thế ngăn bên phải), hãy gọi open()
trên SlidingPaneLayout
để hoán đổi với mảnh mới hiển thị:
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 we're 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 we're 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(); }
Mã này đặc biệt không gọi addToBackStack()
trên FragmentTransaction
. Điều này giúp bạn tránh tạo ngăn xếp lui trong ngăn chi tiết.
Triển khai thành phần điều hướng
Các ví dụ trình bày trong bài viết hướng dẫn này đã sử dụng trực tiếp SlidingPaneLayout
và yêu cầu bạn
quản lý các giao dịch về mảnh theo cách thủ công. Tuy nhiên, Thành phần điều hướng cung cấp cách triển khai sẵn
về bố cục hai ngăn thông qua
AbstractListDetailFragment
,
một lớp API giúp sử dụng SlidingPaneLayout
dưới phần nâng cao để quản lý danh sách
và ngăn chi tiết.
Điều này cho phép bạn đơn giản hóa cấu hình bố cục XML. Thay vì khai báo rõ ràng SlidingPaneLayout
và cả hai ngăn, bố cục của bạn chỉ cần có
FragmentContainerView
để lưu giữ cách triển khai AbstractListDetailFragment
của bạn:
<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>
Trong khi triển khai, bạn cần triển khai
onCreateListPaneView()
và
onListPaneViewCreated()
để cung cấp chế độ xem tùy chỉnh cho ngăn danh sách; nhưng AbstractListDetailFragment
sử dụng
NavHostFragment
làm
ngăn chi tiết. Điều này có nghĩa là bạn có thể xác định một biểu đồ
điều hướng chỉ chứa
các đích đến sẽ xuất hiện trong ngăn chi tiết. Sau đó, bạn có thể dùng
NavController
để thay đổi ngăn
chi tiết giữa các đích trong sơ đồ điều hướng độc lập:
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 we're 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 we're 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(); }
Các đích đến trong biểu đồ điều hướng của ngăn chi tiết sẽ không xuất hiện trên bất kỳ biểu đồ điều hướng nào bên ngoài, trên toàn ứng dụng. Tuy nhiên, mọi đường liên kết sâu trong biểu đồ điều hướng của ngăn chi tiết phải được đính kèm với đích đến lưu trữ SlidingPaneLayout
. Điều này đảm bảo rằng đường liên kết sâu bên ngoài sẽ chuyển tới
đích đến SlidingPaneLayout
rồi chuyển tới đích đến chi tiết
chính xác.
Xem ví dụ để biết cách triển khai đầy đủ bố cục hai ngăn bằng cách sử dụng thành phần Điều hướng.
Tích hợp với nút quay lại của hệ thống
Trên các thiết bị nhỏ hơn nơi ngăn danh sách và ngăn chi tiết chồng chéo nhau, bạn phải đảm bảo rằng nút quay lại của hệ thống sẽ đưa người dùng từ ngăn chi tiết trở lại ngăn danh sách. Bạn có thể thực hiện việc này bằng cách cung cấp phương thức điều hướng quay lại tuỳ chỉnh và kết nối OnBackPressedCallback
với trạng thái hiện tại của SlidingPaneLayout
:
Kotlin
class TwoPaneOnBackPressedCallback( private val slidingPaneLayout: SlidingPaneLayout ) : OnBackPressedCallback( // Set the default 'enabled' state to true only if it is slidable (i.e., the panes // are overlapping) and open (i.e., 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 pressed. 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 (i.e., the panes // are overlapping) and open (i.e., 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 pressed. 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); } }
Sau đó, bạn có thể thêm lệnh gọi lại vào OnBackPressedDispatcher
bằng cách sử dụng 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)) // Setup the RecyclerView adapter, etc. } }
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())); // Setup the RecyclerView adapter, etc. } }
Chế độ khoá
SlidingPaneLayout
luôn cho phép bạn gọi open()
và close()
theo cách thủ công để chuyển đổi giữa ngăn các danh sách và ngăn chi tiết trên điện thoại. Các phương thức này sẽ không có tác dụng nếu cả hai ngăn đều xuất hiện và không chồng chéo nhau.
Khi ngăn danh sách và ngăn chi tiết chồng chéo nhau, người dùng có thể vuốt theo cả hai hướng theo mặc định, thoải mái hoán đổi giữa hai ngăn ngay cả khi không sử dụng tính năng thao tác bằng cử chỉ. Bạn có thể điều khiển hướng vuốt bằng cách thiết lập chế độ khoá của SlidingPaneLayout
:
Kotlin
binding.slidingPaneLayout.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED
Java
binding.getSlidingPaneLayout().setLockMode(SlidingPaneLayout.LOCK_MODE_LOCKED);
Tìm hiểu thêm
Để tìm hiểu thêm về cách thiết kế bố cục cho nhiều hệ số hình dạng, hãy xem hướng dẫn sau: