ActionBar.Tabでandroid.R.id.contentに2つのListFragmentを入れた場合の対処

目次


最小構成のViewでListFragmentを表示しようとしたら両方表示されちゃった。

たとえばこういう構成を考える。

これをActionBar.Tabで制御する場合、
Activity#onCreate()で以下のようにして対応すれば
いずれかのListFragmentが表示されるかなと思った。

しかしこれでは、id/fragment1 と id/fragment2 が同時に表示されてしまう。

public class TestActivity extends Activity
  1. @Override
  2. public void onCreate(Bundle savedInstanceState) {
  3. super.onCreate(savedInstanceState);
  4.  
  5. setContentView(R.layout.main_custom_listview);
  6. createTab(savedInstanceState);
  7. }
  8.  
  9. private void createTab(Bundle inState) {
  10. ActionBar bar = getActionBar();
  11. bar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
  12.  
  13. ActionBar.Tab tab;
  14. FragmentManager fm = getFragmentManager();
  15. Fragment f;
  16.  
  17. f = fm.findFragmentById(R.id.fragment1);
  18. tab = bar.newTab();
  19. tab.setTag(f);
  20. tab.setText("Tab1");
  21. tab.setTabListener(new TabControl());
  22. bar.addTab(tab);
  23.  
  24. f = fm.findFragmentById(R.id.fragment2);
  25. tab = bar.newTab();
  26. tab.setTag(f);
  27. tab.setText("Tab2");
  28. tab.setTabListener(new TabControl());
  29. bar.addTab(tab);
  30. }
  31.  
  32. private static final class TabControl implements ActionBar.TabListener {
  33. public void onTabSelected(Tab tab, FragmentTransaction ft) {
  34. Fragment fragment = (Fragment) tab.getTag();
  35. ft.show(fragment);
  36. }
  37. public void onTabUnselected(Tab tab, FragmentTransaction ft) {
  38. Fragment fragment = (Fragment) tab.getTag();
  39. ft.hide(fragment);
  40. }
  41. public void onTabReselected(Tab tab, FragmentTransaction ft) {
  42. }
  43. }
  44.  

res/layout/main_custom_listview.xml
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
 
    <fragment
        android:id="@+id/fragment1"
        android:name="test.listfrag.CustomListViewTestFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
 
    <fragment
        android:id="@+id/fragment2"
        android:name="test.listfrag.CustomListViewTestFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</merge>
 

この構成では id/fragment2 が表示状態で起動しているので表示される。
当然 id/fragment1 も起動している。
その上で ActionBar.Tab の addTab() では、初回登録時しか自動で selectTab() が呼ばれないため、
結果、id/fragment2 の FragmentTransaction#hide() は呼ばれない。
(正確には ActionBar.TabListener#onTabUnselected() が呼ばれない)

まずはセコい方法でやってみる。(id/fragment1 の挿入位置を鑑みて・・・)

改めて構成を見直してみる。

ふと気づいたのだが、
res/layout/main_custom_listview.xmlで作っておいた<fragment>が、
<LinearLayout>になっている。
これは何を意味するのだろう?

正解を言うと、
public class CustomListViewTestFragment extends ListFragment
  1. @Override
  2. public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
  3. // カスタムビューを指定。
  4. return inflater.inflate(R.layout.custom_listview, container, false);
  5. }
  6.  

で指定した res/layout/custom_listview.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >
  ~~~
    <ListView
        android:id="@android:id/list"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />
  ~~~
</LinearLayout>
 

という構成なのだ。
これをそのまま <fragment> が <merge> して id/fragment1 を付与している。

では、こうしておけばいいんではないか?
res/layout/custom_listview.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:visibility="gone" >
  ~~~
    <ListView
        android:id="@android:id/list"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />
  ~~~
</LinearLayout>
 
最初から id/fragment1 になる <LinearLayout> を visibility="gone" にする!
なんという荒業。。。

で、やってみた。

ですよねー。

セコい方法をやり遂げてみる。

しかし腑に落ちない。
ちゃんと ActionBar.Tab は初回で selectTab() されているのだから、
FragmentTransaction#show() さえ走れば、fragment1/fragment2 のどちらかくらいは
表示されて然るべきだというのに。

