I need to load VectorDrawable resources depending on different cases into imageviews in recycler-view. The current implementation instantiates a new drawable everytime, which i am sure, leads to performance loss with a huge amount of items in the list. I want to change it, so that drawables are reused effectively to reduce the amount of created objects. I am using Picasso and i thought it could be a good idea to delegate this work to it, but it looks like Picasso is not able to load VectorDrawable's. Is there a way to manage this?
UPD.
The resource is loaded using ContextCompat.getDrawable and set using setImageDrawable. ImageView drawable is cleared in onViewRecycled
The only solution i can think of is to move the loading of dynamic drawable images from the UI thread to the background thread. To make the loading smoother, you need to load the images Asynchronously using an AsyncTask in your adapter. This will take the load off the UI thread and make the user experience much better. You are not able to use Picasso, hence i recommend a simple AsyncTask.
I found a good snippet for this, have a look:
public View getView(int position, View convertView,
ViewGroup parent) {
ViewHolder holder;
...
holder.position = position;
new ThumbnailTask(position, holder)
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, null);
return convertView;
}
private static class ThumbnailTask extends AsyncTask {
private int mPosition;
private ViewHolder mHolder;
public ThumbnailTask(int position, ViewHolder holder) {
mPosition = position;
mHolder = holder;
}
#Override
protected Cursor doInBackground(Void... arg0) {
// Download bitmap here
}
#Override
protected void onPostExecute(Bitmap bitmap) {
if (mHolder.position == mPosition) {
mHolder.thumbnail.setImageBitmap(bitmap);
}
}
}
private static class ViewHolder {
public ImageView thumbnail;
public int position;
}
For more info, please have a look at this link.
Related
I'm trying to make an app that takes videos and then displays the videos in a gridview. I have gotten to the point where all the video thumbnails are showing up in the gridview like they should. However, my problem is that the method I am using has a lot of lag time. It takes some time to get the gridview loaded and when I try to scroll there is always some sort of lag. This is most likely due to the fact that I'm using bit maps for every video thumbnail.
I was wondering what I could do to make this a lot quicker and smoother. My code is posted below.
class VideoAdapter extends BaseAdapter {
Context context;
ArrayList<String> videopathlist;
Bitmap bmthumb;
VideoAdapter(Context c, ArrayList<String> filepathlist){
context = c;
videopathlist = new ArrayList<String>();
this.videopathlist.addAll(filepathlist);
Log.i(TAG, "" + videopathlist);
}
#Override
public int getCount() {
return videopathlist.size();
}
#Override
public Object getItem(int position) {
return videopathlist.get(position);
}
#Override
public long getItemId(int arg0) {
return 0;
}
#Override
public View getView(int posiiton, View arg1, ViewGroup arg2) {
ImageView imageview = new ImageView(context);
bmthumb = ThumbnailUtils.createVideoThumbnail(videopathlist.get(posiiton), MediaStore.Video.Thumbnails.MINI_KIND);
if (bmthumb != null) {
Log.i(TAG1, "There is a thumbnail");
imageview.setImageBitmap(bmthumb);
} else {
Log.i(TAG1, "There is NOT a thumbnail");
}
imageview.setScaleType(ImageView.ScaleType.CENTER_CROP);
imageview.setLayoutParams(new GridView.LayoutParams(160, 160));
return imageview;
}
Thanks for your help
2 things:
Use a ViewHolder to cache everything, as described here: Making ListView Scrolling Smooth (applies to GridViews too)
Also, use an ImageLoader to load images dynamically. There are many available or you can build your own. Here are some:
Volley
Android-Universal-Image-Loader
ImageLoader
All these loaders make use of Bitmap caches as described here: Caching Bitmaps
Also, you could potentially be actually creating the thumbnail bitmaps when returning the view. This strikes me as very expensive. I would do this as a seperate step, unrelated to displaying them. For example, kick off an AsyncTask when your activity is first created to generate any new thumbnails. Then in your adapter just query the MediaStore to get the generated thumbnails.
I have a gridview which displays two column imageviews. I am loading these images with an async task (see this post Lazy load of images in ListView )
But when i am scrolling gridview , the images in positions are mixing. For example 14th image shows 1th image , i think view is trying to show old image before async task finishes.
My code :
public Content getItem(int position) {
return contents.get(position);
}
public long getItemId(int position) {
return position;
}
#Override
public boolean hasStableIds() { return true; }
public View getView(int position, View convertView, ViewGroup parent) {
Thumbnail contentView;
Content current = contents.get(position);
if (convertView == null) {
contentView = new Thumbnail(mContext);
}
else {
contentView = (Thumbnail) convertView;
}
contentView.title = current.getTitle();
contentView.image = current.getImage();
contentView.link = current.getLink();
contentView.init();
return contentView;
}
Init function
TextView titleText = (TextView)super.findViewById(R.id.titleText);
titleText.setText(title);
ImageView imageControl = (ImageView)super.findViewById(R.id.thumbImage);
DrawableManager loadImage = new DrawableManager(); loadImage.fetchDrawableOnThread(imgUrl, imageControl);
Waiting for your help
Thanks
It happens because of resource reusing. What you should do:
First, just set some kind of default image to your imageView (contentView.image.setImageResource(DEFAULT_RESOURCE)) inside getView method (transitional default picture is better than wrong one).
Set unique tag to your image, for example, position or url of image to load (contentView.image.setTag(url)).
When AsyncTask finishes, you can use some checks like
String url=(String)imageView.getTag();
if (url.equals(mUrl)) { //mUrl can be transmitted to AsyncTask instance separately
mActivity.runOnUiThread(new Runnable() {
#Override
public void run() {
imageView.setImageDrawable(bmp);
});
});
}
It's needed because Adapter does not allocate memory for all N ImageView for N items. It stores just required amount for visible items and some reserved ImageViews. So there is no warranty that ImageView reference that you store will be actual within several seconds cause it can became invisible and be reused by visible ones.
Don't load images yourself. Use Universal Image Loader . It can cache, images and loaded bitmaps. Making your problem go away. It also supports showing stub images and automatic pause on scrolling and so on.
It solves your specic problems by only loading an image for the last instance of an imageView if you reuse it.
imageLoader.displayImage(imageUri, imageView);
Is all that is needed to asynchronously load an image.
After Honeycomb, Google said that bitmaps are managed by the heap (talked about here), so if a bitmap is no longer accessible, we can assume that GC takes care of it and frees it.
I wanted to create a demo that shows the efficiency of the ideas shown for the listView lecture (from here), so I made a small app. The app lets the user press a button, and then the listview scrolls all the way to the bottom, while it has 10000 items, which their content is the android.R.drawable items (name and image).
For some reason, I get out of memory even though I don't save any of the images, so my question is: How could it be? What is it that I'm missing?
I've tested the app on a Galaxy S III, and yet I keep getting out of memory exceptions if I use the native version of the adapter. I don't understand why it occurs, since I don't store anything.
Here's the code:
public class MainActivity extends Activity
{
private static final int LISTVIEW_ITEMS =10000;
long _startTime;
boolean _isMeasuring =false;
#Override
public void onCreate(final Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final ListView listView=(ListView)findViewById(R.id.listView);
final Field[] fields=android.R.drawable.class.getFields();
final LayoutInflater inflater=(LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
// listen to scroll events , so that we publish the time only when scrolled to the bottom:
listView.setOnScrollListener(new OnScrollListener()
{
#Override
public void onScrollStateChanged(final AbsListView view,final int scrollState)
{
if(!_isMeasuring||view.getLastVisiblePosition()!=view.getCount()-1||scrollState!=OnScrollListener.SCROLL_STATE_IDLE)
return;
final long stopTime=System.currentTimeMillis();
final long scrollingTime=stopTime-_startTime;
Toast.makeText(MainActivity.this,"time taken to scroll to bottom:"+scrollingTime,Toast.LENGTH_SHORT).show();
_isMeasuring=false;
}
#Override
public void onScroll(final AbsListView view,final int firstVisibleItem,final int visibleItemCount,final int totalItemCount)
{}
});
// button click handling (start measuring) :
findViewById(R.id.button).setOnClickListener(new OnClickListener()
{
#Override
public void onClick(final View v)
{
if(_isMeasuring)
return;
final int itemsCount=listView.getAdapter().getCount();
listView.smoothScrollToPositionFromTop(itemsCount-1,0,1000);
_startTime=System.currentTimeMillis();
_isMeasuring=true;
}
});
// creating the adapter of the listView
listView.setAdapter(new BaseAdapter()
{
#Override
public View getView(final int position,final View convertView,final ViewGroup parent)
{
final Field field=fields[position%fields.length];
// final View inflatedView=convertView!=null ? convertView : inflater.inflate(R.layout.list_item,null);
final View inflatedView=inflater.inflate(R.layout.list_item,null);
final ImageView imageView=(ImageView)inflatedView.findViewById(R.id.imageView);
final TextView textView=(TextView)inflatedView.findViewById(R.id.textView);
textView.setText(field.getName());
try
{
final int imageResId=field.getInt(null);
imageView.setImageResource(imageResId);
}
catch(final Exception e)
{}
return inflatedView;
}
#Override
public long getItemId(final int position)
{
return 0;
}
#Override
public Object getItem(final int position)
{
return null;
}
#Override
public int getCount()
{
return LISTVIEW_ITEMS;
}
});
}
}
#all: I know that there are optimizations for this code (using the convertView and the viewHolder design pattern) as I've mentioned the video of the listView made by Google. Believe me, I know what's better; this is the whole point of the code.
The code above is supposed to show that it's better to use what you (and the video) shows. But first I need to show the naive way; even the naive way should still work, since I don't store the bitmaps or the views, and since Google has done the same test (hence they got a graph of performance comparison).
The comment from Tim is spot on. The fact that your code does not utilize convertView in its BaseAdapter.getView() method and keep inflating new views every time is the major cause of why it will eventually run out of memory.
Last time I checked, ListView will keep all the views that are ever returned by the getView() method in its internal "recycle bin" container that will only be cleared if the ListView is detached from its window. This "recycle bin" is how it can produce all those convertView and supply it back to getView() when appropriate.
As a test, you can even comment out the code portion where you assign an image to your view:
// final int imageResId = field.getInt(null);
// imageView.setImageResource(imageResId);
And you will still get the memory allocation failure at one point :)
There are two points your code:
As mentioned by previous answers, you are trying create so many new objects, well, that's the main reason of OutOfMemory problem.
Your code is not efficient enough to load all objects continuously (like swipe up/down for scrolling), well, it's lagging.
Here a hint to fix those two common problems:
Field field = fields[position % fields.length];
View v = convertView;
ViewHolder holder = null;
if (v == null) {
v = inflater.inflate(R.layout.list_item,null);
holder = new ViewHolder();
holder.Image = (ImageView) inflatedView.findViewById(R.id.imageView);
holder.Text = (TextView)inflatedView.findViewById(R.id.textView);
v.setTag(holder);
} else {
holder = (ViewHolder) v.getTag();
}
return v;
This is the simple ViewHolder for efficient ListView.
static class ViewHolder {
ImageView Image;
TextView Text;
}
Pretty much simple but very effective coding.
I have been getting oom error for a long time, when I inflate my listview with too many images (even though the images were already compressed)
Using this in your manifest might solve your issue:
android:largeHeap="true"
this will give your app a large memory to work on.
Use this only when there are no alternate ways for your desired output!
To Know about the drawbacks of using largeHeap check this answer
Your catching Exception e, but OutOfMemoryError is Error, not an Exception. So if your want to catch OutOfMemory your can write something like
catch(Throwable e){}
or
catch(OutOfMemoryError e){}
I'm working on an Android project with a lot image loading from a remote server.
I'm using this utility for downloading the images:
http://code.google.com/p/android-imagedownloader/
The main issue is when any image download finishes, the whole Screen would seem to reset.
Along with the view reset the position of the animated UI controls resets too.
That code is based on an article from two years ago and the Android Developers have since given much better information and methods for handling ASync images within a ListView Adaptewr.
Ideally you should be implementing an ImageDownload class or some sorts and using the notifyDataSetChanged(); call on your ListViewAdpater to have the View updated correctly.
Create an ImageLoadedCallback:
// Interfaces
public interface ImageLoadedCallback {
public void imageLoaded(Drawable imageDrawable, String imageUrl);
}
Implement it on your ListAdapter:
All this code is doing is getting the next item to display in the List and then looking to see if we have the image available, if we do - set it. If not, send away our ASync request to load it and then let the Adapter know that it's ready.
public class ArticleAdapter extends SimpleCursorAdapter implements ImageLoadedCallback {
#Override
public View getView(int position, View convertView, ViewGroup parent) {
if(getCursor().moveToPosition(position)) {
ViewHolder viewHolder;
if(convertView == null) {
convertView = inflater.inflate(R.layout.article_list_item, null);
viewHolder = new ViewHolder();
viewHolder.image = (ImageView) convertView.findViewById(R.id.imgArticleImage);
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) convertView.getTag();
}
String image = getCursor().getString(getCursor().getColumnIndex("thumbURL"));
if(imgCache.hasImage(image)) {
viewHolder.image.setImageDrawable(imgCache.loadDrawable(image, this));
} else {
imgCache.loadDrawable(image, this);
}
}
return convertView;
}
public void imageLoaded(Drawable imageDrawable, String imageUrl) {
this.notifyDataSetChanged();
}
}
Main question:
What is the most efficient bug free method of lazy loading remote images in a simple ListView adapter that utilizes the ViewHolder pattern?
I currently have an implementation that will first check a SoftReference Bitmap HashMap for a soft caches version of the image. If that fails I check my hard cache for a copy of the image. If that fails I get it from the web. I do all of this on a separate thread and in a queue to eliminate concurrent or duplicate downloads.
The problem is when loading through a callback. Because I utilize the ViewHolder pattern my views are constantly being recycled and I have not found a solid way to eliminate different images being randomly attached to my ImageViews. I do default to a default image before each load but because the views are being recycled so quickly "old" listeners apply onto my ImageView providing the wrong image which are then replaced with the correct image.
The only semi-solid solution I have found is to use the ViewHolder itself as the listener but this only makes the problem less apparent. It still happens on a fast scroll.
Any help would be appreciated.
Update:
https://github.com/DHuckaby/Prime
I have found a solution to the image switching problem and I will provide a code chunk below. I will not accept it yet though because I do not know if this is the most efficient method which is my original question. If this is implemented correctly it will work perfectly.
public void getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
...
String imagePath = insertImageUrlHere();
Object tag = holder.userThumb.getTag();
if (tag != null && tag.equals(imagePath)) {
// Do nothing
} else {
holder.userThumb.setTag(imagePath);
holder.userThumb.setImageResource(R.drawable.default_image);
AsynchronousImageLoadingUtility.load(imagePath, holder);
}
...
return convertView;
}
private static class ViewHolder implements AsynchronousImageLoadingUtilityCallback {
private ImageView userThumb;
#Override
public void onImageLoad(String source, Bitmap image) {
if (image != null && userThumb.getTag().equals(source)) {
userThumb.setImageBitmap(image);
}
}
}