implement LazyCoverView

This commit is contained in:
Adrian Ulrich 2015-08-09 12:25:40 +02:00
parent 8393184d49
commit 93b750dacd
5 changed files with 250 additions and 6 deletions

View File

@ -23,6 +23,15 @@ THE SOFTWARE.
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal">
<ch.blinkenlights.android.vanilla.LazyCoverView
android:id="@+id/cover"
android:background="?android:attr/selectableItemBackground"
android:longClickable="true"
android:scaleType="fitCenter"
android:layout_width="44dip"
android:layout_height="44dip"
android:visibility="gone"
/>
<TextView
android:id="@+id/text"
android:longClickable="true"

View File

@ -0,0 +1,216 @@
/*
* Copyright (C) 2015 Adrian Ulrich <adrian@blinkenlights.ch>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package ch.blinkenlights.android.vanilla;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.AttributeSet;
import android.widget.ImageView;
import android.util.LruCache;
/**
* LazyCoverView implements a 'song-aware' ImageView
*
* View updates should be triggered via setCover(type, id) to
* instruct the view to load the cover from its own LRU cache.
*
* The cover will automatically be fetched & scaled in a background
* thread on cache miss
*/
public class LazyCoverView extends ImageView
implements Handler.Callback
{
/**
* Context of constructor
*/
private Context mContext;
/**
* UI Thread handler
*/
private static Handler sUiHandler;
/**
* Worker thread handler
*/
private static Handler sHandler;
/**
* The fallback cover image resource encoded as bitmap
*/
private static Bitmap sFallbackBitmap;
/**
* Bitmap LRU cache
*/
private static BitmapCache sBitmapCache;
/**
* The key we are expected to draw
*/
private String mExpectedKey;
/**
* Dimension of cached pictures in pixels
*/
private int mImageDimensionPx;
/**
* Bitmap cache LRU cache implementation
*/
private class BitmapCache extends LruCache<String, Bitmap> {
public BitmapCache(int size) {
super(size);
}
}
/**
* Cover message we are passing around using mHandler
*/
private static class CoverMsg {
public int type; // Media type
public long id; // ID of this media type to query
public LazyCoverView view; // The view we are updating
CoverMsg(int type, long id, LazyCoverView view) {
this.type = type;
this.id = id;
this.view = view;
}
public String getKey() {
return this.type +"/"+ this.id;
}
/**
* Returns true if the view still requires updating
*/
public boolean isRecent() {
return this.getKey().equals(this.view.mExpectedKey);
}
}
/**
* Constructor of class inflated from XML
*
* @param context The context of the calling activity
* @param attributes attributes passed by the xml inflater
*/
public LazyCoverView(Context context, AttributeSet attributes) {
super(context, attributes);
mContext = context;
}
/**
* Setup the handler of this view instance. This function
* must be called before calling setCover().
*
* @param looper The worker thread to use for image scaling
*/
public void setup(Looper looper) {
if (sBitmapCache == null) {
sBitmapCache = new BitmapCache(255); // Cache up to 255 items
}
if (sFallbackBitmap == null) {
sFallbackBitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.fallback_cover);
}
if (sUiHandler == null) {
sUiHandler = new Handler(this);
}
if (sHandler == null) {
sHandler = new Handler(looper, this);
}
// image dimension we are going to cache - we should probably calculate this
mImageDimensionPx = 128;
}
/**
* mHandler and mUiHandler callbacks
*/
private static final int MSG_CACHE_COVER = 60;
private static final int MSG_DRAW_COVER = 61;
@Override
public boolean handleMessage(Message message) {
switch (message.what) {
case MSG_CACHE_COVER: {
CoverMsg payload = (CoverMsg)message.obj;
if (payload.isRecent() == false) {
// This RPC is already obsoleted: drop it
break;
}
Bitmap bitmap = sBitmapCache.get(payload.getKey());
if (bitmap == null) {
Song song = MediaUtils.getSongByTypeId(mContext.getContentResolver(), payload.type, payload.id);
if (song != null) {
bitmap = song.getCover(mContext);
}
if (bitmap == null) {
bitmap = sFallbackBitmap;
}
bitmap = Bitmap.createScaledBitmap(bitmap, mImageDimensionPx, mImageDimensionPx, true);
sBitmapCache.put(payload.getKey(), bitmap);
}
sUiHandler.sendMessage(sUiHandler.obtainMessage(MSG_DRAW_COVER, payload));
break;
}
case MSG_DRAW_COVER: {
CoverMsg payload = (CoverMsg)message.obj;
synchronized(payload.view) {
if (payload.isRecent()) {
drawFromCache(payload);
}
}
}
default:
return false;
}
return true;
}
/**
* Attempts to set the image of this cover
*
* @param type The Media type
* @param id The id of this media type to query
*/
public void setCover(int type, long id) {
CoverMsg payload = new CoverMsg(type, id, this);
mExpectedKey = payload.getKey();
if (drawFromCache(payload) == false) {
// We send this delayed to avoid a cache-miss-storm if the user scrolls
// quickly in the listview
sHandler.sendMessageDelayed(sHandler.obtainMessage(MSG_CACHE_COVER, payload), 200);
}
}
/**
* Updates the view with a cached bitmap
* A fallback image will be used on cache miss
*
* @param payload The cover message containing the cache key and view to use
*/
public boolean drawFromCache(CoverMsg payload) {
boolean cacheHit = true;
Bitmap bitmap = sBitmapCache.get(payload.getKey());
if (bitmap == null) {
cacheHit = false;
bitmap = sFallbackBitmap;
}
payload.view.setImageBitmap(bitmap);
return cacheHit;
}
}

