I am still fighting with a fluent recycler view that loads images without blocking the ui thread too much.
The problem is in the word "recycle" itself.
In my app I have a long list with mp3 files that, ofc, all have different names, different artists and different album covers.
However, they all DO use the same layout.
But can I now recycle the view or cant I?
Since loading the album cover right (also cropping it) took the most resources, I have decided to NOT recycle views with an album cover and ONLY recycle those who have no album cover, like so:
private async Task SetContentAsync(PhotoViewHolder vh, int position)
{
string SongName = "";
string ArtistName = "";
Bitmap bitmap = null;
byte[] data = null;
try
{
reader.SetDataSource(mp3Obj[position].Mp3Uri);
}
catch { }
await Task.Run(() => // cause problems with the reload
{
SongName = reader.ExtractMetadata(MediaMetadataRetriever.MetadataKeyTitle);
ArtistName = reader.ExtractMetadata(MediaMetadataRetriever.MetadataKeyArtist);
data = reader.GetEmbeddedPicture();
if (data != null)
{
try
{
bitmap = BitmapFactory.DecodeByteArray(data, 0, data.Length);
}
catch { }
}
});
((Activity)ctx).RunOnUiThread(() =>
{
vh.SongName.SetTypeface(tf, TypefaceStyle.Normal);
vh.AristName.SetTypeface(tf, TypefaceStyle.Normal);
vh.SongName.Text = SongName;
vh.AristName.Text = ArtistName;
try
{
if (bitmap != null)
{
vh.IsRecyclable = false; // MOST IMPORTANT THING TO DO!
ConvertBitmapToBackground(bitmap, vh, false); // Set As Backgorund, blurry and black ( just sets the variable)
CutImageIntoForm(bitmap, vh); // Set as the musical note button
}
else // because recycler items inherit their shit and if it is altered it just shows views were there shouldnt be any ...
{
vh.IsRecyclable = true;
// vh.CoverArt.SetImageResource(Resource.Drawable.btn_musicalnote);
// ConvertBitmapToBackground(bitmap, vh, true); // Set As Backgorund, blurry and black ( just sets the variable)
}
}
catch { }
});
}
Notice how I say "vh.IsRecyclable = ..." depending on whether there is an album cover or not?
This was the solution with the least amount of lags and works when only a few views include an album cover art. However, when the user has all songs with album covers the recycler view becomes extremely slow.
Another issue I am having is that sometimes, when lag occurs, the views double or even tripple. The still lead to the right file, but they all have the same song name for instance.
So what is best practice?
When can I recycle a view and when not?
How can I speed up the recycler view?
Thanks a lot for any answer!
Related
I am working on an app that uses a Recyclerview to display mp3 files, providing its cover art image along with other info. It works but is slow once it starts dealing with a dozen or more cover arts to retrieve, as I am currently doing this from the id3 on the main thread, which I know is not a good idea.
Ideally, I would work with placeholders so that the images can be added as they become available. I've been looking into moving the retrieval to a background thread and have looked at different options: AsyncTask, Service, WorkManager. AsyncTask seems not to be the way to go as I face memory leaks (I need context to retrieve the cover art through MetadataRetriever). So I am leaning away from that. Yet I am struggling to figure out which approach is best in my case.
From what I understand I need to find an approach that allows multithreading and also a means to cancel the retrieval in case the user has already moved on (scrolling or navigating away). I am already using Glide, which I understand should help with the caching.
I know I could rework the whole approach and provide the cover art as images separately, but that seems a last resort to me, as I would rather not weigh down the app with even more data.
The current version of the app is here (please note it will not run as I cannot openly divulge certain aspects). I am retrieving the cover art as follows (on the main thread):
static public Bitmap getCoverArt(Uri medUri, Context ctxt) {
MediaMetadataRetriever mmr = new MediaMetadataRetriever();
mmr.setDataSource(ctxt, medUri);
byte[] data = mmr.getEmbeddedPicture();
if (data != null) {
return BitmapFactory.decodeByteArray(data, 0, data.length);
} else {
return null;
}
}
I've found many examples with AsyncTask or just keeping the MetaDataRetriever on the main thread, but have yet to find an example that enables a dozen or more cover arts to be retrieved without slowing down the main thread. I would appreciate any help and pointers.
It turns out it does work with AsyncTask, as long as it is not a class onto itself but setup and called from a class with context. Here is a whittled down version of my approach (I am calling this from within my Adapter.):
//set up titles and placeholder image so we needn't wait on the image to load
titleTv.setText(selectedMed.getTitle());
subtitleTv.setText(selectedMed.getSubtitle());
imageIv.setImageResource(R.drawable.ic_launcher_foreground);
imageIv.setAlpha((float) 0.2);
final long[] duration = new long[1];
//a Caching system that helps reduce the amount of loading needed. See: https://github.com/cbonan/BitmapFun?files=1
if (lruCacheManager.getBitmapFromMemCache(selectedMed.getId() + position) != null) {
//is there an earlier cached image to reuse? imageIv.setImageBitmap(lruCacheManager.getBitmapFromMemCache(selectedMed.getId() + position));
imageIv.setAlpha((float) 1.0);
titleTv.setVisibility(View.GONE);
subtitleTv.setVisibility(View.GONE);
} else {
//time to load and show the image. For good measure, the duration is also queried, as this also needs the setDataSource which causes slow down
new AsyncTask<Uri, Void, Bitmap>() {
#Override
protected Bitmap doInBackground(Uri... uris) {
MediaMetadataRetriever mmr = new MediaMetadataRetriever();
mmr.setDataSource(ctxt, medUri);
byte[] data = mmr.getEmbeddedPicture();
Log.v(TAG, "async data: " + Arrays.toString(data));
String durationStr = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
duration[0] = Long.parseLong(durationStr);
if (data != null) {
InputStream is = new ByteArrayInputStream(mmr.getEmbeddedPicture());
return BitmapFactory.decodeStream(is);
} else {
return null;
}
}
#Override
protected void onPostExecute(Bitmap bitmap) {
super.onPostExecute(bitmap);
durationTv.setVisibility(View.VISIBLE);
durationTv.setText(getDisplayTime(duration[0], false));
if (bitmap != null) {
imageIv.setImageBitmap(bitmap);
imageIv.setAlpha((float) 1.0);
titleTv.setVisibility(View.GONE);
subtitleTv.setVisibility(View.GONE);
} else {
titleTv.setVisibility(View.VISIBLE);
subtitleTv.setVisibility(View.VISIBLE);
}
lruCacheManager.addBitmapToMemCache(bitmap, selectedMed.getId() + position);
}
}.execute(medUri);
}
I have tried working with Glide for the caching, but I haven't been able to link the showing/hiding of the TextViews to whether there is a bitmap. In a way though, this is sleeker as I don't need to load the bulk of the Glide-library. So I am happy with this for now.
I created a plugin using Picasso and it uses the android.widget.ImageView to load the cached image into.
The plugin works fine if using a Repeater but whenever i try using it with a ListView after scrolling past about the 7th item the ListView begins to reuse old images even if the image source is different
The reason why is because list views reuse the entire fragment; so what happens is that your img being reused gets the old image shown unless you clear it.
I actually use Picasso myself; and this is my current picasso library.
So if you look in my code below, when I set the new .url, I clear the existing image. (I made a comment on the specific line) -- This way the image now show blank, and then picasso loads it from either memory, disk or a remote url (in my case a remote url) and it will assign the proper image.
"use strict";
var Img = require('ui/image').Image;
var application = require("application");
var PT = com.squareup.picasso.Target.extend("Target",{
_owner: null,
_url: null,
onBitmapLoaded: function(bitmap, from) {
// Since the actual image / target is cached; it is possible that the
// target will not match so we don't replace the image already seen
if (this._url !== this._owner._url) {
return;
}
this._owner.src = bitmap;
},
onBitmapFailed: function(ed) {
console.log("Failed File", this._url);
},
onPrepareLoad: function(ed) {
}
});
Object.defineProperty(Img.prototype, "url", {
get: function () {
return this._url;
},
set: function(src) {
if (src == null || src === "") {
this._url = "";
this.src = null;
return;
}
var dest = src;
this._url = dest;
this.src = null; // -- THIS IS THE LINE TO CLEAR THE IMAGE
try {
var target = new PT();
target._owner = this;
target._url = dest;
var x = com.squareup.picasso.Picasso.with(application.android.context).load(dest).into(target);
} catch (e) {
console.log("Exception",e);
}
},
enumerable: true,
configurable: true
});
Please note you only need to require this class once, then it attaches itself to the <Image> component and adds the new .url property; this allows me to use this in the Declarative XML in all the rest of the screens and when I need picasso, I just use the .url property to have picasso take over the loading of that image.
I am displaying all apps installed in a gridView. When loading a lot of apps, lets say 30 or more, the icons will display at the default Android icon and then several seconds later update to the correct icon. I am wondering about improvements I can make to my code to make the icon images display faster.
Load the following with: new LoadIconsTask().execute(mApps.toArray(new AppsInstalled[]{}));
Here is what I do.
private class LoadIconsTask extends AsyncTask<AppsInstalled, Void, Void>{
#Override
protected Void doInBackground(AppsInstalled... params) {
// TODO Auto-generated method stub
Map<String, Drawable> icons = new HashMap<String, Drawable>();
PackageManager manager = getApplicationContext().getPackageManager();
// match package name with icon, set Adapter with loaded Map
for (AppsInstalled app : params) {
String pkgName = app.getAppUniqueId();
Drawable ico = null;
try {
Intent i = manager.getLaunchIntentForPackage(pkgName);
if (i != null) {
ico = manager.getActivityIcon(i);
}
} catch (NameNotFoundException e) {
Log.e(TAG, "Unable to find icon match based on package: " + pkgName
+ " : " + e.getMessage());
}
icons.put(app.getAppUniqueId(), ico);
}
mAdapter.setIcons(icons);
return null;
}
Also populate my listing of apps before I loadIconsTask() with
private List<App> loadInstalledApps(boolean includeSysApps) {
List<App> apps = new ArrayList<App>();
// the package manager contains the information about all installed apps
PackageManager packageManager = getPackageManager();
List<PackageInfo> packs = packageManager.getInstalledPackages(0); // PackageManager.GET_META_DATA
for (int i = 0; i < packs.size(); i++) {
PackageInfo p = packs.get(i);
ApplicationInfo a = p.applicationInfo;
// skip system apps if they shall not be included
if ((!includeSysApps)
&& ((a.flags & ApplicationInfo.FLAG_SYSTEM) == 1)) {
continue;
}
App app = new App();
app.setTitle(p.applicationInfo.loadLabel(packageManager).toString());
app.setPackageName(p.packageName);
app.setVersionName(p.versionName);
app.setVersionCode(p.versionCode);
CharSequence description = p.applicationInfo
.loadDescription(packageManager);
app.setDescription(description != null ? description.toString()
: "");
apps.add(app);
}
return apps;
}
In regards to my Adapter class it is standard. My getView() looks like the following:
#Override
public View getView(int position, View convertView, ViewGroup parent) {
AppViewHolder holder;
if (convertView == null) {
convertView = mInflater.inflate(R.layout.row, null);
// creates a ViewHolder and stores a reference to the children view
// we want to bind data to
holder = new AppViewHolder();
holder.mTitle = (TextView) convertView.findViewById(R.id.apptitle);
holder.mIcon = (ImageView) convertView.findViewById(R.id.appicon);
convertView.setTag(holder);
} else {
// reuse/overwrite the view passed assuming that it is castable!
holder = (AppViewHolder) convertView.getTag();
}
App app = mApps.get(position);
holder.setTitle(app.getTitle());
if (mIcons == null || mIcons.get(app.getPackageName()) == null) {
holder.setIcon(mStdImg);
} else {
holder.setIcon(mIcons.get(app.getPackageName()));
}
return convertView;
}
Is there a better way? Can I somehow store the images of the icons in a data structure and when I return back to this Activity I can skip the loadIconsTask? Is that possible? Thank you in advance.
You can use Picasso library with a custom RequestHandler to load the icons in the background.
First create a RequestHandler which will handle the specific case where an app icon needs to be loaded.
public class AppIconRequestHandler extends RequestHandler {
/** Uri scheme for app icons */
public static final String SCHEME_APP_ICON = "app-icon";
private PackageManager mPackageManager;
public AppIconRequestHandler(Context context) {
mPackageManager = context.getPackageManager();
}
/**
* Create an Uri that can be handled by this RequestHandler based on the package name
*/
public static Uri getUri(String packageName) {
return Uri.fromParts(SCHEME_APP_ICON, packageName, null);
}
#Override
public boolean canHandleRequest(Request data) {
// only handle Uris matching our scheme
return (SCHEME_APP_ICON.equals(data.uri.getScheme()));
}
#Override
public Result load(Request request, int networkPolicy) throws IOException {
String packageName = request.uri.getSchemeSpecificPart();
Drawable drawable;
try {
drawable = mPackageManager.getApplicationIcon(packageName);
} catch (PackageManager.NameNotFoundException ignored) {
return null;
}
Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
return new Result(bitmap, Picasso.LoadedFrom.DISK);
}
}
In your adapter, create a Picasso instance and add your RequestHandler.
// field variable
private Picasso mPicasso;
// in constructor
Picasso.Builder builder = new Picasso.Builder(context);
builder.addRequestHandler(new AppIconRequestHandler(context));
mPicasso = builder.build();
In your adapter's getView() load the icon using Picasso.
mPicasso.load(AppIconRequestHandler.getUri(app.packageName)).into(holder.mIcon);
it's surprising the system takes that much time in getting these lists, you may want to add some logs with timestamping to see which one is the demanding operation.
I don't know if that procedure can be further optimized, I haven't used these system API's very much, but what you can certainly do is to cache this list
Create it in onResume / onCreate as a static list, and (for the sake of correctness) destroy it in onPause / onStop if you want to consider the case where the user may install an application while in your app (onPause will be called), but you can certainly skip this step.
You may want to also permanently cache the list in the sdcard and find some simple and fast heuristic to decide if the list has changed in order to recreate it. Something like maybe the number of installed packages together with something else (to discard the case when the user uninstalls 3 apps and install 3 different apps, the number of packages will be the same and you have to detect this somehow).
EDIT- To recommend a caching mechanism, you should identify which one is the slow operation. Just guessing, and from your question "the icons take some seconds to appear" it looks like that the slow operation is:
ico = manager.getActivityIcon(i);
but I might be wrong. Let's suppose I'm right, so a cheap caching can be:
1) Move the Map<String, Drawable> icons = new HashMap<String, Drawable>(); outside of doInBackground to the root of the class and make it static, like:
private static Map<String, Drawable> sIcons = new HashMap<String, Drawable>()
2) In your loadIconsTask consider the case you already have this icon:
for (AppsInstalled app : params) {
String pkgName = app.getAppUniqueId();
if (sIcons.containsKey(pkgName) continue;
.
.
.
}
This is because sIcons is now static and will be alive as long as your application is alive.
3) As a classy thing, you may want to change sIcons from Drawable to Bitmap. Why? Because a Drawable may keep inside references to Views and Context and it's a potential memory leak. You can get the Bitmap from a Drawable very easily, calling drawable.getBitmap() , (Assuming drawable is a BitmapDrawable, but it will obviously be because it's an app icon), so suming up you'll have:
// the static icon dictionary now stores Bitmaps
static Map<String, Bitmap> sIcons = new HashMap<String, Bitmap>();
.
.
// we store the bitmap instead of the drawable
sIcons.put(app.getAppUniqueId(), ((BitmapDrawable)ico).getBitmap());
.
.
// when setting the icon, we create the drawable back
holder.setIcon(new BitmapDrawable(mIcons.get(app.getPackageName())));
This way your static hashmap will never leak any memory.
4) You may want to check if it's worth to store those bitmaps on disk. Mind this is some additional work and it might not be worth if the time to load the icon from disk is similar to the time to load the icon calling ico = manager.getActivityIcon(i);. It may be (i don't know if manager.getActivityIcon() extracts the icon from the APK) but it certainly may be not.
If you check out it's worth, when you create the list, you can save the bitmaps to the sdcard like this:
// prepare a file to the application cache dir.
File cachedFile=new File(context.getCacheDir(), "icon-"+app.getPackageName());
// save our bitmap as a compressed JPEG with the package name as filename
myBitmap.compress(CompressFormat.JPEG, quality, new FileOutputStream(cachedFile);
... then when loading the icons, check if the icon exists and load from the sdcard instead:
String key=app.getPackageName();
File localFile=new File(context.getCacheDir(), "icon-"+key);
if (localFile.exists()) {
// the file exists in the sdcard, just load it
Bitmap myBitmap = BitmapFactory.decodeStream(new FileInputStream(localFile));
// we have our bitmap from the sdcard !! Let's put it into our HashMap
sIcons.put(key, myBitmap)
} else {
// use the slow method
}
Well as you see it's just a matter of identifying the slow operation. If our above assumption is correct, your stored bitmaps will survive your application destroy and it will hopefully optimize the icon loading.
You can use Glide for automatic loading and caching and the URI of each application icon:
final RequestManager mGlide = Glide.with(activity);
final Uri appIconUri = applicationInfo.icon != 0 ?
Uri.parse("android.resource://" + packageName + "/" + applicationInfo.icon) :
null;
if (appIconUri != null) mGlide.load(appIconUri).into(holder.appIconImgView);
else {
mGlide.clear(holder.appIconImgView); // as suggested here: https://bumptech.github.io/glide/doc/getting-started.html
mGlide.load(android.R.drawable.sym_def_app_icon).into(holder.appIconImgView);
}
The reason I suggest Glide and not other image loading libraries is that Glide supports XML drawable (or dynamic/adaptive or vector icons) loading while others don't (see https://github.com/facebook/fresco/issues/2173)
I'm downloading files with my app via an API (this is working perfectly). Later in my app I'm doing a call to my database to get the file info: id, name, location and return it as a HashMap:
HashMap<Integer, ArrayList<String>> imgTitle;
I take the returned HashMap, extract the ArrayList and use that to populate some buttons. the array list holds the filename and file location. When the page loads the names display correctly but the images don't always load or one loads. If I go back and reenter the activity (via a button press) I'll get different images showing up. I've sent my array to the log and the file location is present and correct for all arrays. Why are only some image showing?
for (Map.Entry<Integer, ArrayList<String>> e : imgTitle.entrySet()) {
...
ArrayList<String> catList = e.getValue();
final String catTitle = catList.get(0);
File indexImage = new File(catList.get(1));
// add images
ImageButton imgButton = new ImageButton(this);
imgButton.setImageURI(Uri.fromFile(indexImage));
...
}
The above is inside a method called on the onCreate. My assumption is maybe the setImageURI is too slow? Any ideas?
Prior to using the setImageURI I used
imgButton.setImageResource(R.drawable.test_vehicle);
as a placeholder it was working fine. However, now I have to use a file that was downloaded and saved locally.
You could try to decode a Bitmap from a stream, like this:
File indexImage = new File(catList.get(1));
InputStream imageStream = new FileInputStream(indexImage);
Bitmap backgroundImage = null;
try {
backgroundImage = BitmapFactory.decodeStream(imageStream);
}
catch(Exception e) {
e.printStackTrace();
}
finally {
try {
//Dispose of the temporary resources
imageStream.close();
imageStream = null //So that the stream is deleted on next GC
}
catch(IOException e) {
e.printStackTrace();
}
}
if (backgroundImage != null) {
ImageButton imgButton = new ImageButton(this);
imgButton.setImageBitmap(backgroundImage);
backgroundImage.recycle();
}
Note: This is pure speculation, I haven't tested any of this. It's simply the first solution that came to my mind
I have the following problem:
suppose that until now, I am using R.drawable.img to set the image in some imageviews
with
imgView.setImage(aProduct.getImgId());
the simplified product class look like this:
class Product{
int imgId;
....
public void setImgId(int id){
this.imgId=id;
}
public int getImgId(){
return this.imgId
}
...
}
my application is now "evolved" because the user can add customized products
taking the img from the camera and getting the Uri of the picture.
and to set the image on the ImageView imgView.setImgURI(Uri)
Now my question is:
what would be the best approach to have a mixed int/Uri image resources ambient?
can I obtain the Uri of a "R.drawable.img"?
I'm not sure if my question is clear, I mean:
I have to check, before to set the imageview, if my product has an Uri or an int Id,
and then make an "if" to call the appropriate method, or there is a simpler solution?
Thank you for reading, and sorry for my english.
Regards.
Your problem is that there are basically 3 types of image resources:
R.id... resources: internal resources, such as icons you put into the res folder
content URI's: local files or content provider resources such as content:// or file:///sdcard/...
remote file URL's: images on the web, such as http://...
You are looking for a way to pass around one identifier that can deal with all three. My solution was to pass around a string: either the toString() of the URI's, or just the string respresentation of the R.id integer.
I'm using the following code, for example, to deal with this and make the code work with it:
public static FastBitmapDrawable returnAndCacheCover(String cover, ImageRepresentedProductArtworkCreator artworkCreator) {
Bitmap bitmap = null;
Uri coverUri = null;
boolean mightBeUri = false;
//Might be a resId. Needs to be cached. //TODO: problem: resId of default cover may not survive across versions of the app.
try {
bitmap = BitmapFactory.decodeResource(Collectionista.getInstance().getResources(), Integer.parseInt(cover));
} catch (NumberFormatException e) {
//Not a resId after all.
mightBeUri=true;
}
if(bitmap==null || mightBeUri){
//Is not a resId. Might be a contentUri.
try {
coverUri = Uri.parse(cover);
} catch (NullPointerException ne) {
//Is null
return null;
}
}
if(coverUri!=null){
if(coverUri.getScheme().equals("content")){
//A contentUri. Needs to be cached.
try {
bitmap = MediaStore.Images.Media.getBitmap(Collectionista.getInstance().getContentResolver(), coverUri);
} catch (FileNotFoundException e) {
return null;
} catch (IOException e) {
return null;
}
}else{
//Might be a web uri. Needs to be cached.
bitmap = loadCoverFromWeb(cover);
}
}
return new FastBitmapDrawable(bitmap);
}
You might be interested to take over the logic part. Ofcourse cover is the string in question here.
Forget android.resource:// as a replacement for the R.id... integer. Claims are going round it does not work: http://groups.google.com/group/android-developers/browse_thread/thread/a672d71dd7df4b2d
To find out how to implement loadCoverFromWeb, have a look around in other questions or ping me. This web stuff is kind of a field of it's own.
(Based on GPLv3 code out of my app Collectionista: https://code.launchpad.net/~pjv/collectionista/trunk)