納得がいかないのでこの辺を調べてみる。
frameworks/base/core/java/android/app/BackStackRecord.java
@hide なソースなのだけれど、これを見る限り動いてもよさそうなんだよね。
  1. public FragmentTransaction hide(Fragment fragment) {
  2. Op op = new Op();
  3. op.cmd = OP_HIDE;
  4. op.fragment = fragment;
  5. addOp(op);
  6.  
  7. return this;
  8. }
  9.  
  10. public FragmentTransaction show(Fragment fragment) {
  11. Op op = new Op();
  12. op.cmd = OP_SHOW;
  13. op.fragment = fragment;
  14. addOp(op);
  15.  
  16. return this;
  17. }
  18.  
あ、addOp()ってことは hide() => show() と叩けば、
必ず hide() が走ってから show() が走る?
ならこれでどうだ!
public class TestActivity extends Activity
  1. private static final class TabControl implements ActionBar.TabListener {
  2. public void onTabSelected(Tab tab, FragmentTransaction ft) {
  3. Fragment fragment = (Fragment) tab.getTag();
  4. ft.hide(fragment);
  5. ft.show(fragment);
  6. }
  7. public void onTabUnselected(Tab tab, FragmentTransaction ft) {
  8. Fragment fragment = (Fragment) tab.getTag();
  9. ft.show(fragment);
  10. ft.hide(fragment);
  11. }
  12. public void onTabReselected(Tab tab, FragmentTransaction ft) {
  13. }
  14. }
  15.  

……。
………。
…………。

あ、表示された!

でもなんか納得いかないんだよなぁ。。。

正攻法でやってみる

もっとスマートにできないもんだろうか。
そう思って色々とやった結果、これが一番正攻法のように思う。

public class TestActivity extends Activity
  1. @Override
  2. public void onCreate(Bundle savedInstanceState) {
  3. super.onCreate(savedInstanceState);
  4.  
  5. // XXX : Fragmentは表示したいときに追加するのが吉。
  6. // なので自動で表示状態にしてしまうのは、今回は、よろしくない。
  7. // setContentView(R.layout.main_custom_listview);
  8. createTab(savedInstanceState);
  9. }
  10.  
  11. private void createTab(Bundle inState) {
  12. ActionBar bar = getActionBar();
  13. bar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
  14.  
  15. ActionBar.Tab tab;
  16. CustomListViewTestFragment f;
  17.  
  18. // XXX : 必要なインスタンスの生成だけを行い、登録は別途行う。
  19. f = new CustomListViewTestFragment();
  20. tab = bar.newTab();
  21. tab.setTag(f);
  22. tab.setText("Tab1");
  23. tab.setTabListener(new TabControl());
  24. bar.addTab(tab);
  25.  
  26. f = new CustomListViewTestFragment();
  27. tab = bar.newTab();
  28. tab.setTag(f);
  29. tab.setText("Tab2");
  30. tab.setTabListener(new TabControl());
  31. bar.addTab(tab);
  32. }
  33.  
  34. private static final class TabControl implements ActionBar.TabListener {
  35. public void onTabSelected(Tab tab, FragmentTransaction ft) {
  36. Fragment fragment = (Fragment) tab.getTag();
  37. // XXX : 今回の肝。Fragmentを必要なときに画面に追加する。
  38. // 追加済みかどうかはFragmentが知っている!
  39. if (!fragment.isAdded()) {
  40. ft.add(android.R.id.content, fragment);
  41. }
  42. ft.show(fragment);
  43. }
  44. public void onTabUnselected(Tab tab, FragmentTransaction ft) {
  45. Fragment fragment = (Fragment) tab.getTag();
  46. ft.hide(fragment);
  47. }
  48. public void onTabReselected(Tab tab, FragmentTransaction ft) {
  49. }
  50. }
  51.  

こうすることで目的の画面は表示された。
構成もいい感じ。

まとめ

今回のことでいくつか分かったことがある。
  • <Fragment>は自動的に onCreateView() で inflate() した ViewGroup に置き換わる。
  • 重なるように Fragment を表示する場合は、setContentView()よりadd()する方が都合がいい。
    • そうすれば自動的にparentのViewGroupにmergeがかかる。
  • このとき FragmentTransaction#add() 済みかどうかは Fragment#isAdded() で確認する。
  • show()/hide()で操作するときは、Fragment#onHiddenChanged() が便利。

  • ただし、Fragmentを new したばかりの場合は ListFragment#getListView()をすると落ちる。
    • onCreateView()後ではないので当然か・・・。




最終更新:2012年02月01日 03:55