Android用(ちょっと)本格的なミュージックプレーヤの開発 part2

ミュージックプレーヤ開発 part2 です。

今回はアルバムの一覧を取得します。

アルバムの取得ではアルバムアートの処理も扱います。

アルバム管理用クラス

前回のトラックと同様にアルバム一覧、アーティスト一覧も
コンテントプロバイダから取得できます。
基本は前回と同じですのでいきなりクラスを掲載します。

●Album

public class Album {

        public long             id;
        public String           album;
        public String           albumArt;       
        public long             albumId;
        public String           albumKey;
        public String           artist;
        public int              tracks;

        public static final String[] FILLED_PROJECTION = {
                MediaStore.Audio.Albums._ID,
                MediaStore.Audio.Albums.ALBUM,
                MediaStore.Audio.Albums.ALBUM_ART,
                MediaStore.Audio.Albums.ALBUM_KEY,
                MediaStore.Audio.Albums.ARTIST,
                MediaStore.Audio.Albums.NUMBER_OF_SONGS,
        };

        public Album(Cursor cursor){  
                id       = cursor.getLong(  cursor.getColumnIndex( MediaStore.Audio.Albums._ID            ));
                album    = cursor.getString(cursor.getColumnIndex( MediaStore.Audio.Albums.ALBUM          ));
                albumArt = cursor.getString(cursor.getColumnIndex( MediaStore.Audio.Albums.ALBUM_ART      ));
                albumId  = cursor.getLong(  cursor.getColumnIndex( MediaStore.Audio.Media._ID             ));
                albumKey = cursor.getString(cursor.getColumnIndex( MediaStore.Audio.Albums.ALBUM_KEY      ));
                artist   = cursor.getString(cursor.getColumnIndex( MediaStore.Audio.Albums.ARTIST         ));
                tracks   = cursor.getInt(   cursor.getColumnIndex( MediaStore.Audio.Albums.NUMBER_OF_SONGS ));
        }

        public static List getItems(Context activity) {

                List albums = new ArrayList();
                ContentResolver resolver = activity.getContentResolver();
                Cursor cursor = resolver.query(
                                MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, 
                                Album.FILLED_PROJECTION, 
                                null, 
                                null,
                                "ALBUM  ASC"
                                );

                while( cursor.moveToNext() ){
                    albums.add(new Album(cursor));
                }
                cursor.close();
                return albums;
        }

}

基本は前回のTrackと全く同じです。
唯一違うのは getItems() において
resolver.query() の最後の引数に ASC オプションを追加して
レコードの並びを指定しています。
これで名前がABC・・・の順番に並び替わります。

以上で アルバムの一覧が取得できるようになりました。
次は肝心の表示についてです。
アルバムの一覧を表示する際に、いくつかテクニックが必要です。

というのも、、
アルバムの一覧を表示をするのならやはりアルバムアートの
表示に対応しておきたいからです。

アルバムアートの画像についてもコンテントプロバイダから画像へのパスを
取得することができるので、簡単に実装するなら、前回Trackの処理において
TextView となっているところを ImageView に書き換えてやるだけで
 "一応" 表示することはできるようになります。

ただ、この場合少しだけ問題があります。
アルバムが数タイトルしか保存してない端末であれば問題ないのですが、
アルバムが数十からそれ以上になってくると、ロードに時間がかかり、
リストがカクカクとした動作になってしまうのです。。。

次々と数百pxの画像を取得してListViewにロードしていれば
カクついてしまうのも無理はありません。。。

これを防止するために、画像の読み込み部分について少しだけ
工夫する必要があります。


・アルバムアートをAsyncTaskで非同期処理で読み込む
・アルバムアート用画像は縮小してキャッシュする

これでアルバムアートがなめらかに表示できます。

まずは画像のキャッシュ部分を先につくります。
HashMapを利用した超シンプルなメモリキャッシュです。

public  class ImageCache {
    private static HashMap<String,Bitmap> cache = new HashMap<String,Bitmap>();  

    public static Bitmap getImage(String key) {  
        if (cache.containsKey(key)) {  
            return cache.get(key);  
        }  
        return null;  
    }  

    public static void setImage(String key, Bitmap image) {  
        cache.put(key, image);  
    }  

} 

