创建双窗格布局

尝试 Compose 方式
Jetpack Compose 是 Android 的推荐 UI 工具包。了解如何在 Compose 中使用布局。

您的应用中的每个屏幕都必须具有响应性并适应可用空间。您可以 使用 ConstraintLayout 构建响应式 UI,该 UI 允许单窗格方法扩展到多种尺寸,但较大的设备可能受益于将布局拆分为多个窗格。例如,您可能希望屏幕显示项目列表,旁边是所选项目的详细信息列表。

The SlidingPaneLayout 组件支持在较大的设备和可折叠设备上并排显示两个窗格,同时自动适应以在较小的设备(如手机)上一次只显示一个窗格。

有关设备特定指南,请参阅 屏幕兼容性概述

设置

要使用 SlidingPaneLayout,请在您的应用的 build.gradle 文件中包含以下依赖项

Groovy

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

Kotlin

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

XML 布局配置

SlidingPaneLayout 提供水平双窗格布局,供在 UI 的顶层使用。此布局使用第一个窗格作为内容列表或浏览器,从属于主要详细信息视图,用于在另一个窗格中显示内容。

An image showing an example of SlidingPaneLayout
图 1. 使用 SlidingPaneLayout 创建的布局示例。

SlidingPaneLayout 使用两个窗格的宽度来确定是否并排显示窗格。例如,如果列表窗格的最小尺寸测量为 200 dp,而详细信息窗格需要 400 dp,那么只要 SlidingPaneLayout 至少有 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 属性将初始片段添加到详细信息窗格,确保大屏幕设备上的用户在应用首次启动时不会看到空白的右侧窗格。

以编程方式调换详细信息窗格

在前面的 XML 示例中,点击 RecyclerView 中的元素会触发详细信息窗格的变化。当使用片段时,这需要进行 FragmentTransaction 以替换右侧窗格,在 SlidingPaneLayout 上调用 open() 以切换到新可见的片段

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()。这避免在详细信息窗格中构建回退堆栈。

本页中的示例直接使用 SlidingPaneLayout,并且要求您手动管理片段事务。但是,导航组件 通过 AbstractListDetailFragment 提供了一个预构建的双窗格布局实现,这是一个 API 类,它在幕后使用 SlidingPaneLayout 来管理您的列表和详细信息窗格。

这使您可以简化 XML 布局配置。您无需显式声明 SlidingPaneLayout 和两个窗格,您的布局只需要一个 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();
}

详细信息窗格的导航图中的目标不得出现在任何外部的应用程序级导航图中。但是,详细信息窗格的导航图中的任何深度链接必须附加到承载 SlidingPaneLayout 的目标。这有助于确保外部深度链接首先导航到 SlidingPaneLayout 目标,然后导航到正确的详细信息窗格目标。

请参阅 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);

了解更多

要详细了解为不同外形尺寸设计布局,请参阅以下文档

其他资源