View File

@ -309,29 +309,30 @@ public class LibraryPagerAdapter
LayoutInflater inflater = activity.getLayoutInflater();
LibraryAdapter adapter;
LinearLayout header = null;
Looper looper = mWorkerHandler.getLooper();
switch (type) {
case MediaUtils.TYPE_ARTIST:
adapter = mArtistAdapter = new MediaAdapter(activity, MediaUtils.TYPE_ARTIST, null);
adapter = mArtistAdapter = new MediaAdapter(activity, MediaUtils.TYPE_ARTIST, null, looper);
mArtistAdapter.setExpandable(mSongsPosition != -1 || mAlbumsPosition != -1);
mArtistHeader = header = (LinearLayout)inflater.inflate(R.layout.library_row_expandable, null);
break;
case MediaUtils.TYPE_ALBUM:
adapter = mAlbumAdapter = new MediaAdapter(activity, MediaUtils.TYPE_ALBUM, mPendingAlbumLimiter);
adapter = mAlbumAdapter = new MediaAdapter(activity, MediaUtils.TYPE_ALBUM, mPendingAlbumLimiter, looper);
mAlbumAdapter.setExpandable(mSongsPosition != -1);
mPendingAlbumLimiter = null;
mAlbumHeader = header = (LinearLayout)inflater.inflate(R.layout.library_row_expandable, null);
break;
case MediaUtils.TYPE_SONG:
adapter = mSongAdapter = new MediaAdapter(activity, MediaUtils.TYPE_SONG, mPendingSongLimiter);
adapter = mSongAdapter = new MediaAdapter(activity, MediaUtils.TYPE_SONG, mPendingSongLimiter, looper);
mPendingSongLimiter = null;
mSongHeader = header = (LinearLayout)inflater.inflate(R.layout.library_row_expandable, null);
break;
case MediaUtils.TYPE_PLAYLIST:
adapter = mPlaylistAdapter = new MediaAdapter(activity, MediaUtils.TYPE_PLAYLIST, null);
adapter = mPlaylistAdapter = new MediaAdapter(activity, MediaUtils.TYPE_PLAYLIST, null, looper);
break;
case MediaUtils.TYPE_GENRE:
adapter = mGenreAdapter = new MediaAdapter(activity, MediaUtils.TYPE_GENRE, null);
adapter = mGenreAdapter = new MediaAdapter(activity, MediaUtils.TYPE_GENRE, null, looper);
mGenreAdapter.setExpandable(mSongsPosition != -1);
break;
case MediaUtils.TYPE_FILE:

View File

@ -28,6 +28,7 @@ import android.database.Cursor;
import android.database.DatabaseUtils;
import android.graphics.Color;
import android.net.Uri;
import android.os.Looper;
import android.provider.BaseColumns;
import android.provider.MediaStore;
import android.text.Spannable;
@ -76,6 +77,7 @@ public class MediaAdapter
* The current data.
*/
private Cursor mCursor;
private Looper mLooper;
/**
* The type of media represented by this adapter. Must be one of the
* MediaUtils.FIELD_* constants. Determines which content provider to query for
@ -134,6 +136,10 @@ public class MediaAdapter
* If true, show the expander button on each row.
*/
private boolean mExpandable;
/**
* If true, return views with covers and fire callbacks
*/
private boolean mHasCoverArt;
/**
* Construct a MediaAdapter representing the given <code>type</code> of
@ -145,12 +151,13 @@ public class MediaAdapter
* and what fields to display in the views.
* @param limiter An initial limiter to use
*/
public MediaAdapter(LibraryActivity activity, int type, Limiter limiter)
public MediaAdapter(LibraryActivity activity, int type, Limiter limiter, Looper looper)
{
mActivity = activity;
mType = type;
mLimiter = limiter;
mInflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mLooper = looper;
switch (type) {
case MediaUtils.TYPE_ARTIST:
@ -169,6 +176,7 @@ public class MediaAdapter
mSongSort = MediaUtils.ALBUM_SORT;
mSortEntries = new int[] { R.string.name, R.string.artist_album, R.string.year, R.string.number_of_tracks, R.string.date_added };
mSortValues = new String[] { "album_key %1$s", "artist_key %1$s,album_key %1$s", "minyear %1$s,album_key %1$s", "numsongs %1$s,album_key %1$s", "_id %1$s" };
mHasCoverArt = true;
break;
case MediaUtils.TYPE_SONG:
mStore = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
@ -178,6 +186,7 @@ public class MediaAdapter
R.string.artist_year, R.string.album_track, R.string.year, R.string.date_added, R.string.song_playcount };
mSortValues = new String[] { "title_key %1$s", "artist_key %1$s,album_key %1$s,track %1$s", "artist_key %1$s,album_key %1$s,title_key %1$s",
"artist_key %1$s,year %1$s,track %1$s", "album_key %1$s,track %1s", "year %1$s,title_key %1$s", "_id %1$s", SORT_MAGIC_PLAYCOUNT };
mHasCoverArt = true;
break;
case MediaUtils.TYPE_PLAYLIST:
mStore = MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI;
@ -448,10 +457,14 @@ public class MediaAdapter
holder.text = (TextView)view.findViewById(R.id.text);
holder.arrow = (ImageView)view.findViewById(R.id.arrow);
holder.cover = (LazyCoverView)view.findViewById(R.id.cover);
holder.arrow.setOnClickListener(this);
holder.text.setOnClickListener(this);
holder.cover.setOnClickListener(this);
holder.arrow.setVisibility(mExpandable ? View.VISIBLE : View.GONE);
holder.cover.setVisibility(mHasCoverArt ? View.VISIBLE : View.GONE);
holder.cover.setup(mLooper);
} else {
holder = (ViewHolder)view.getTag();
}
@ -477,6 +490,10 @@ public class MediaAdapter
holder.title = title;
}
if (mHasCoverArt) {
holder.cover.setCover(mType, holder.id);
}
return view;
}

View File

@ -26,4 +26,5 @@ public class ViewHolder {
public String title;
public TextView text;
public ImageView arrow;
public LazyCoverView cover;
}