setImageでキーと画像を登録しておいて
getImageでキーを問い合わせると対応する画像を返してくれます。

本来はメモリ使用量のチェックなどをするべきですが、後述しますが
アルバムアート用画像は 72x72px に圧縮するので 何百アルバムも
保存されなければ大丈夫だろう ということで手をぬいてしまっています。。。
気になる方は確認するようにしてください。。。

このImageCacheを利用して、非同期で画像を読み込むImageGetTask
というクラスを作ります。これにはメインスレッドとは別スレッドで非同期処理が
行える"AsyncTask"を使って実現します。

このクラスにはついでに画像を、好みのサイズに変形してデコードする
機能もつけておきましょう。

class ImageGetTask extends  AsyncTask<String,Void,Bitmap> {
     private ImageView image;
     private String    tag;

     public ImageGetTask(ImageView _image){
         super();
         image = _image;
         tag   =  image.getTag().toString();
     }

     @Override
     protected Bitmap doInBackground(String... params) {
         Bitmap bitmap = ImageCache.getImage(params[0]);
         if(bitmap==null){
             bitmap = decodeBitmap(params[0],72,72);
             ImageCache.setImage(params[0], bitmap);
         }
         return bitmap;
     }

     @Override
     protected void onPostExecute(Bitmap result) {
         if(tag.equals(image.getTag()))image.setImageBitmap(result);
     }

     public static Bitmap decodeBitmap(String path, int width, int height){
         final BitmapFactory.Options options = new BitmapFactory.Options();  
             options.inJustDecodeBounds = true;  
             BitmapFactory.decodeFile(path, options); 
             options.inSampleSize = calculateInSampleSize(options, width, height);
             options.inJustDecodeBounds = false;  
        return BitmapFactory.decodeFile(path, options);    
     }

     public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {  

        final int height = options.outHeight;  
        final int width = options.outWidth;  
        int inSampleSize = 1;  

        if (height > reqHeight || width > reqWidth) {  
            if (width > height) {  
                inSampleSize = Math.round((float)height / (float)reqHeight);  
            } else {  
                inSampleSize = Math.round((float)width / (float)reqWidth);  
            }  
        }  
        return inSampleSize;  
    }

}

calculateInSampleSize() は こちら をそのままコピー参考にさせて頂きました。
機能としては、
ImageViewを登録しておいて、指定した画像を別スレッドで読み込み、
キャッシュに縮小画像があればそれを使う、
なければ新たに読み込んで72x72pxに縮小してからキャッシュに保存する。
最後に読み込みが終わったらImageViewに反映させる
ということをやっています。

最後に以上の準備を踏まえて前回と同様にListView用のアダプタークラスを作成
していきます。
と、その前に

アルバムアートが登録されていないアルバムも多いので、それらのためにダミーの
画像を用意しておきます。
通常のサイズが 200 x 200 slim と書いてあるものは 72 x 72です。

この画像は こちらの記事 のサンプルに入っていた画像をちょっとお借りしています。
実際にマーケットなどに出す際はオリジナルの画像を用意しましょう。

画像が用意できたら、レイアウトを組みます。
アルバムタイトルとアーティスト、登録されているトラック数を表示するようにしました。

このレイアウトに合わせて、ListAlbumAdapter を作成します。

public class ListAlbumAdapter extends ArrayAdapter<Album> {

                LayoutInflater mInflater;
                static Context Mcontext;

        public ListAlbumAdapter(Context context, List<Album> item){
                super(context, 0, item);
                mInflater =  (LayoutInflater) context.getSystemService( Context.LAYOUT_INFLATER_SERVICE );
                Mcontext = context;
        }

