建立雙窗格版面配置

應用程式中的每個畫面都必須是回應式內容,且應配合可用空間進行調整。使用 ConstraintLayout 建立回應式 UI,讓您能夠使用單一窗格縮放至多種大小,但在使用較大的裝置時,將版面配置分割成多個窗格或許會更為方便。例如,您可能希望螢幕並排顯示項目清單和目前選取項目的詳細資料。

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 會依據兩個窗格的寬度,判斷是否要並排顯示窗格。舉例來說,如果清單窗格測得的最小尺寸為 200dp,且詳細資料窗格需要 400dp,那麼只要擁有至少 600dp 的可用寬度,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
        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>

在這個範例中,FragmentContainerViewandroid: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 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();
}

這個程式碼「不會」呼叫 FragmentTransaction 上的 addToBackStack()。這樣就免去了在詳細資料窗格中建立返回堆疊的作業。

上述範例直接使用 FragmentContainerView 和片段交易。不過,您可以改為使用導覽元件實作詳細資料窗格。如果使用 NavHostFragment 做為詳細資料窗格,您可以使用 NavController 切換導覽圖表中的目的地。導覽圖表中只包含要顯示在詳細資料窗格中的目的地:

Kotlin

// A method on the Fragment that owns the SlidingPaneLayout,
// called by the adapter when an item is selected.
fun openDetails(itemId: Int) {
    // Assume the NavHostFragment is added with the +id/detail_container.
    val navHostFragment = childFragmentManager.findFragmentById(
       R.id.detail_container) as NavHostFragment
    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

// A method on the Fragment that owns the SlidingPaneLayout,
// called by the adapter when an item is selected.
void openDetails(int itemId) {
    // Assume the NavHostFragment is added with the +id/detail_container.
    NavHostFragment navHostFragment = (NavHostFragment) getChildFragmentManager()
        .findFragmentById(R.id.detail_container);
    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();
}

詳細資料窗格導覽圖表中的目的地「不得」顯示在應用程式內通用的外部導覽圖表中。不過,詳細資料窗格導覽圖表中的所有深層連結均應附加至代管 SlidingPaneLayout 的目的地。這可確保外部深層連結先前往 SlidingPaneLayout 目的地,「然後」再前往正確的詳細資料窗格目的地。

與系統返回按鈕整合

在小型裝置上,清單和詳細資料窗格將會重疊,因此需要確保系統返回按鈕會將使用者從詳細資料窗格移回清單窗格。為此,請提供自訂返回導覽,並將 OnBackPressedCallback 連線至 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);
    }
}

然後,您可以使用 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))

        // 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.
    }
}

鎖定模式

SlidingPaneLayout 讓您始終能夠手動呼叫 open()close(),以便使用手機在清單和詳細資料窗格之間進行切換。如果兩個窗格都顯示且未重疊,這些方法就不會生效。

根據預設,當清單和詳細資料窗格重疊時,使用者可以左右滑動,不使用手勢操作即可在兩個窗格之間切換。您可以設定 SlidingPaneLayout 的鎖定模式,控制滑動方向:

Kotlin

binding.slidingPaneLayout.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED

Java

binding.getSlidingPaneLayout().setLockMode(SlidingPaneLayout.LOCK_MODE_LOCKED);

瞭解詳情

如要進一步瞭解如何為不同的板型規格設計版面配置,請參閱下列指南: