implement LazyCoverView
This commit is contained in:
parent
8393184d49
commit
93b750dacd
@ -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"
|
||||
|
216
src/ch/blinkenlights/android/vanilla/LazyCoverView.java
Normal file
216
src/ch/blinkenlights/android/vanilla/LazyCoverView.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
@ -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:
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -26,4 +26,5 @@ public class ViewHolder {
|
||||
public String title;
|
||||
public TextView text;
|
||||
public ImageView arrow;
|
||||
public LazyCoverView cover;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user