I have a gridview with thumbnails from sdcard. I use an asynctask to lazy load images. It works perfect when i scroll slowly however when I scroll too fast, different images loading on same grid item multiple times and it takes 6 7 second to finally load the correct image. I tried to check if the position of the view is visible by using getFirstVisiblePosition and getLastVisiblePosition, and this time some of the images never loads.
Did you try to cache your images ? With LruCache for exemple.
Here is the doc :
http://developer.android.com/reference/android/util/LruCache.html
And here the official tutorial
http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html
Quick use to it :
LruCache<String, Bitmap> mMemoryCache;
public void onCreate(Bundle b)
{
.....
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
final int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
#Override
protected int sizeOf(String key, Bitmap bitmap) {
// The cache size will be measured in kilobytes rather than
// number of items.
return bitmap.getByteCount() / 1024;
}
};
....
}
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
public Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}
EDIT:
If you're loading you image asynchronously you have to look this tutorial :
http://developer.android.com/training/displaying-bitmaps/process-bitmap.html
It shows you how to handle concurrency and cancel current task if one is running
Related
The problem is as it is stated in question title. In fact I want to load images which I have their url in my records into RecyclerView and at the same time persist downloaded image to database. I am using realm.io and Glide and my RecyclerViewAdapter is as below:
#Override
public void onBindViewHolder(MyViewHolder holder, int position) {
final ProductModel obj = getData().get(position);
holder.data = obj;
holder.title.setText(obj.getTitle());
if (obj.getImage() == null) {
Glide
.with(context)
.load(obj.getImageUrl())
.fitCenter()
.placeholder(R.drawable.bronteel_logo)
.into(new GlideDrawableImageViewTarget(holder.icon) {
#Override
protected void setResource(GlideDrawable resource) {
// this.getView().setImageDrawable(resource); is about to be called
super.setResource(resource);
// here you can be sure it's already set
((ProductsFragment) mFragment).saveImage(obj, resource);
}
});
} else {
byte[] data = obj.getImage();
BitmapFactory.Options options = new BitmapFactory.Options();
options.inMutable = true;
Bitmap bmp = BitmapFactory.decodeByteArray(data, 0, data.length, options);
holder.icon.setImageBitmap(bmp);
}
}
class MyViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
public TextView title;
public ImageView icon;
public ProductModel data;
public MyViewHolder(View view) {
super(view);
title = (TextView) view.findViewById(R.id.textView);
icon = (ImageView) view.findViewById(R.id.imageView);
view.setOnClickListener(this);
}
#Override
public void onClick(View view) {
if (data.getImage() != null)
activity.startActivity(new Intent(activity, ProductActivity.class).putExtra("id", data.getId()));
}
}
And here's how I save images:
public void saveImage(final ProductModel data, Drawable drw) {
new AsyncImagePersister(data).execute(drw);
}
private class AsyncImagePersister extends AsyncTask<Drawable, Void, byte[]> {
private final ProductModel data;
AsyncImagePersister(ProductModel data) {
this.data = data;
}
#Override
protected byte[] doInBackground(Drawable... drawables) {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
Bitmap bmp = drawableToBitmap(drawables[0]);
bmp.compress(Bitmap.CompressFormat.PNG, 100, stream);
return stream.toByteArray();
}
#Override
protected void onPostExecute(final byte[] bytes) {
super.onPostExecute(bytes);
realm.executeTransaction(new Realm.Transaction() {
#Override
public void execute(Realm realm) {
data.setImage(bytes);
}
});
}
public Bitmap drawableToBitmap (Drawable drawable) {
Bitmap bitmap = null;
if (drawable instanceof BitmapDrawable) {
BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
if(bitmapDrawable.getBitmap() != null) {
return bitmapDrawable.getBitmap();
}
}
if(drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) {
bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); // Single color bitmap will be created of 1x1 pixel
} else {
bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
}
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return bitmap;
}
}
However, when loading the images for the first time from internet (using Glide) it shows wrong pictures for different places and on the other hand after it fetches all images, the saved images to realm are in their correct place.
So what am I doing wrong? Please help. Thanks.
The misplaced images is due to views are being recycled, So the loaded bitmap does not necessarily belong to the current position, And another thing to consider is that using AsyncTask inside a RecyclerView won't play nice and will cause lags in your UI, And for the final point, saving the byte[] array in your model might end up to a OOM exception!
If you want do some long running task inside your adapter, think of using a Service, IntentService or ThreadHandler, so you will be sending tasks one by one and the'd be queued and executed one by one.
About having offline access to images:
One option could be using Glide.diskCacheStrategy method and use DiskCacheStrategy.ALL so the original image size will be cached and you can use later in offline mode
Second option is to use Picasso instead of Glide!
so that you can use a custom RequestHandler and download the image and save it somewhere so you can access it later, consider memory management is all on your side and you should handle it!
here's a hint for your second option:
create class which extends from RequestHandler:
CustomReqHandler : RequestHandler() {
Then you should override two methods: canHandleRequest(), load()
in canHandleRequest() you should determine whether you want to handle current request or not, so define a custom scheme for these requests and check if this is one of them like:
val scheme:String = data.uri.scheme
the 2nd method is load() which is executed on a background thread and returns a Result object, download the image, save it somewhere, and return Result object!
You don't actually have to save the loaded images in your database when you're using Glide for this purpose. Glide caches the images loaded once automatically and efficiently. The caching is a complex system and if you want to read more about the caching with Glide, you might have a look here.
Now, about the images loaded in wrong place - this should not happen. I found no serious bug in your onBindViewHolder but hence as I suggest you not to save the images locally you might consider loading the images simply with Glide like this.
Glide
.with(context)
.load(obj.getImageUrl())
.fitCenter()
.placeholder(R.drawable.bronteel_logo)
.into(holder.icon);
Just you need to make sure if the obj.getImageUrl() is returning proper url.
currently i am loading the images even if user not scrolled to view so when he will the image will be load faster from Picasso caching , on the list contractor
private void createImageCatch(ArrayList<Article> items) {
for (int i = 0; i < items.size(); i++) {
Article article = (Article) items.get(i);
if (article.getImageUrl() != null
&& article.getImageUrl().length() > 0)
Picasso.with(mContext).load(article.getImageUrl())
.fetch();
}
}
this is working perfect , the images are ready to go even if user scroll fast , but is this the best way to do this ?
IMPROVE UPDATE - added static boolean to make sure this method called only once , if images was added on server side this is not big deal for few images to be loaded on scroll time as long must of them already in cach.
Have you considered using the Volley library? It has a special ImageView that downloads and caches images for you.
Replace the ImageView in your XML with something like this:
<com.android.volley.toolbox.NetworkImageView
android:id="#+id/photo"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_alignParentBottom="true"
android:layout_alignParentTop="true"
android:layout_marginRight="6dip"
/>
And load your image with
NetworkImageView photo = (NetworkImageView)view.findViewById(R.id.photo);
photo.setImageUrl("http://someurl.com/image.png", mImageLoader);
You do need to set up your ImageLoader, so create some fields:
private RequestQueue mRequestQueue;
private ImageLoader mImageLoader;
and initialize them in your onCreate:
mRequestQueue = Volley.newRequestQueue(context);
mImageLoader = new ImageLoader(mRequestQueue, new ImageLoader.ImageCache() {
private final LruCache<String, Bitmap> mCache = new LruCache<String, Bitmap>(10);
public void putBitmap(String url, Bitmap bitmap) {
mCache.put(url, bitmap);
}
public Bitmap getBitmap(String url) {
return mCache.get(url);
}
});
Source: Setting up the Android Google Volley ImageLoader for NetworkImageView
If you want to reuse ~30 small images, you can keep them in the memory as soft reference of Bitmaps, and recycle all after leaving Fragment or Activity with ListView.
For example your ImageView has size 55dp x 55dp, and there are 30 rows with images.
DisplayManager:
public static DP_55;//55 dp to px, do not count it each time, define it!
public static defineSize(Activity activity)
{
DisplayMetrics metrics = new DisplayMetrics();
activity.getWindowManager().getDefaultDisplay().getMetrics(metrics);
DP_55 = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 55, metrics);
}
Your Activity or Fragment:
public HashMap<String, SoftReference<Bitmap>> bitmaps = new HashMap<>();
Load images from File or Internet as Bitmap, scale to your size (55 dp) and put to the dictionary:
if (!bitmaps.containsKey(urlOrPath)) {
//Load 'bitmap' and put to dictionary:
bitmaps.put(urlOrPath, new SoftReference<>(
Bitmap.createScaledBitmap(
bitmap,
DisplayManager.DP_55, DisplayManager.DP_55, true)));
}
else //Just reuse it!
iv_image.setImageBitmap(bitmaps.get(urlOrPath).get());
How to recycle it:
Iterator<Map.Entry<String, SoftReference<Bitmap>>> iterator = bitmaps.entrySet().iterator();
while (iterator.hasNext()) {
iterator.next().getValue().get().recycle();
iterator.remove();
}
You won't get out of 40MB limit (if ImageView size is small and there are < 50 rows), and loading images after scrolling maximum fast.
Otherwise use ImageLoaders (Glide, Picasso, etc).
Im caching my Bitmaps in GridView to LruCache. I made manager for this, see below:
private LruCache<String, Bitmap> mMemoryCache;
public LruCacheManager(){
init();
}
private void init(){
// Get max available VM memory, exceeding this amount will throw an
// OutOfMemory exception. Stored in kilobytes as LruCache takes an
// int in its constructor.
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// Use 1/8th of the available memory for this memory cache.
final int cacheSize = maxMemory / 8;
//Log.i("ImageCache","cacheSize: " + cacheSize);
if(mMemoryCache == null){
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
#Override
protected int sizeOf(String key, Bitmap bitmap) {
// The cache size will be measured in kilobytes rather than
// number of items.
// The cache size will be measured in kilobytes rather than
// number of items.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
return bitmap.getByteCount() ;
} else {
return bitmap.getRowBytes() * bitmap.getHeight();
}
}
};
}
}
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
Log.i("LruCacheManager","Bitmap is getting added, " + key);
mMemoryCache.put(key, bitmap);
}
}
public Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}
when I call addBitmapToMemoryCache() in my AsyncTask to save Bitmaps to MemoryCache.
But when i call getBitmapFromMemoryCache() its null.
//get cached Bitmap
LruCacheManager imCache = new LruCacheManager();
String imageKey = categoryNames[position];
Bitmap cachedBm = imCache.getBitmapFromMemCache(imageKey);
//Decide whatever use cached image or not
if (cachedBm != null) {
Log.i("AdapterGridView","Using cached image, " + imageKey);
viewHolder.icon.setImageBitmap(cachedBm);
} else {
//starts Asynctask to scale pictures and show them, happens off the main thread
new AsyncTaskImageLoader(viewHolder.icon, imageKey, mContext, imCache, mThumbIds[position]).execute();
}
Which means, AsyncTask is called again and again. In AsyncTask im adding the Bitmaps to LruCache. Because returned Bitmap is null, there is no Bitmap saved in LruCache. But i have no clue why.
I also searched online and it has maybe to do something with recycling/Garbage Collector.
So how can i properly load cached images?
Any help or clarification is appriciate.
EDIT:
I call this inside BaseAdapter in getView() method. I think it has something to do with it. For the first time, each image is added to Cache, but then, the first image is added like 10 times.
First I would set an arbitrary memory size and try with 1 image. The rest looks good... If what I have below doesn't work, give us printouts of your memory, etc. You might not have any.
In my version I get memory by
final int maxMemory = (int) (Runtime.getRuntime().maxMemory());
then set it by a fraction ( I think I picked an 8th)
I do the /1024 when I return get the size of, I do not do it for setting the memory. So if you have 1/1000 of the memory you think you have, that would be the likely issue..
I am using NetworkImageView to show some covers downloaded from a remote URL and I successfully manage to cache and show them, but I want to let users set their own cover images if they want.
I tried to use setImageUrl method with Uri.fromFile(mCoverFile).toString() as arguments, but it doesn't work. Since it is a mix of remote and local images I can't switch to regular ImageViews, so I was wondering if there's any way to enable loading of local images.
I am of course aware of the ImageView's setImageBitmap method, but NetworkImageView automatically resizes the created Bitmap and also prevents View recycling in GridViews and ListViews.
UPDATE: njzk2's answer did the trick. To autoresize the Bitmap according to your View size, then just copy the ImageRequest.doParse method from Volley's source.
NetworkImageView uses ImageLoader, which in turn uses an ImageCache.
You can provide a custom ImageCache with your images, provided you use the same mechanism for keys:
return new StringBuilder(url.length() + 12).append("#W").append(maxWidth)
.append("#H").append(maxHeight).append(url).toString();
url is not tested before the actual request would be done, so no issue here.
Typically, your 'cache' could look like :
public class MyCache implements ImageLoader.ImageCache {
#Override
public Bitmap getBitmap(String key) {
if (key.contains("file://")) {
return BitmapFactory.decodeFile(key.substring(key.indexOf("file://") + 7));
} else {
// Here you can add an actual cache
return null;
}
}
#Override
public void putBitmap(String key, Bitmap bitmap) {
// Here you can add an actual cache
}
}
You use it like :
imageView.setImageUrl(Uri.fromFile(mCoverFile).toString(), new MyCache());
(This has not been actually tested and there may be some adjustments to do)
Thank you for your answer. I wrote some code based on your help.
usage: just use LocalImageCache.class as Cache. No more code to change.
private ImageLoader mLocalImageLoader;
mLocalImageLoader = new ImageLoader(mRequestQueue,
new LocalImageCache(mCtx));
NetworkImageView test = (NetworkImageView) findViewById(R.id.iv_test);
test.setImageUrl("/storage/emulated/0/DCIM/Philm/2017_03_24_01_.png", MySingleton.getInstance(this.getApplicationContext()).getLocalImageLoader());
public class LocalImageCache extends LruCache<String, Bitmap> implements ImageLoader.ImageCache {
public LocalImageCache(int maxSize) {
super(maxSize);
}
public LocalImageCache(Context ctx) {
this(getCacheSize(ctx));
}
#Override
public Bitmap getBitmap(String key) {
key = key.substring(key.indexOf("/"));
Bitmap result = get(key);
Log.d("TAG", key);
if (result == null) {
Bitmap temp = BitmapFactory.decodeFile(key);
put(key, temp);
return temp;
} else {
return result;
}
}
#Override
public void putBitmap(String key, Bitmap bitmap) {
// Here you can add an actual cache
// Never touch here
}
// 默认屏幕5倍的图片缓存
// Returns a cache size equal to approximately three screens worth of images.
public static int getCacheSize(Context ctx) {
final DisplayMetrics displayMetrics = ctx.getResources().
getDisplayMetrics();
final int screenWidth = displayMetrics.widthPixels;
final int screenHeight = displayMetrics.heightPixels;
// 4 bytes per pixel
final int screenBytes = screenWidth * screenHeight * 4;
return screenBytes * 5;
}
#Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight();
}
}
NetworkImageView extends ImageView. You should be able to use the same methods as a regular ImageView
image.setImageResource(R.drawable.my_image);
or
imageView.setImageBitmap(BitmapFactory.decodeFile(imagePath));
I'm implementing an image cache system for caching downloaded image.
My strategy is based upon two-level cache:
Memory-level and disk-level.
My class is very similar to the class used in the droidfu project
My downloaded images are put into an hashmap and the Bitmap objet is
wrapped inside a SoftRererence object. Also every image is saved
permanently to the disk.
If a requested image is not found into the
Hashmap<String,SoftReference<Bitmap>> it will be searched on the disk,
readed, and then pushed back into the hashmap. Otherwise the image will be
downloaded from the network.
Since I store the images into the phisical device momery, I have added a check for preserve the device space and stay under a 1M of occupied space:
private void checkCacheUsage() {
long size = 0;
final File[] fileList = new File(mCacheDirPath).listFiles();
Arrays.sort(fileList, new Comparator<File>() {
public int compare(File f1, File f2) {
return Long.valueOf(f2.lastModified()).compareTo(
f1.lastModified());
}
});
for (File file : fileList) {
size += file.length();
if (size > MAX_DISK_CACHE_SIZE) {
file.delete();
Log.d(ImageCache.class.getSimpleName(),
"checkCacheUsage: Size exceeded " + size + "("
+ MAX_DISK_CACHE_SIZE + ") wiping older file {"+file.toString()+"}");
}
}
}
This method is called sometime afte a disk writing:
Random r = new Random();
int ra = r.nextInt(10);
if (ra % 2 == 0){
checkCacheUsage();
}
What I'd like to add is the same check on the HashMap size to prevent it will grow too much. Something like this:
private synchronized void checkMemoryCacheUsage(){
long size = 0;
for (SoftReference<Bitmap> a : cache.values()) {
final Bitmap b = a.get();
if (b != null && ! b.isRecycled()){
size += b.getRowBytes() * b.getHeight();
}
if (size > MAX_MEMORY_SIZE){
//Remove some elements from the cache
}
}
Log.d(ImageCache.class.getSimpleName(),
"checkMemoryCacheUsage: " + size + " in memory");
}
My question is:
What could be a right MAX_MEMORY_SIZE value?
Also, Is it a good approach?
A good answer also could be: "Don't do it! SoftReference is already enough"
Don't do it! SoftReference is already enough!
Actually SoftReference is designed to do exactly what you need.
Sometimes SoftReference doesn't do what you need. Then you just get rid of SoftReference and write your own memory management logic. But as far as you use SoftReference you should not be worried about memory consumption, SoftReference does it for you.
I am using one-third of the heap for Image cache.
int memoryInMB = activityManager.getMemoryClass();
long totalAppHeap = memoryInMB * 1024 * 1024;
int runtimeCacheLimit = (int)totalAppHeap/3;
By the way, about soft reference, in Android Soft references do not work as you expect. There is a platform issue that soft references are collected too early, even when there is plenty of memory free.
Check http://code-gotcha.blogspot.com/2011/09/softreference.html
I've been looking into different caching mechanisms for my scaled bitmaps, both memory and disk cache examples. The examples where to complex for my needs, so I ended up making my own bitmap memory cache using LruCache.
You can look at a working code-example here or use this code:
Memory Cache:
public class Cache {
private static LruCache<Integer, Bitmap> bitmaps = new BitmapLruCache();
public static Bitmap get(int drawableId){
Bitmap bitmap = bitmaps.get(drawableId);
if(bitmap != null){
return bitmap;
} else {
bitmap = SpriteUtil.createScaledBitmap(drawableId);
bitmaps.put(drawableId, bitmap);
return bitmap;
}
}
}
BitmapLruCache:
public class BitmapLruCache extends LruCache<Integer, Bitmap> {
private final static int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
private final static int cacheSize = maxMemory / 2;
public BitmapLruCache() {
super(cacheSize);
}
#Override
protected int sizeOf(Integer key, Bitmap bitmap) {
// The cache size will be measured in kilobytes rather than number of items.
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
}
}