4ヶ月前にこんなツイートをしたまま放置してましたごめんなさい。書きます。
※ツイートした当時に認識していたものよりも問題が大きかったので、タイトルは少し違ったものになりました
はじめに
これからはFragmentの時代や! と誰が言ったか定かではありませんが、Fragmentの概念が登場してから1年半の月日が経ちそうです。みなさん、Fragment使ってますか?
僕はお仕事でゴリゴリFragment使ってます。ときどき使い方間違っててTLの皆様から失笑を買ったりしておりますがめげません(´;ω;`)ブワッ
で、アプリの中でListFragmentを使う機会というのは割と多いと思うんですが、やはりViewのカスタマイズが必要になってくる場面があると思います。そんな要望に応えるのがこちらの記事。
ListFragment 内で表示する View をカスタマイズするには、
onCreateView() 内で、差し替えたい View をインフレートします。
ListFragment の View をカスタマイズする方法 - adakoda
実は、これをそのまま使うと、ListFragment#setListShown(boolean)が使えなくなってしまいます。
ということで、今回はこの問題への対策となります。
※この問題自体はSupport Package版・3.x Higher版のListFragmentの両方で起こりますが、後者の対応方法は分からないので誰かよろしく
ver.0 元の動作
ソース
MainActivity.java
public class MainActivity extends FragmentActivity implements OnClickListener { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); Button btnSetAdapter = (Button) findViewById(R.id.btn_set_adapter); btnSetAdapter.setOnClickListener(this); Button btnToggleListShown = (Button) findViewById(R.id.btn_toggle_listshown); btnToggleListShown.setOnClickListener(this); } @Override public void onClick(View v) { // リストの中身 String[] items = { "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "20" }; // ListFragment SampleListFragment fragment = (SampleListFragment) getSupportFragmentManager().findFragmentById(R.id.f_list); switch (v.getId()) { case R.id.btn_set_adapter: // ListAdapterをセット(初期表示のProgressBarはここで消える) fragment.setListAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, items)); break; case R.id.btn_toggle_listshown: // リストを消してProgressBarを表示する fragment.setListShown(isListShown); isListShown = !isListShown; break; } } }
SimpleListFragment.java
public class SampleListFragment extends ListFragment { }
main.xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <fragment android:id="@+id/f_list" android:name="net.nkzn.android.sample.v4.customlist.SampleListFragment" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_above="@+id/btn_toggle_listshown" /> <Button android:id="@+id/btn_set_adapter" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_alignParentLeft="true" android:text="set adapter" /> <Button android:id="@+id/btn_toggle_listshown" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_above="@+id/btn_set_adapter" android:layout_alignParentLeft="true" android:text="toggle ListShown" /> </RelativeLayout>
解説
初期表示時からListAdapterをセットするまでの間、ProgressBarが自動でぐるぐる回っててくれます。
一度Adapterをセットしたあとは、ListFragment#setListShown(boolean)で以下の動作を切り替えられます。
false | リストを隠してProgressBarを出す |
---|---|
true | リストを出してProgressBarを隠す |
そして余談ですが何も書かなくても動くListFragmentさんある意味すげえ。
ver.1 Viewをカスタマイズ
あだこだ先生の言うとおりに、以下のコードを足したり変更したりしました。
※背景画像やピンはヒバナさんからいただきました。
ソース
SampleListFragment.java
@Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.custom_list_layout, container, false); return view; }
custom_list_layout.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@drawable/tile_bg" > <!-- タイトル付けてみた --> <TextView android:id="@+id/tv_title" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="8dp" android:drawableLeft="@drawable/pin" android:drawablePadding="8dp" android:gravity="center_vertical" android:text="カスタマイズサンプル" android:textAppearance="?android:attr/textAppearanceMedium" /> <!-- ListView用(idは定義済みのandroid:list)--> <ListView android:id="@id/android:list" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingLeft="16dp" android:paddingRight="16dp" android:layout_below="@+id/tv_title" android:drawSelectorOnTop="false" /> <!-- アイテムが空の場合に使用するテキスト(idは定義済みのandroid:empty)--> <TextView android:id="@id/android:empty" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_below="@id/android:list" android:text="No data" /> </RelativeLayout>
解説
哀しいことその1:ListAdapterのセット前にぐるぐるしてくれません。
哀しいことその2:setListShownすると落ちます。
ver.1で何が起きたのか
エラーメッセージ
FATAL EXCEPTION: main
java.lang.IllegalStateException: Can't be used with a custom content view
at android.support.v4.app.ListFragment.setListShown(ListFragment.java:282)
at android.support.v4.app.ListFragment.setListShown(ListFragment.java:258)
at net.nkzn.android.sample.v4.customlist.MainActivity.onClick(MainActivity.java:38)
at android.view.View.performClick(View.java:2604)
at android.view.View$PerformClick.run(View.java:9314)
at android.os.Handler.handleCallback(Handler.java:587)
at android.os.Handler.dispatchMessage(Handler.java:92)
at android.os.Looper.loop(Looper.java:130)
at android.app.ActivityThread.main(ActivityThread.java:3691)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:507)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:912)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:670)
at dalvik.system.NativeStart.main(Native Method)
Can't be used with a custom content view
「ListFragment#setListShownはカスタマイズしたViewでは使えませんよ」と。
なんか無理らしい
じゃあ諦めようか。
やっぱAndroidといえばコードリーディングっしょ!
ソースコード探訪
無理って言われたからって諦めてたらプログラマーやってられないんで、無理を道理でひっくり返している前例を探しに行きましょう。つまり、「本来のListFragmentはsetListShownできてるんだから、ListFragmentだけがやってる方法があるはずだ!」というわけです。
Support Packageの中にある本家ListFragmentをさくっと参照しちゃいましょう。
$ANDROID_SDK/extras/android/support/v4/src/java/android/support/v4/app/ListFragment
なんかonCreateViewの中身が色々とアレな様子が御覧いただけましたでしょうか。INTERNAL_HOGEHOGE_IDさんたちがキモのようです。
ver.2 既に出来上がったものがこちらになります。
ソース
SampleListFragment.java
public class SampleListFragment extends ListFragment { /** おまじないの材料1 */ private static final int INTERNAL_PROGRESS_CONTAINER_ID = 0x00ff0002; /** おまじないの材料2 */ private static final int INTERNAL_LIST_CONTAINER_ID = 0x00ff0003; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.custom_list_layout, container, false); // ----------おまじないここから---------- ProgressBar pBar = (ProgressBar) view.findViewById(android.R.id.progress); LinearLayout pframe = (LinearLayout) pBar.getParent(); pframe.setId(INTERNAL_PROGRESS_CONTAINER_ID); ListView listView = (ListView) view.findViewById(android.R.id.list); listView.setItemsCanFocus(false); FrameLayout lFrame = (FrameLayout) listView.getParent(); lFrame.setId(INTERNAL_LIST_CONTAINER_ID); // ----------おまじないここまで---------- return view; } }
custom_list_layout.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:background="@drawable/tile_bg" > <TextView android:id="@+id/tv_title" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="8dp" android:drawableLeft="@drawable/pin" android:drawablePadding="8dp" android:gravity="center_vertical" android:text="カスタマイズサンプル" android:textAppearance="?android:attr/textAppearanceMedium" /> <FrameLayout android:layout_width="match_parent" android:layout_height="wrap_content" > <ListView android:id="@id/android:list" android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingLeft="16dp" android:paddingRight="16dp" android:drawSelectorOnTop="false" /> </FrameLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:visibility="gone" > <ProgressBar android:id="@android:id/progress" style="?android:attr/progressBarStyleLarge" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout> <!-- アイテムが空の場合に使用するテキスト(idは定義済みのandroid:empty)--> <TextView android:id="@id/android:empty" android:layout_width="match_parent" android:layout_height="match_parent" android:text="No data" /> </LinearLayout>
解説
(*´ω`*)むふー。
要点を挙げます。
- カスタムレイアウトxml側
- ListViewをFrameLayoutで囲む(width/height重要)
- ProgressBarをLinearLayoutで囲む(width/height重要)
- ListFragment#onCreateView
- ListViewを囲んだFrameLayoutに0x00ff0002というIDを振る
- ProgressBarを囲んだLinearLayoutに0x00ff0003というIDを振る
これだけで、ListFragment#setListShownがカスタムレイアウトでも使えるようになります。
まとめ
ListFragmentさんが本来扱いたいIDやレイアウトを配置・設定しておく、というのが今回の肝でした。
人間側としてはかなり釈然としませんが、まあ動くので仕方ない。あとemptyさんがちゃんと動くか微妙。
しかし3.x Higherではどうすればいいんだろうなあ。