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);
}
}
}





  

2 件のコメント:

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

    返信削除
  2. Want To Boost Your ClickBank Traffic And Commissions?

    Bannerizer makes it easy for you to promote ClickBank products with banners, simply go to Bannerizer, and grab the banner codes for your favorite ClickBank products or use the Universal ClickBank Banner Rotator Tool to promote all of the ClickBank products.

    返信削除