アプリの画面はすべてレスポンシブにし、使用可能なスペースに適応させる必要があります。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
(リストと詳細のペインを管理するために内部で SlidingPaneLayout
を使用する API クラス)を通じて、2 ペイン レイアウトのビルド済み実装が提供されます。
これにより、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 の例。