应用中的每个屏幕都必须具备自适应能力,并能根据可用空间进行调整。您可以使用 ConstraintLayout
构建自适应界面,让单窗格方法能够扩展到多种尺寸,但对于较大的设备,将布局拆分为多个窗格可能会更合适。例如,您可能希望屏幕在所选项目的详细信息列表旁边显示项目列表。
SlidingPaneLayout
组件支持在较大的设备和可折叠设备上并排显示两个窗格,同时自动进行调整,以便在手机等较小的设备上一次仅显示一个窗格。
如需查看特定于设备的指南,请参阅屏幕兼容性概览。
设置
如需使用 SlidingPaneLayout
,请在应用的 build.gradle
文件中添加以下依赖项:
Groovy
dependencies { implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0" }
Kotlin
dependencies { implementation("androidx.slidingpanelayout:slidingpanelayout:1.2.0") }
XML 布局配置
SlidingPaneLayout
提供一种水平的双窗格布局,可在界面的顶层使用。在这种布局中,第一个窗格用作内容列表或浏览器,从属于另一个窗格中用于显示内容的主要详细信息视图。
![显示 SlidingPaneLayout 示例的图片](https://developer.android.google.cn/static/images/ui/slidingpanelayout-example.png?authuser=6&hl=da)
SlidingPaneLayout
创建的布局示例。
SlidingPaneLayout
会根据两个窗格的宽度来确定是否并排显示这些窗格。例如,如果测量后发现列表窗格的最小尺寸为 200 dp,而详情窗格需要 400 dp,那么只要可用宽度不小于 600 dp,SlidingPaneLayout
就会自动并排显示这两个窗格。
如果子视图的总宽度超过了 SlidingPaneLayout
中的可用宽度,这些视图就会重叠在一起。在这种情况下,子视图会展开,填充 SlidingPaneLayout
中的可用宽度。用户可以从屏幕的边缘拖回最顶层的视图以将其移开。
如果视图不重叠,SlidingPaneLayout
支持在子视图上使用布局参数 layout_weight
,以定义测量完成后如何划分剩余的空间。此参数仅与宽度相关。
对于在屏幕上有空间并排显示两个视图的可折叠设备上,SlidingPaneLayout
会自动调整两个窗格的大小,使其位于重叠的折叠边或合页的任意一侧。在这种情况下,设置的宽度会被视为折叠功能两侧必须存在的最小宽度。如果没有足够的空间来保持该最小尺寸,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>
在此示例中,FragmentContainerView
上的 android:name
属性会将初始 fragment 添加到详情窗格中,以确保大屏设备上的用户在应用首次启动时不会看到空的右侧窗格。
以编程方式更换详细信息窗格
在前面的 XML 示例中,点按 RecyclerView
中的某个元素会触发详细信息窗格中的更改。在使用 fragment 时,这需要一个可替换右侧窗格的 FragmentTransaction
,针对 SlidingPaneLayout
调用 open()
来更换为新的可见 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(); }
具体来说,此代码不会对 FragmentTransaction
调用 addToBackStack()
,这样可避免在详细信息窗格中构建返回堆栈。
Navigation 组件实现
本页中的示例直接使用 SlidingPaneLayout
,并要求您手动管理 fragment 事务。不过,Navigation 组件通过 AbstractListDetailFragment
提供了双窗格布局的预构建实现,后者是一个在后台使用 SlidingPaneLayout
管理列表窗格和详情窗格的 API 类。
这样,您就可以简化 XML 布局配置。您的布局只需要 FragmentContainerView
来保留 AbstractListDetailFragment
实现,而无需显式声明 SlidingPaneLayout
和这两个窗格:
<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(); }
详情窗格的导航图中的目的地不得出现在任何应用范围的外部导航图中。不过,详细信息窗格的导航图中的任何深层链接都必须附加到托管 SlidingPaneLayout
的目的地。这有助于确保外部深层链接先导航到 SlidingPaneLayout
目的地,然后再导航到详情窗格中的正确目的地。
如需了解使用 Navigation 组件的双窗格布局的完整实现,请参阅 TwoPaneFragment 示例。
与系统返回按钮集成
在列表窗格与详情窗格重叠的小屏幕设备上,请确保用户可通过系统返回按钮从详情窗格返回到列表窗格。为此,您可以提供自定义返回导航,并将 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); } }
您可以使用 addCallback()
向 OnBackPressedDispatcher
添加回调:
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()
,以在手机上的列表窗格和详情窗格之间转换。如果两个窗格均可见且不重叠,这些方法不会产生任何影响。
如果列表窗格和详细信息窗格重叠,那么在默认情况下,用户可以向两个方向滑动,这样一来,即使在没有使用手势导航的情况下,也可以随意在两个窗格之间切换。您可以设置 SlidingPaneLayout
的锁定模式来控制滑动方向:
Kotlin
binding.slidingPaneLayout.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED
Java
binding.getSlidingPaneLayout().setLockMode(SlidingPaneLayout.LOCK_MODE_LOCKED);
了解详情
如需详细了解如何针对不同外形规格设计布局,请参阅以下文档:
其他资源
- 自适应布局 Codelab
- GitHub 上的 SlidingPaneLayout 示例。