aamall

2013年06月17日

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用のアダプタークラスを作成
していきます。
と、その前に

キャプチeeeeャ





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

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

キャプrrrrrチャ





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

このレイアウトに合わせて、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 にしただけです。

キャプチャwer







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

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




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

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

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


お疲れ様でしたー










[!]Linuxをはじめよう!の運営にご協力ください
こちらよりAmazonギフト券による支援を募集しております。

受取人のEメールに hirohorse2-suplbl(アットマーク)yahoo.co.jp を設定してください
少額でも非常に助かりますので、お気に召されました記事がありましたら、何卒ご支援の方よろしくおねがいします。



hiroumauma at 00:28│Comments(8)TrackBack(0) Android | アプリ開発

トラックバックURL

この記事へのコメント

             
1. Posted by ヒロ   2014年05月10日 09:42
非常によく参考にさせてもらってます。 ありがとうございます。 指摘になってしまい恐縮なのですが、サンプルコード2番めの最後の部分
}
cache.put(key, image);
}

は誤記ではないでしょうか。 (その手前でメソッドは閉じていますので。。。)
2. Posted by hiroumauma   2014年05月10日 20:15
>ヒロさん

ご指摘は
2番目のコードの

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

この部分でしょうか?

cache.put() は setImageメソッド内の処理なので問題ないかと思います。
3. Posted by hiroumauma   2014年05月11日 10:06
>ヒロさん

ちょっと気になったので記事全コード
見なおしてやっとわかりました。

ImageGetTaskクラスのサンプルのところですね。

どうやら、サンプルコードを preタグで囲んで
記事に載せる際に僕が横着をして前のコードのタグ部分をコピーしたせいで一部
残ってしまっていたようです。

ご指摘の通りメソッド外部なので意味をなしません。

早速削除させて頂きました。

ご指摘ありがとうございます。
4. Posted by ヒロ   2014年05月13日 10:33
返信が遅くなり申し訳ありませんでした。

説明不足なところ、色々と
ご確認・ご対応いただき、ありがとうございました。
5. Posted by  かいり   2014年05月26日 17:37
大変参考にさせてもらっています。

現在Part4まで作成したのですが、アルバムのタイトルは取得できているのですがアルバムのアーティスト、登録されているトラック数の取得がうまく行きません。
なにか原因としてあげられるものはあるでしょうか?
簡単にで良いので回答いただけると幸いです。

なんど見返してもうまく行かなくて・・・
6. Posted by hiroumauma   2014年05月26日 18:27
>かいりさん

うーん…
申し訳ないのですが、コメントの情報だけでは決定的な原因は
思いつきませんでした。

アルバムタイトルが取得できているということは、
とりあえずcursorを取得して
 albums.add(new Album(cursor));
までは回っているはずです。

ひとまず Log.d などを使用して
この cursor に正しくアーティスト名やトラック数が格納
されているか確認してみてください。

while( cursor.moveToNext() )

のループ内で

Log.d("Artist", cursor.getString(cursor.getColumnIndex( MediaStore.Audio.Albums.ARTIST)));

などと記述しておいてデバッグ画面にアーティスト名が並べばOKです。
この時点で入っていない場合、取得に失敗しています。

参考> http://relog.xii.jp/mt5r/2011/03/android-7.html
実機端末でテストしている場合、まれに使用不可能なURIがあったりしますが…
(MediaStore.Audio.Artists.EXTERNAL_CONTENT_URIが使えないというのは考えづらいですが…)


cursorにはデータが正しく格納されている場合は、
データの表示やコピー作業のどこかでミスが発生いるのかと思います。

例えば、似た名前のメンバ変数を使用しており、
コピー用のコードのどこかで入れ替わっていたりする場合です。

記事掲載のコードは実際にエミュレータとXperia acroHDにて
動作を確認しておりますので、仕組み上は動かないということはないはずです。
ズバッ決定的な解決方法をお示しできればよいのですが、
力足らずで申し訳ありません。

7. Posted by かいり   2014年05月27日 09:57
ご丁寧な回答ありがとうございます。

回答を参考にして、デバッグ作業をしているところです。

LogCatを見ると、Can't open file reading.と表示されていました。
ずっと前から消えず、放置していたのですが関係ありそうでしょうか?

これが出ても、アルバムのタイトル等は取得できているので放置してて・・
プログラミング初心者なもので、わけのわからない質問で申し訳ないです。
8. Posted by かいり   2014年05月27日 12:51
知り合いに相談しながらデバッグをして、解決することが出来ました!

恥ずかしながら、
変数名が違ったため、おかしくなってたみたいです。。
お手数おかけしました!
ありがとうございました。

コメントする

名前
URL
 
  絵文字
 
 
記事検索
最新コメント
月別アーカイブ