見事にハマりました。FMS Player の曲一覧は ExpandableListVIew を用いて表示しています。Group としてアルバム名の一覧が表示されます。アルバムを押すと Child としてアルバム内の楽曲の一覧が表示されるというUIです。当初は、SimpleExpandableListAdapter を用いて実装していました。この Adapter は表示する内容をメモリ上に持ちます。ですので大量のデータを表示するのは難しいと思ってました。試しに私の楽曲データ、1800アルバム、 30000 曲以上を読み込ませてみたところ、見事に Out of memory で落ちてしまいました。そこで登場したのが SimpleCursorTreeAdapter です。ところが、この Adapter が曲者だったんです。
まずは、SimpleCursorTreeAdapter について簡単に説明します。SimpleCursorTreeAdapter は abstruct クラスで、継承したクラスを作成して、Cursor getChildrenCursor(Cursor groupCursor) を実装する必要があります。FMS Player の場合、アルバム内の楽曲一覧 Cursor を取得することになります。実際のコードは、
public class MusicListAdapter extends SimpleCursorTreeAdapter { public MusicListAdapter(Context context, Cursor cur, int groupLayout, String[] groupFrom, int[] groupTo, int childLayout, String[] childrenFrom, int[] childrenTo) { super(context, cur, groupLayout, groupFrom, groupTo, childLayout, childrenFrom, childrenTo); } @Override protected Cursor getChildrenCursor(Cursor groupCursor) { String album = musicDBHelper.getColumnString(groupCursor, Music.ALBUM); Cursor cur = musicDBHelper.GetAlbumMusic(album); return cur; } }
のようになります。やっていることは、musicDBHelper.getColumnString(groupCursor, Music.ALBUM) で groupCursor からアルバム名を取得、musicDBHelper.GetAlbumMusic(album) で、そのアルバムに含まれる楽曲の Cursor を返しているだけです。
次に、実際に Adapter を生成して登録するコードは、
private void ShowMusicList(String genre, String name, String artist) { try { categoryCursor = musicDBHelper.GetAlbums(genre, name, artist); MusicListAdapter adapter = new MusicListAdapter( this, categoryCursor, R.layout.music_list_album_item, new String[] {Music.ALBUM}, new int[] {R.id.groupText1}, R.layout.music_list_track_item, new String[] {Music.NAME, Music.ARTIST}, new int[] {R.id.childText1, R.id.childText2} ); musicList.setAdapter(adapter); } catch(Exception e) { Log.e(TAG, e.toString()); } }
のようになります。MusicListAdapter を new する際に、第2引数に musicDBHelper.GetAlbums(genre, name, artist) で取得したアルバム一覧の Cursor を指定しています。このカーソルを用いて Group 一覧が表示されるのでしょう。そして Group をクリックした時に先のコードで示した getChildrenCursor() が呼ばれて Child 一覧が表示されるという仕組みだと思われます。
これで間違いはないはずなのですが、動かしてみるとエラーが発生しました。
java.lang.IllegalStateException: Couldn't read row 0, col -1 from CursorWindow. Make sure the Cursor is initialized correctly before accessing data from it.
どうやら Cursor からデータを取得できていないようだということはわかります。musicDBHelper.GetAlbums() は内部でアルバム名のカラムを指定してデータを取得しており、musicDBHelper.GetAlbumMusic() は内部で、TrackIDというキー項目と、楽曲名とアーティスト名のカラムを指定していました。どれも取得できないはずはないのです。実際、それぞれのメソッドを単体で動かすと問題なく動きました。
ちなみに、SQLite に作成した Music テーブルの構造は以下のとおりです。
create table Music ( Track_id integer primary key, No integer, Name text, Artist text, Composer text, Album text, Genre text, Disc_number integer, Disc_count integer, Track_number integer, Track_count integer, Date_modified text, Date_added text, Location text )
当初 まったく見当がつかなかったのですが、別のところから答えが見つかりました。ExpandableListView の Click Listner です。
private ExpandableListView.OnChildClickListener mMusicListChildClickListenner = new ExpandableListView.OnChildClickListener() { @Override public boolean onChildClick(ExpandableListView parent, View view, int groupPosition, int childPosition, long id) { return false; } }
onChildClick() の最終引数に long id というのがあります。この id は当初システムで生成されるものだと思ったのですが、、もしかしたらデータベースから ID を返す必要があるのではないか?とすると、そのIDカラムをどのように指定するのか?Music テーブルの場合は Track_id がプライマリキーです。
Googleを検索してサンプルを色々と見てみると、プライマリキーのカラムが _id となっていることに気づきました。そこで Music テーブルの Track_id カラム名を _id に変更するとビンゴ!!見事に動きました。
テーブルのプライマリキーは _id とするというのが Android の流儀なんでしょうかね。見事にハマりました。