Mọi màn hình trong ứng dụng đều phải thích ứng và phù hợp với không gian hiện có.
Bạn có thể tạo giao diện người dùng thích ứng bằng
ConstraintLayout
cho phép một ngăn đơn
tiếp cận mở rộng thành nhiều kích thước, nhưng các thiết bị lớn hơn có thể được hưởng lợi từ việc chia tách
bố cục thành nhiều ngăn. Ví dụ: bạn có thể muốn màn hình hiển thị
danh sách các mục bên cạnh danh sách chi tiết về mục đã chọn.
Chiến lược phát hành đĩa đơn
SlidingPaneLayout
hỗ trợ hiển thị hai ngăn cạnh nhau trên các thiết bị lớn hơn và
có thể gập lại và tự động điều chỉnh để chỉ hiển thị một ngăn tại một thời điểm
các thiết bị nhỏ hơn như điện thoại.
Để biết hướng dẫn dành riêng cho từng loại thiết bị, vui lòng xem 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 bố cục 2 ngăn theo chiều ngang để sử dụng ở trên cùng
của một giao diện người dùng. 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 chế độ xem 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 ngăn danh sách được đo lường để có
kích thước tối thiểu là 200 dp và ngăn chi tiết cần có kích thước 400 dp, thì
SlidingPaneLayout
sẽ tự động hiển thị hai ngăn cạnh nhau miễn là
có chiều rộng tối thiểu là 600 dp.
Các chế độ xem 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 chế độ xem 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 chế độ xem 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 chế độ xem không chồng chéo nhau, SlidingPaneLayout
sẽ hỗ trợ việc sử dụng bố cục
tham số layout_weight
trên các khung hiển thị con để xác định cách phân 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 có không gian trên màn hình để hiển thị cả hai chế độ xem cạnh nhau
SlidingPaneLayout
sẽ tự động điều chỉnh kích thước của hai ngăn để
chúng được đặt ở hai bên của đường ranh giới phần hiển thị hoặc bản lề xếp chồng. Trong phần này
trong trường hợp, chiều rộng đã đặt được coi là chiều rộng tối thiểu phải có trên mỗi
của tính năng gập. Nếu không có đủ dung lượng để duy trì
kích thước tối thiểu, SlidingPaneLayout
sẽ chuyển về chế độ xem chồng chéo.
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 chế độ xem 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 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>
Trong ví dụ này, thuộc tính android:name
trên FragmentContainerView
sẽ thêm
mảnh ban đầu đến ngăn chi tiết, đảm bảo rằng
người dùng trên màn hình lớn
thiết bị sẽ không thấy ngăn trống bên phải 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ước, hãy nhấn vào một phần tử trong RecyclerView
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 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(); }
Mã này đặc biệt không gọi
addToBackStack()
vào 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ên trang này sử dụng trực tiếp SlidingPaneLayout
và yêu cầu bạn phải
quản lý các giao dịch 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 dựng sẵn
bố cục hai ngăn thông qua
AbstractListDetailFragment
!
một lớp API sử dụng SlidingPaneLayout
nâng cao để quản lý danh sách của bạn
và ngăn chi tiết.
Điều này cho phép bạn đơn giản hoá cấu hình bố cục XML của mình. 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>
Triển khai
onCreateListPaneView()
và
onListPaneViewCreated()
để cung cấp một khung hiển thị tuỳ chỉnh cho ngăn danh sách. Đối với ngăn chi tiết,
AbstractListDetailFragment
sử dụng
NavHostFragment
.
Đ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 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(); }
Các đích đến trong biểu đồ điều hướng của ngăn chi tiết không được xuất hiện trong
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, bất kỳ đường liên kết sâu nào trong thông tin chi tiết
Bạn phải đính kèm biểu đồ điều hướng của ngăn với đích đến lưu trữ
SlidingPaneLayout
. Điều này giúp đảm bảo rằng các đường liên kết sâu bên ngoài sẽ điều hướng
đến đích đến SlidingPaneLayout
, sau đó chuyển đến đúng thông tin chi tiết
đích của ngăn.
Xem Ví dụ về TwoPaneFragment để triển khai đầy đủ bố cục hai ngăn bằ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, ngăn danh sách và ngăn chi tiết chồng chéo lên nhau, hãy đảm bảo hệ thống
nút quay lại sẽ đưa người dùng từ ngăn chi tiết trở lại ngăn danh sách. Nên
bằng cách cung cấp chức năng tuỳ chỉnh
điều hướng và kết nối
OnBackPressedCallback
thành
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, 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); } }
Bạn có thể thêm lệnh gọi lại vào phương thức
OnBackPressedDispatcher
đang 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)) // 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. } }
Chế độ khoá
SlidingPaneLayout
luôn cho phép bạn gọi open()
và
close()
để chuyển đổi giữa ngăn danh sách và ngăn chi tiết trên điện thoại. Các phương thức này không có
nếu cả hai ngăn đều hiển thị 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 tài liệu sau đây: