2 ペイン レイアウトを作成する

Compose を試す
Jetpack Compose は、Android に推奨される UI ツールキットです。Compose でレイアウトを操作する方法を学習します。
<ph type="x-smartling-placeholder"></ph> アダプティブ レイアウト →

アプリのすべての画面がレスポンシブで、利用可能なスペースに適応する必要があります。 レスポンシブ UI を作成するには、 ConstraintLayout - シングルペイン アプローチはさまざまなサイズにスケーリングできるが、デバイスが大きい場合は分割することでメリットが得られる可能性がある 複数のペインに分割できますたとえば、特定のスレッドを 選択したアイテムの詳細のリストの横にあるアイテムのリスト。

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 は、上部で使用する水平 2 ペイン レイアウトを提供します。 必要があります。このレイアウトでは、1 つ目のペインをコンテンツ リストまたはブラウザとして使用し、別のペインにコンテンツを表示するための主な詳細ビューに従属させます。

SlidingPaneLayout の例を示す画像
図 1. 以下を使用して作成されたレイアウトの例 SlidingPaneLayout

SlidingPaneLayout は、2 つのペインの幅を使用して、ペインを並べて表示するかどうかを決定します。たとえば、リストペインが 100 ミリ秒以内の 最小サイズが 200 dp で、詳細ペインに 400 dp が必要な場合は、 SlidingPaneLayout では、次の条件を満たしている限り、自動的に 2 つのペインを並べて表示します。 600 dp 以上の幅が利用可能である。

組み合わされた幅が 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>

この例では、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 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();
}

このコードは特に addToBackStack()FragmentTransaction)これにより、詳細ペインでバックスタックが構築されなくなります。

このページの例では、SlidingPaneLayout を直接使用しているため、次のことを行う必要があります。 手動でフラグメント トランザクションを管理します。ただし、 Navigation コンポーネントは、事前構築済みの 2 ペイン レイアウトにする AbstractListDetailFragment、 内部で 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() リストペインのカスタムビューを提供します。詳細ペインについては AbstractListDetailFragmentNavHostFragment。 つまり、詳細ペインに表示されるデスティネーションのみを含むナビゲーション グラフを定義できます。すると 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 の例 Navigation コンポーネントを使用した 2 ペイン レイアウトの完全な実装をご覧ください。

システムの [戻る] ボタンとの統合

リストペインと詳細ペインが重なる小型デバイスでは、システムの [戻る] ボタンをクリックすると、ユーザーが詳細ペインからリストペインに戻ります。すべきこと カスタム バック機能を用意することで ナビゲーションOnBackPressedCallbackSlidingPaneLayout の現在の状態:

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);
    }
}

次のようにコールバックを OnBackPressedDispatcher addCallback():

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);

詳細

さまざまなフォーム ファクタ向けのレイアウトのデザインについて詳しくは、 次のドキュメント:

参考情報