        @Override
        public View getView(int position, View convertView,ViewGroup parent){

                Album item = getItem(position); 
                ViewHolder holder;

                if(convertView==null){
                        convertView = mInflater.inflate(R.layout.item_album, null);
                        holder = new ViewHolder();
                        holder.albumTextView    = (TextView)convertView.findViewById(R.id.title);
                        holder.artistTextView   = (TextView)convertView.findViewById(R.id.artist);
                        holder.tracksTextView   = (TextView)convertView.findViewById(R.id.tracks);
                        holder.artworkImageView = (ImageView)convertView.findViewById(R.id.albumart);
                        convertView.setTag(holder);
                }else{
                        holder = (ViewHolder) convertView.getTag();
                }

                holder.albumTextView.setText(item.album);
                holder.artistTextView.setText(item.artist);
                holder.tracksTextView.setText(String.valueOf(item.tracks)+"tracks");

                String path = item.albumArt;
                holder.artworkImageView.setImageResource(R.drawable.dummy_album_art_slim_gray);
                if(path==null){
                        path = String.valueOf( R.drawable.dummy_album_art_slim);
                        Bitmap bitmap = ImageCache.getImage(path);
                        if(bitmap==null){
                                bitmap = BitmapFactory.decodeResource(Mcontext.getResources(),R.drawable.dummy_album_art_slim);
                                ImageCache.setImage(path, bitmap);
                        }
                }
                holder.artworkImageView.setTag(path);
                ImageGetTask task = new ImageGetTask(holder.artworkImageView);
                task.execute(path);

                return convertView;     
        }

        static class ViewHolder{
                TextView  albumTextView;
                TextView  artistTextView;
                TextView  tracksTextView;
                ImageView artworkImageView;
        }
}

基本は前回と同じです。
holderに アルバムタイトルやアーティストの名前を入れるところまではいいと思います。

String path = item.albumArt;

これ以下の行が画像の読み込み部分です。
最初に 一時措置として グレー処理した画像を登録しておきます。
次に、アルバムアートの画像のパスがnull で登録されていない場合は
青い正式版のダミーアートを使うようにします。

path = String.valueOf( R.drawable.dummy_album_art_slim);

は本当はパスなど入っていませんが、イメージキャッシュに登録するため
の手順を揃えるためにわざとこのような書き方にしています。

        holder.artworkImageView.setTag(path);
        ImageGetTask task = new ImageGetTask(holder.artworkImageView);
        task.execute(path);

最後にこの部分で実際に非同期処理を実現しています。
まずは 表示したい ImageViewにタグとしてpathを保存しておきます。
次にImageGetTaskを生成して今のImageViewを登録、
最後に表示したい画像のpathを与えて 小タスクを実行します。

このように記述しているのは、あまりにも素早くスクロールが行われた場合
小タスクが画像を読み込む前にListViewのアイテムが画面外にでて
再利用されてしまい、表示したいアイテムと実際に表示される画像がずれて
しまうことがあるためです。

上記のImageGetTaskのonPostExecute() にて
タグが合っているかチェックしてから画像を表示しています。

この実装で、アルバム一覧を開いた瞬間はグレーの一時画像が表示され、
別タスクで読み込みが終わった画像から順次本物のアートに置き換わって
いきます。別タスクなのでリストの挙動はなめらかなままなわけです。
SONYのWALKMANアプリのリストと似た感じです。。。

これで本当の本当に準備完了です。
MainActivityの整備をしていないのでとりあえず前回のトラック一覧を
コメントアウトして アルバム一覧を表示してみます。

        List<Album> albums = Album.getItems(this);
        ListView trackList = (ListView)findViewById(R.id.list);
        ListAlbumAdapter adapter = new ListAlbumAdapter(this, albums);
        trackList.setAdapter(adapter);

追加するのは前回と全く同じで Track を Album にしただけです。

実際に起動してみるとこのとおり、アルバム一覧が表示されました。
画像はエミュレータでのものなのですべてダミー画像が表示されています。

アルバムアートが登録されている実機で起動すればそのアルバムアートが
表示されます。

今回は以上です。
アルバム一覧はアルバムアートが表示される分、特に実機で起動してみると
"ミュージックライブラリを作っている感" があっていいですね。

アート画像を登録した大量のアルバムをスクロールすると、
狙いどおりになめらかにスクロールしながら画像が次々と読み込まれていきます!
なかなか気持ちいいです。

次回は、アーティストの一覧表示はトラックと変わらないのでさらっと済ませてから
放置していたメインアクティビティをいじります。
今日は前回のトラック一覧を殺して表示テストをしたので、次回は横スワイプで
トラック一覧やアルバム一覧が移り変わるようにしたいです。

お疲れ様でしたー

コメントを追加する