はじめに
マルチペインレイアウトについての説明は省略する。 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
を使うだけで最低限のマルチペイン対応はできる。
ただし、自動化してくれるのは本当に最低限のことだけだ。
見た目
縦画面 → 横画面
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ファイルでマルチペイン化するのとどちらが良いかの判断は難しい…。