マルチペインレイアウトを簡単に実装する方法

はじめに

マルチペインレイアウトについての説明は省略する。 http://developer.android.com/design/patterns/multi-pane-layouts.html

マルチペインレイアウトのうち、左側ペインがリスト・右側ペインが詳細になっているものをMaster/Detailパターンと呼ぶ。 今回はSlidingPaneLayoutを使ってこのMaster/Detailパターンのマルチペイン実装を行う。

SlidingPaneLayout

SlidingPaneLayoutはr18からSupportLibraryに含まれるようになった比較的新しいViewGroupだ。 https://developer.android.com/reference/android/support/v4/widget/SlidingPaneLayout.html 機能は大雑把に説明すると以下の様な感じになる。

  • 2つの子Viewを並べた時、画面に入りきるようならそのまま表示する
  • 入りきらないようなら自動的に一つ目のViewをスライドメニューのように振る舞わせる

SlidingPaneLayoutを使うだけで最低限のマルチペイン対応はできる。 ただし、自動化してくれるのは本当に最低限のことだけだ。

見た目

縦画面 sScreenshot_2014-08-13-02-37-06.jpgsScreenshot_2014-08-13-02-37-12.jpg 横画面 sScreenshot_2014-08-13-02-37-21.jpg

1枚目の画像で詳細パネルが灰色になっているが、これはSlidingPaneLayoutが自動的にフィルタをかけている。

実装

まず、レイアウトを以下のように記述する。 リスト領域とコンテンツ領域にそれぞれandroid:layout_widthを指定し、端末の横幅がその合計以上あった場合には並べて表示、それ未満ではスライド表示の切り替えができる。 コンテンツ領域にandroid:layout_weightを指定することで、端末の横幅が580dp以上あった場合、余白部分までコンテンツ領域を広げることができる。 また、コンテンツ領域にandroid:backgroundを指定しているが、未指定の場合スライドで隠れたときのシャドウ描画がおかしくなる場合があるので何か指定したほうが良い。

<android.support.v4.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">

    <ListView
        android:id="@android:id/list"
        android:layout_width="280dp"
        android:layout_height="wrap_content" />

    <FrameLayout
        android:id="@+id/content"
        android:layout_width="300dp"
        android:layout_height="match_parent"
        android:layout_weight="1"
        android:background="@android:color/white" />

</android.support.v4.widget.SlidingPaneLayout>

Fragmentの実装はリスト側(SlidingPaneLayout側)のみ記載する。 書いているのは詳細パネルの遷移処理と、遷移を行ったときにメニュー側を閉じる処理だけ。 その他のマルチペイン化やスライドアニメーションなどはSlidingPaneLayoutが自動的にやってくれる。 SlidingPaneLayoutとリストパネルの実装を同じFragmentで行うのが実装簡略化のためのコツだ。

public class SlidingPanelFragment extends Fragment {

    SlidingPaneLayout mSlidingPaneLayout;


    public SlidingPanelFragment() {
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {


        View view = inflater.inflate(R.layout.fragment_slidingpanel, container, false);
        mSlidingPaneLayout = (SlidingPaneLayout) view.findViewById(R.id.sliding_pane_layout);

        // 適当にリストビューの設定
        ListView listView = (ListView) mSlidingPaneLayout.findViewById(android.R.id.list);
        listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {

                // 詳細側Fragmentの遷移処理
                DetailFragment fragment = new DetailFragment();
                // パラメータが必要な場合はsetArgumentsする

                FragmentTransaction ft = getFragmentManager().beginTransaction();
                ft.replace(R.id.content, fragment);
                ft.addToBackStack(null);
                ft.commit();

                // パネルを閉じる
                if (mSlidingPaneLayout.isSlideable() && mSlidingPaneLayout.isOpen()) {
                    mSlidingPaneLayout.closePane();
                }
            }
        });

        return view;
    }

}

これで終わり。 ね、簡単でしょ?

注意点

SlidingPaneLayoutを実装する上で、大きな注意点がある。 SlidingPaneLayoutではパネルの状態に応じてメニューの表示/非表示を切り替えてくれないため、メニュー制御をする処理を自前で書く必要がある点だ。 このパネルの状態に応じて、というのが曲者で、初回表示時のレイアウトを取得するためのViewTreeObserver.OnGlobalLayoutListenerとパネルスライド時のコールバックを受けるためのSlidingPaneLayout.PanelSlideListenerの2つを組み合わせて実装する必要がある。

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {


        View view = inflater.inflate(R.layout.fragment_slidingpanel, container, false);
        mSlidingPaneLayout = (SlidingPaneLayout) view.findViewById(R.id.sliding_pane_layout);

        // パネルの状態に応じてメニュー制御を行う
        mSlidingPaneLayout.setPanelSlideListener(new SlidingPaneLayout.PanelSlideListener() {
            @Override
            public void onPanelSlide(View view, float v) {
            }

            @Override
            public void onPanelOpened(View view) {
                panelOpened();
            }

            @Override
            public void onPanelClosed(View view) {
                panelClosed();
            }
        });

        // 初回のみ、レイアウト状態に応じてメニュー制御を行う必要がある
        mSlidingPaneLayout.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @SuppressWarnings("deprecation")
            @Override
            public void onGlobalLayout() {
                if (mSlidingPaneLayout.isSlideable()) {
                    // パネルがスライド可能な場合、状態に応じてメニュー変更
                    if (mSlidingPaneLayout.isOpen()) {
                        panelOpened();
                    } else {
                        panelClosed();
                    }
                }
                // 初回のみわかれば良いのでリスナー解除
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
                    mSlidingPaneLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                else
                    mSlidingPaneLayout.getViewTreeObserver().removeGlobalOnLayoutListener(this);
            }
        });


        // 以下略
    }

    /**
     * パネルが閉じられた時の処理
     */
    private void panelClosed() {

        // リスト側メニューを無効化、詳細側メニューを有効化
        setHasOptionsMenu(false);
        if (getChildFragmentManager().findFragmentById(R.id.content) != null) {
            getChildFragmentManager().findFragmentById(R.id.content).setHasOptionsMenu(true);
        }
    }

    /**
     * パネルが開かれた時の処理
     */
    private void panelOpened() {

        // リスト側メニューを有効化、詳細側メニューを有効化
        setHasOptionsMenu(true);
        if (getChildFragmentManager().findFragmentById(R.id.content) != null) {
            getChildFragmentManager().findFragmentById(R.id.content).setHasOptionsMenu(false);
        }
    }

まとめ

簡単といいながら、メニューを使う場合には途端に面倒になるのが難点。 それでもアニメーション込のマルチペインレイアウト化してくれるのはありがたい。 layoutファイルでマルチペイン化するのとどちらが良いかの判断は難しい…。