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));
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).
I am creating an app that has a file directory that contains pictures, videos, pdfs, etc. I am currently working on displaying thumbnails for pictures. I am using the RecyclerView and ViewHolder to display list items that each represent a photo item. I then use an AsyncTask to download the Bitmaps one at a time and store them in a Hashmap. Everything works fine except when I scroll down in a large list of photos very quickly. The placeholder image for random items at the bottom of the list are replaced with thumbnails that have already been loaded at the top of the list. When the background thread reaches the image at the bottom, then the correct image replaces the wrong image. After all the thumbnails are loaded then everything works as intended.
Here is the code for the AsyncTask. I think the problem has to do with the position integer I am passing into the constructor. The position variable represents the position in the Adapter. Maybe there is a way to make sure the image is loading the placeholder image I have in onPreExecute()?
/**
* AsyncTask to download the thumbnails in the RecyclerView list.
*/
private class CreateThumbnail extends AsyncTask<Void, Void, android.graphics.Bitmap> {
// ******
// FIELDS
// ******
private ImageView mPreviewInstance;
private File mFile;
private RelativeLayout.LayoutParams lp;
private FileHolder mFileHolder;
private int mPosition;
private UUID mId;
private FolderFile mFolderFile;
// ***********
// Constructor
// ***********
/**
* #param holder - ViewHolder passed for the list item.
* #param position - position in the Adapter.
* #param id - id for list item stored in database.
*/
private CreateThumbnail(FileHolder holder, int position, UUID id) {
mPosition = position;
mFileHolder = holder;
mPreviewInstance = mFileHolder.mImagePreview;
mId = id;
mFolderFile = FolderFileLab.get(getContext()).getFolderFile(mId);
}
// ****************
// OVERRIDE METHODS
// ****************
#Override
protected void onPreExecute() {
}
#Override
protected Bitmap doInBackground(Void... params) {
FolderFileLab lab = FolderFileLab.get(getContext());
if (!lab.getCurrentMap().containsKey(mId)) {
mFile = lab.getPhotoFile(mFolderFile);
// Create Bitmap (Biggest use of memory and reason this background thread exists)
Bitmap bitmap = PictureUtils.getScaledBitmap(
mFile.getPath(), getActivity());
// Scales Bitmap down for thumbnail.
Bitmap scaledBitmap;
if (bitmap.getWidth() >= bitmap.getHeight()) {
scaledBitmap = Bitmap.createBitmap(bitmap, bitmap.getWidth() / 2
- bitmap.getHeight() / 2,
0, bitmap.getHeight(), bitmap.getHeight());
} else {
scaledBitmap = Bitmap.createBitmap(bitmap, 0, bitmap.getHeight() / 2
- bitmap.getWidth() / 2,
bitmap.getWidth(), bitmap.getWidth());
}
// Cache bitmap
HashMap<UUID, Bitmap> map = lab.getCurrentMap();
map.put(mId, scaledBitmap);
lab.updateMap(map);
return scaledBitmap;
} else {
// If Hashmap already contains the id get the Bitmap.
return lab.getCurrentMap().get(mId);
}
}
#Override
protected void onPostExecute(Bitmap bitmap) {
// Checks to see if the bitmap is still displayed in the list. If not nothing happens.
// If it is then it displays the image.
if (mPreviewInstance.getVisibility() == View.VISIBLE && mFileHolder.getPosition()
== mPosition && bitmap != null) {
// Formatting for thumbnail
lp = new RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout
.LayoutParams.WRAP_CONTENT);
lp.setMargins(7, 7, 7, 0);
// Displaying thumbnail on UI thread.
mPreviewInstance.setLayoutParams(lp);
mPreviewInstance.setBackground(null);
mPreviewInstance.setImageBitmap(bitmap);
}
}
}
Here is some of the relevant Adapter code where the AsyncTask is started.
#Override
public void onBindViewHolder(FileHolder holder, int position) {
FolderFile file = mFiles.get(position);
holder.bindFile(file);
if (file.isPhoto()) {
createThumbnail = new CreateThumbnail(holder, position,file.getId());
createThumbnail.execute();
}
}
Figured it out!
I added code to change the photo to the placeholder image after every bind. This is what I changed in my adapter.
#Override
public void onBindViewHolder(FileHolder holder, int position) {
FolderFile file = mFiles.get(position);
holder.bindFile(file);
if (file.isPhoto()) {
Drawable placeholder = getResources().getDrawable(R.mipmap.picture_blu);
holder.mImagePreview.setBackground(placeholder);
holder.mImagePreview.setImageBitmap(null);
createThumbnail = new CreateThumbnail(holder, position, file.getId());
createThumbnail.execute();
}
}
Your views are recycled, so by the time the async task finishes, the imageView has been reused and has a new image assigned to it.
What you can do is assign to the imageView a tag that is the file name of the file you are trying to load into it. You keep track of that same file name in the async task. Then in your AsyncTask, in onPostExecute, you check if the tag the imageView has is the same file name that you just loaded. If it is, you go ahead and set the bitmap to the imageView. If it is not, then the view has been recycled and you simply drop the Bitmap you just created; another async task will be loading the right bitmap.
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
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..