2011年7月5日火曜日

Android ListFragment でカスタムレイアウトを使うと setEmptyText() が使えない

もともとは adakoda さんのブログのエントリ

[Android] ListFragment の View をカスタマイズする方法 - adakoda - http://goo.gl/UBgyu

で、onCreateView() を override してカスタムレイアウトを指定すると setEmptyText() で落ちるという話があって、正しい ID を指定してないからでは〜?的な流れになったのですが、どうもそうではないようなので調べてみました。

まず、setEmptyText(CharSequence text) は API Level 11 (Android 3.0) で追加された API です。

結果をまとめると

・ListActivity
  ・setEmptyView() : 使える
  ・getEmptyView() : 使える
  ・@android:id/list, @android:id/empty を使った
   カスタムレイアウト : 使える

・ListFragment
  ・@android:id/list, @android:id/empty を使った
   カスタムレイアウト : 使える

  ・カスタムレイアウトを使わない場合
    ・getListView().setEmptyView() : なにもおこらない
    ・getListView().getEmptyView() : 使える
    ・setEmptyText() : 使える

  ・カスタムレイアウトを使った場合
    ・getListView().setEmptyView() : なにもおこらない
     (@android:id/empty がそのまま表示される)
    ・getListView().getEmptyView() : 使える
    ・setEmptyText() : 落ちる

ListFragment で、カスタムレイアウトで setEmptyText() を使いたい場合は、次のように override しておくこと

@Override
public void setEmptyText(CharSequence text) {
TextView tv = (TextView)getListView().getEmptyView();
tv.setText(text);
}


ちなみに、このときのカスタムレイアウトはこんな感じ

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<ListView
android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:drawSelectorOnTop="false" />

<TextView
android:id="@android:id/empty"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="No data"
android:textSize="30sp"/>
</FrameLayout>


さて、結論をいったので、それまでの過程もちょっと紹介。

まずは、ListFragment の onCreateView() で生成される View のなかから empty 用っぽい View を見つけて、それの ID から resourceName とればわかるんじゃね?と思ってやってみました。


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

View v = super.onCreateView(inflater, container, savedInstanceState);
Log.d("NoteListFragment", v.getClass().getName() + ", " + v.getId());

if (v instanceof ViewGroup) {
ViewGroup vg = (ViewGroup) v;
for (int i = 0; i < vg.getChildCount(); i++) {
View child = vg.getChildAt(i);

String resourcename = getResourceNameByID(android.R.id.class, child.getId());

Log.d("NoteListFragment", child.getClass().getName() + ", "
+ child.getId() + ", " + resourcename);

if(child instanceof ViewGroup) {
ViewGroup vg2 = (ViewGroup) child;
for (int j = 0; j < vg2.getChildCount(); j++) {
View child2 = vg2.getChildAt(j);

String resourcename2 = getResourceNameByID(android.R.id.class, child2.getId());

Log.d("NoteListFragment", child2.getClass().getName() + ", "
+ child2.getId() + ", " + resourcename2);
}
}
}
}
return v;
}

public String getResourceNameByID(Class<?> clazz, int resourceId)
throws IllegalArgumentException {

Field[] fields = clazz.getFields();
for (Field f : fields) {
try {
if (resourceId == f.getInt(null)) {
return f.getName();
}
} catch (Exception e) {
e.printStackTrace();
throw new IllegalArgumentException();
}
}
return null;
}


リフレクション。リフレクション。
結果はこんな感じ

07-05 13:46:40.090: DEBUG/NoteListFragment(30597): android.widget.FrameLayout, -1
07-05 13:46:40.090: DEBUG/NoteListFragment(30597): android.widget.LinearLayout, 16908904, null
07-05 13:46:40.090: DEBUG/NoteListFragment(30597): android.widget.ProgressBar, -1, null
07-05 13:46:40.090: DEBUG/NoteListFragment(30597): android.widget.TextView, -1, null
07-05 13:46:40.090: DEBUG/NoteListFragment(30597): android.widget.FrameLayout, 16908905, null
07-05 13:46:40.090: DEBUG/NoteListFragment(30597): android.widget.ListView, 16908298, list
07-05 13:46:40.090: DEBUG/NoteListFragment(30597): android.widget.TextView, 16908906, null

ListView には list (= android.R.id.list) が割当てられているのに、その下の TextView (EmptyView だと思われる) には null が入っている。つまり、android.R.id.** に対象の ID がなかったということです。コードで動的に生成してるのかな?

念のためこの TextView が EmptyView なのかチェック


@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
...

setEmptyText(getActivity().getString(R.string.no_notes));
Log.d("NoteListFragment", getListView().getEmptyView().getId() + "");
}


結果は

07-05 13:56:22.510: DEBUG/NoteListFragment(30953): 16908906

ビンゴ。

ちなみに setEmptyText() する前に getListView().getEmptyView().getId() するとヌルポになります。
EmptyView が生成されてないんですね。このことからも EmptyView が動的に生成されてるのがわかります。

なので、レイアウトをカスタムしないで setEmptyText を override するときは super.setEmpty() を忘れずに最初にしてください。そうしないとその後の getListView().getEmptyView() には null が返ってきてしまいます。


@Override
public void setEmptyText(CharSequence text) {
super.setEmptyText(text); // don't forget
TextView tv = (TextView)getListView().getEmptyView();
tv.setText(text);
}



カスタムを使っていてもいなくても、カスタムレイアウトに android:empty が入っていてもいなくても動くようにするには、こんな感じかなぁ。。。


@Override
public void setEmptyText(CharSequence text) {
try {
super.setEmptyText(text);
}
catch (IllegalStateException e1){
e1.printStackTrace();
Log.d("NoteListFragment", e1.getClass().getName() + " : " + e1.getMessage());

try {
TextView tv = (TextView)getListView().getEmptyView();
tv.setText(text);
}
catch(NullPointerException e2) {
e2.printStackTrace();
Log.d("NoteListFragment", e2.getClass().getName() + " : " + e2.getMessage());

TextView tv = new TextView(getActivity());
tv.setText(text);
tv.setId(android.R.id.empty);

ViewParent v = getListView().getParent();
if(v instanceof ViewGroup) {
((ViewGroup)v).addView(tv);
}
getListView().setEmptyView(tv);
}
}
}





  

1 件のコメント:

  1. setListShownも
    mProgressContainer = root.findViewById(com.android.internal.R.id.progressContainer);
    で適当なのが取得できなくて死ぬ罠…(´・ω・`) ListFragmentはデフォView以外使うべきではないってことなのかなぁ

    返信削除