Androidアプリ開発逆引きレシピに、リスト表示するサンプルアプリ「043 最後まで表示したら自動で項目が追加されるようにしたい」があります。このサンプルは、Activity、ListViewを使って実装しており、追加読み込みには、OnScrollListener と非同期処理を扱うAsyncTaskを使っています。
最近は、AsyncTaskではなく、AsyncTaskLoaderを使うのが流行りのようです。そこで、ListFragmentを組み合わせて、「text数字」という項目からなるリストを表示し、上にプルアップすると、さらにデータを読み込み表示するサンプルアプリを作ってみました。AsyncTaskLoaderでは、非同期処理を逐次的に行うのはそれほど自明ではないようで、ソースを下手に書くと、前に表示されていたリストデータと新たに追加されたリストデータが混在して表示されてしまいます。この点、AsyncTaskを使う場合、追加読み込みは、AsyncTaskのインスタンスを新たに生成し、executeメソッドを呼べばいいだけで、楽でした(※ 注: 非同期処理が走っているかのチェックをして、走っている場合は、追読み込みをしない処理は必要でした)。AsyncTaskLoaderを逐次的に呼べるようにした方法は以下の通りです。
* AsyncTaskLoaderを逐次的に行うようにするには、非同期処理を実行中かのmIsLoadingというboolean変数を導入し、非同期処理開始時、mIsLoadingをtrueにして、リストプルアップで呼ばれる additionalReadingメソッド内で、mIsLoadingの値をチェックをし、trueの場合、returnして、非同期処理を新たに呼ばないようにしました。1つの非同期処理が終了したとき呼ばれるonLoadFinished メソッド内で、mIsLoadingをfalseにしました。
* AsyncTaskLoaderを(逐次的に)複数回呼べるようにするために、コンストラクタに、非同期処理が何回目に呼ばれたかの引数を加え、AsyncTaskLoaderの初期化の際、initLoaderの第1引数に非同期処理が呼ばれる回数を入れました。
以下は実装したソースです。
ローダーのアイテム用データクラス
public class Entry { private String mLabel; public String getLabel() { return mLabel; } public void setLabel(String label) { mLabel = label; } @Override public String toString() { return mLabel; } }
アダプター
import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.TextView; import java.util.List; public class CustomAdapter extends ArrayAdapter<Entry> { private LayoutInflater mLayoutInflater; public CustomAdapter(Context context) { super(context, android.R.layout.simple_list_item_1); mLayoutInflater = (LayoutInflater)context .getSystemService(Context.LAYOUT_INFLATER_SERVICE); } public void setData(List<Entry> data) { clear(); if (data != null) { // addAll(data); // addAll is usable from API level 11. for (Entry entry: data) { add(entry); } } } @Override public View getView(int position, View convertView, ViewGroup parent) { // 特定の行(position)のデータを取得 Entry item = (Entry)getItem(position); // 同じ行に表示されるViewは使い回しされるため初回だけ生成 if (null == convertView) { convertView = mLayoutInflater.inflate( R.layout.list_item, null); } // データをViewの各Widgetにセット TextView textView = (TextView)convertView.findViewById(R.id.text); textView.setText(item.toString()); return convertView; } }
AsyncTaskLoader
import android.content.Context; import android.support.v4.content.AsyncTaskLoader; import android.util.Log; import java.text.Collator; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; public class CustomLoader extends AsyncTaskLoader<List<Entry>> { List<Entry> list; static List<Entry> sOldList; static List<Entry> sData; int mCount; int m; private static final String TAG = CustomLoader.class.getSimpleName(); public CustomLoader(Context context, int count) { super(context); mCount = count; } /** * バックグラウンドでローダ用のデータを読み込む */ @Override public List<Entry> loadInBackground() { // 2秒止める try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } if (mCount == 0) { sData = new ArrayList<Entry>(); m = 0; } else { // それまでのリスト sData = sOldList; m = sData.size(); } Log.d(TAG, "m = " + m); for (int i = 0; i < 30; i++) { Entry entry = new Entry(); entry.setLabel("text" + (i + m)); sData.add(entry); } // Entry用のリストを作成 List<Entry> entries = new ArrayList<Entry>(sData.size()); for (int i = 0; i < sData.size(); i++) { Entry entry = sData.get(i); entries.add(entry); } // このソートは不要。AsyncTaskLoaderを何回も呼ぶ時、逐次的に、前の処理が終わったら // 次の処理を呼ぶ場合は、データ順序が正しくなるが、このアプリの実装初期、 // 築時的に正しく呼ばれていなく、データの順序が正しくならなかったので、 // リストのソートを実装した。 // リストをソート Collections.sort(entries, CUSTOM_COMPARATOR); // entriesの内容をsOldListにコピー sOldList = new ArrayList<Entry>(); for (Entry entry: entries) { sOldList.add(entry); } return entries; } /** * 提供する新しいデータがあるときに呼び出される */ @Override public void deliverResult(List<Entry> data) { if (isReset()) { // リセット時(または最初に読み込みが開始されていない、もしくは // reset()が呼び出された後)現在の非同期クエリを解放 if (data != null) { onReleaseResources(data); } return; } List<Entry> oldEntries = data; sData = data; if (isStarted()) { // 読み込みが開始されている (startLoading()が呼び出されているが // stopLoading()やreset()はまだ呼び出されていない)場合いn、その結果を返す super.deliverResult(data); } // この時点で、必要であれば oldEntries に関連するリソースを解放できる if (oldEntries != null && oldEntries != data) { onReleaseResources(oldEntries); } } @Override protected void onStartLoading() { forceLoad(); } @Override protected void onStopLoading() { cancelLoad(); } @Override public void reset() { super.reset(); onStopLoading(); } /** * 読み込んだデータ・セットに関連するリソースを解放するヘルパーメソッド */ protected void onReleaseResources(List<Entry> apps) { // Cursorの場合は閉じる // 単純なリストList<>の場合は特に何もしない } /** * Entry用のComparator */ public static final Comparator<Entry> CUSTOM_COMPARATOR = new Comparator<Entry>() { // private final Collator sCollator = Collator.getInstance(); @Override public int compare(Entry object1, Entry object2) { // 文字順だと、数字の順にならない // return sCollator.compare(object1.getLabel(), object2.getLabel()); String str1 = object1.toString(); String str2 = object2.toString(); int length1 = str1.length(); int length2 = str2.length(); int num1 = Integer.valueOf(str1.substring(4, length1)); int num2 = Integer.valueOf(str2.substring(4, length2)); // 整数引数をとるCollator#compareはない // return sCollator.compare(num1, num2); if (num1 > num2) { return +1; } else if (num1 == num2) { return 0; } else { return -1; } } }; }
ListFragment
import android.os.Bundle; import android.support.v4.app.ListFragment; import android.support.v4.app.LoaderManager; import android.support.v4.content.Loader; import android.util.Log; import android.widget.AbsListView; import java.util.List; public class CustomListFragment extends ListFragment implements LoaderManager.LoaderCallbacks<List<Entry>>, AbsListView.OnScrollListener { private CustomAdapter mCustomAdapter; private boolean mIsLoading = false; private int mCount = 0; private final static String TAG = CustomListFragment.class.getSimpleName(); @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); mCustomAdapter = new CustomAdapter(getActivity()); setListAdapter(mCustomAdapter); // スクロールリスナーを設定 getListView().setOnScrollListener(this); setListShown(false); mIsLoading = true; getLoaderManager().initLoader(0, null, this); } @Override public Loader onCreateLoader(int id, Bundle args) { return new CustomLoader(getActivity(), mCount); } @Override public void onLoadFinished(Loader<List<Entry>> loader, List<Entry> data) { mCustomAdapter.setData(data); mIsLoading = false; setListShown(true); mCount++; } @Override public void onLoaderReset(Loader<List<Entry>> loader) { mCustomAdapter.setData(null); } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { if (totalItemCount == firstVisibleItem + visibleItemCount) { additionalReading(); } } @Override public void onScrollStateChanged(AbsListView view, int scrollState) { } private void additionalReading() { Log.d(TAG, "mIsLoading = " + mIsLoading); // 既に読み込み中ならスキップ if (mIsLoading) { return; } setListShown(false); mIsLoading = true; getLoaderManager().initLoader(mCount, null, this); } }
MainActivity
import android.support.v4.app.FragmentActivity; import android.os.Bundle; import android.support.v4.app.FragmentTransaction; import android.view.Menu; import android.view.MenuItem; public class MainActivity extends FragmentActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); CustomListFragment frag = new CustomListFragment(); FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); ft.add(R.id.main, frag, "CustomListFragment"); ft.commit(); } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.main, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); if (id == R.id.action_settings) { return true; } return super.onOptionsItemSelected(item); } }
リストのレイアウトファイル list_item.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text=""/> </LinearLayout>
Fragmentのレイアウトファイル fragment_sample.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical"> <ListView android:id="@+id/ListView" android:layout_width="fill_parent" android:layout_height="0dip" android:layout_weight="1" /> </LinearLayout>
Activityのレイアウトファイル activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:id="@+id/main" tools:context=".MainActivity"> </LinearLayout>
問題点
* データ読み込み開始時、setListShown(false)を呼ばないと、ずっとプログレスバーが表示され続ける。
* プログレスバーが画面中央に表示される。フッターの位置に表示したいのだけど、方法が不明。Activity + AsyncTaskでは簡単なんだけど。
GitHubに置いたソースコード
andropenguin/CustomListFragementSample4
参考
* Androidアプリ開発逆引きレシピ
* Android UI Cookbook for 4.0 ICS アプリ開発術
追記(Aug 20, 2014)
本記事中のコードには反映していないが、GitHubで、ソースコードに以下の変更を行う。上記問題点は解消された。
GitHubに置いたソースコード
andropenguin/CustomListFragementSample4
* プログレスバーの位置を画面下部に変える。
* レイアウトファイルで、ListViewのidを@id/android:listに変える。
* setListShownを使うとアプリが落ちるし、データ読み込み中、リストが非表示になるのは見栄えが悪いので、使用をやめる。