アプリ内のすべての画面はレスポンシブで、利用可能なスペースに適応する必要があります。ConstraintLayout
を使用してレスポンシブ UI を作成すると、単一ペイン アプローチをさまざまなサイズにスケーリングできますが、大規模なデバイスでは、レイアウトを複数のペインに分割することでメリットが得られる場合があります。たとえば、選択したアイテムの詳細リストの横にアイテムのリストを表示したい場合があります。
SlidingPaneLayout
コンポーネントは、大型のデバイスや折りたたみ式デバイスでは 2 つのペインを並べて表示し、スマートフォンなどの小型のデバイスでは一度に 1 つのペインのみを表示するように自動的に適応します。
デバイス固有のガイダンスについては、画面の互換性の概要をご覧ください。
セットアップ
SlidingPaneLayout
を使用するには、アプリの build.gradle
ファイルに次の依存関係を含めます。
Groovy
dependencies { implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0" }
Kotlin
dependencies { implementation("androidx.slidingpanelayout:slidingpanelayout:1.2.0") }
XML のレイアウト構成
SlidingPaneLayout
は、UI の最上位で使用する水平 2 ペイン レイアウトを提供します。このレイアウトでは、1 つ目のペインをコンテンツ リストまたはブラウザとして使用し、別のペインにコンテンツを表示するための主な詳細ビューに従属させます。
SlidingPaneLayout
は、2 つのペインの幅を使用して、ペインを並べて表示するかどうかを決定します。たとえば、リストペインの最小サイズが 200 dp で、詳細ペインに 400 dp が必要な場合、SlidingPaneLayout
は 600 dp 以上の幅が利用可能であれば、2 つのペインを自動的に並べて表示します。
組み合わされた幅が SlidingPaneLayout
で使用可能な幅を超えた場合、子ビューは重複します。この場合、子ビューは SlidingPaneLayout
で使用可能な幅いっぱいに拡張されます。ユーザーは画面の端からドラッグして戻ることで、最上部のビューを邪魔にならないようにスライドさせることができます。
ビューが重ならない場合、SlidingPaneLayout
では、子ビューでレイアウト パラメータ layout_weight
を使用して、測定完了後に残りのスペースを分割する方法を定義できます。このパラメータは、幅にのみ関連します。
画面上に両方のビューを並べて表示するスペースがある折りたたみ式デバイスでは、SlidingPaneLayout
によって 2 つのペインのサイズが自動的に調整され、重なっている折り目またはヒンジの両側に配置されます。この場合、設定された幅は、折りたたみ機能の両側に存在する必要がある最小幅とみなされます。最小サイズを維持するのに十分なスペースがない場合、SlidingPaneLayout
はビューをオーバーラップする状態に戻ります。
以下は、RecyclerView
を左ペイン、FragmentContainerView
を左ペインのコンテンツを表示する主な詳細ビューとして持つ SlidingPaneLayout
を使用している例です。
<!-- 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()
を呼び出しません。これにより、詳細ペインでバックスタックが構築されなくなります。
Navigation コンポーネントの実装
このページの例では、SlidingPaneLayout
を直接使用するため、フラグメント トランザクションを手動で管理する必要があります。ただし、Navigation コンポーネントでは、AbstractListDetailFragment
を通じて 2 ペイン レイアウトの事前構築済みの実装が提供されます。これは、内部で SlidingPaneLayout
を使用してリストペインと詳細ペインを管理する API クラスです。
これにより、XML レイアウト構成を簡素化できます。SlidingPaneLayout
と両方のペインを明示的に宣言する代わりに、レイアウトには、AbstractListDetailFragment
の実装を保持するために FragmentContainerView
だけが必要となります。
<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 コンポーネントを使用した 2 ペイン レイアウトの完全な実装については、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()
を手動で呼び出して、スマートフォンのリストペインと詳細ペイン間を移動できます。両方のペインが表示され、重なっていない場合、これらのメソッドは機能しません。
リストペインと詳細ペインが重なっている場合、ユーザーはデフォルトで両方向へのスワイプが可能で、ジェスチャー ナビゲーションを使用していない場合でも 2 つのペインを自由に切り替えることができます。SlidingPaneLayout
のロックモードを設定することで、スワイプの方向を制御できます。
Kotlin
binding.slidingPaneLayout.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED
Java
binding.getSlidingPaneLayout().setLockMode(SlidingPaneLayout.LOCK_MODE_LOCKED);
さらに詳しく
さまざまなフォーム ファクタ向けのレイアウトの設計について詳しくは、次のドキュメントをご覧ください。
参考情報
- アダプティブ レイアウトの Codelab
- GitHub の SlidingPaneLayout の例。