I need to display some images in a RecyclerView.
These images should be rendered "on the fly" as user is scrolling the list. Rendering each image takes a long time: 50-500ms. Before the image is displayed to the user, a progress bar is displayed.
Due to the long rendering time this part is placed into an AsyncTask.
See the code below:
class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
class ViewHolder extends RecyclerView.ViewHolder {
ImageView myImg;
ProgressBar myProgressBar;
public ViewHolder(View view) {
super(itemView);
myImg = view.findViewById(R.id.myImg);
myProgressBar = view.findViewById(R.id.myProgressBar);
}
}
#Override
public void onBindViewHolder(#NonNull ViewHolder holder, int position) {
holder.myProgressBar.setVisibility(View.VISIBLE);
AsyncTask.execute(() -> {
Bitmap bitmap = longRenderingFunction();
holder.myImg.post(() -> {
holder.myImg.setImageBitmap(bitmap);
holder.myProgressBar.setVisibility(View.GONE);
If the user is scrolling the list slowly, e.g. 1-2 screens per second, the images are loaded correctly. Sometimes one can notice the progress bar, which quickly disappears.
But if the user is scrolling very fast, the images appear and then are replaced, sometimes two times:
In general it is clear, that on fast scrolling:
Many onBindViewHolder are called and multiple AsyncTasks per recyclable item ViewHolder are started.
Even a new onBindViewHolder is triggered for the same item, the old AsyncTask keep running
Then AsyncTasks for the same ViewHolder are completing one after another.
Each AsyncTask puts its own resulting bitmap to the ImageView.
My intentions would be:
minimum: do not setImageBitmap from the outdated AsyncTasks
maximum: to stop already outdated AsyncTasks as soon as possible to save system resources
I would appreciate to hear some hints or maybe solutions for this problem.
Here is the solution I came up with.
Stop flickering
The first part is pretty simple:
A simple index was introduced:
int loadingPosition;
At the beginning of onBindViewHolder the current position is saved:
holder.loadingPosition = position;
Before switching the bitmap the current position is checked against the last started. If a new task has been already started, the index was changed beforehand, then the image will not be updated:
if (holder.loadingPosition == position) {
holder.myImg.setImageBitmap(bitmap);
Improve performance
To prevent the task to run further when not needed a Future object was introduced. It allows to manage the started task, while AsyncTask does not:
Future<?> future;
Start the async task via ExecutorService instead of AsyncTask:
holder.future = executor.submit(() -> { ...
Stopping the running task, if it is still running:
if ((holder.future != null) && (!holder.future.isDone()))
holder.future.cancel(true);
Complete code
The whole code looks like that:
class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
private ExecutorService executor = Executors.newSingleThreadExecutor();
class ViewHolder extends RecyclerView.ViewHolder {
ImageView myImg;
ProgressBar myProgressBar;
int loadingPosition;
Future<?> future;
public ViewHolder(View view) {
super(itemView);
myImg = view.findViewById(R.id.myImg);
myProgressBar = view.findViewById(R.id.myProgressBar);
}
}
#Override
public void onBindViewHolder(#NonNull ViewHolder holder, int position) {
holder.loadingPosition = position;
if ((holder.future != null) && (!holder.future.isDone()))
holder.future.cancel(true);
holder.myProgressBar.setVisibility(View.VISIBLE);
holder.future = executor.submit(() -> {
Bitmap bitmap = longRenderingFunction();
holder.myImg.post(() -> {
if (holder.loadingPosition == position) {
holder.myImg.setImageBitmap(bitmap);
holder.myProgressBar.setVisibility(View.GONE);
Maybe using the both methods is a little bit overkill, but it provides some level of insurance.
Related
My app is all about performance, so I would really like to optimize this RecyclerView as much as possible. I have measured how long every part takes to complete, and the whole thing needs about 150ms to load. Here is the RELEVANT code:
public class AppAdapter extends RecyclerView.Adapter<AppAdapter.TabViewHolder> {
private Context context;
private MenuActivity menuActivity;
private Intent intent;
private ArrayList<String> stringys = new ArrayList<>();
public void setUpAdapter(Context mContext, MenuActivity mMenuActivity, Intent mIntent, ArrayList<String> mString) {
this.menuActivity = mMenuActivity;
this.context = mContext;
this.intent = mIntent;
stringys.addAll(mString);
}
#Override
public TabViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(context);
Log.d(TAG, "CreatingViewholder " + "Time: " + menuActivity.deltaTime());
View view = inflater.inflate(R.layout.item_tab, parent, false); //Here is where the wait happens
Log.d(TAG, "ViewHolderCreated " + "Time: " + menuActivity.deltaTime());
return new TabViewHolder(view);
}
#Override
public void onBindViewHolder(#NonNull TabViewHolder holder, int position) {
holder.bind(position, holder);
}
#Override
public int getItemCount() {
return stringys.size();
}
class TabViewHolder extends RecyclerView.ViewHolder {
TextView text;
ImageView image;
TabViewHolder(View itemView) {
super(itemView);
text = itemView.findViewById(R.id.label);
image = itemView.findViewById(R.id.icon);
}
void bind(int position, TabViewHolder holder) {
new LongOperation(text, image).execute(stringys.get(position));
}
}
private class LongOperation extends AsyncTask<String, Void, Void> {
TextView text;
ImageView image;
CharSequence textToSet;
Drawable imageToSet;
public LongOperation(TextView text, ImageView image) {
super();
this.text = text;
this.image = image;
}
#Override
protected Void doInBackground(String... params) {
textToSet = params[0].getTitle(context.getDefaultSharedPreferences());
imageToSet = params[0].getIcon(context.getDefaultSharedPreferences());
return null;
}
#Override
protected void onPostExecute(Void result) {
text.setText(textToSet);
image.setImageDrawable(imageToSet);
}
}
}
I have several ideas/questions about this:
Is it possible to reuse this ViewHolder? I'm using it every time, and each inflation takes about 5ms, which adds up quickly because this is a grid, and I have about 40 holders loading when I launch this.
If it helps, I am also ready to use another kind of view. I took recyclerview as it made the most sense imo, but if there is a better-performing view I can change to that.
Would it help if I used a Linear Layout, and put 4 of the ViewHolders I currently use next to each other? Would my time be then reduced by 4?
In the asynctask, I call context.getDefaultSharedPreferences() twice. Would it load faster (the async part) if I did it once and had it as a variable to pass?
The asynctask it might leak if it isn't static. I assume that's not a problem because it finished very quickly anyways, right?
RecyclerView is used for long lists, implementation of ViewHolder pattern improves performance by re-using Views after they leave the screen. If you have don't have enough items to fill the viewport there's no reason to use it.
Also keep in mind while debugging setting bitmaps is much slower, if possible try building as "release" and see how it performs.
Your ViewHolder will be reused as you scroll down, but you need at least enough view holders to display what is currently on screen, plus a few more that are just off screen. So when you first load this are there 40 views displayed on the screen? If not, you have an issue.
It depends if you can scroll your view a lot.
You might get a slight improvement but I doubt it.
Shared preferences calls are cached. You don't have to worry about them taking a long time after the first access.
Not sure
Other improvements:
You could move your LayoutInflater creation to setUpAdapter()
What is in R.layout.item_tab? Make sure your hierarchy of views is as flat as possible. Use Constraint Layout if you can.
I am getting some odd behavior in my scroll-able listview. It is controlled by a BaseAdapter.
As I scroll up or down it will sometimes hang and then bounce back in the opposite direction. I have no idea why and do not know how to trouble shoot it.
My base adapter is below and it loads about 90 fragments with an image and a bunch of text.
public class PosterListAdapter extends BaseAdapter {
private final ArrayList<Poster> listItems;
private final LayoutInflater inflater;
public PosterListAdapter(ArrayList<Poster> listItems, LayoutInflater inflater) {
this.listItems = listItems;
this.categoryList = categoryItems;
this.inflater = inflater;
}
#Override
public int getCount() {
//Log.d("getCount", String.valueOf(this.listItems.size()));
return this.listItems.size();
}
#Override
public Poster getItem(int i) {
return this.listItems.get(i);
}
#Override
public long getItemId(int i) {
return 0;
}
#Override
public View getView(int i, View convertView, ViewGroup viewGroup) {
MyViewHolder mViewHolder;
SparseArray<String> categoryMap = db.getCategoryMap();
if (convertView == null) {
convertView = inflater.inflate(R.layout.catalog_list_fragment, viewGroup, false);
mViewHolder = new MyViewHolder(convertView);
convertView.setTag(mViewHolder);
} else {
mViewHolder = (MyViewHolder) convertView.getTag();
}
//LayoutParams params = (LayoutParams) imageView.getLayoutParams();
Poster item = this.listItems.get(i);
String filename = item.getPosterFilename();
mViewHolder.posterAuthor.setText("Author: "+item.getPresenterFname()+' '+item.getPresenterLname());
mViewHolder.posterAltAuthors.setText("Supporting Authors: "+item.getPosterAuthors());
mViewHolder.posterTitle.setText(item.getPosterTitle());
mViewHolder.posterSynopsis.setText(item.getPosterSynopsis());
mViewHolder.posterNumber.setText("Poster: "+String.valueOf(item.getPosterNumber()));
mViewHolder.posterPresentation.setText("Live Presentation: "+item.getSessionDate()+" at "+item.getSessionTime()+"\nAt Station: "+item.getPosterStation());
String category = categoryMap.get(item.getCatID());
mViewHolder.posterCategory.setText("Category: "+category);
File imgFile = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getAbsolutePath(), "/"+filename+"/"+filename+".png");
//Here I thought maybe the resizing of the image or even the image itself
// was causing the hang up so I tried the list without it and it is still
// hanging.
//mViewHolder.imageView.setImageURI(Uri.fromFile(new File(imgFile.toString())));
if (imgFile.exists()){
/*Bitmap bmp = BitmapFactory.decodeFile(imgFile.toString());
int newWidth = 500;
Bitmap sizedBMP = getResizedBitmap(bmp, newWidth);
mViewHolder.imageView.setImageBitmap(sizedBMP);*/
}
else{
//set no image available
}
return convertView;
}
private class MyViewHolder {
TextView posterTitle, posterAuthor, posterSynopsis, posterCategory, posterNumber, posterAltAuthors,posterPresentation;
ImageView imageView;
public MyViewHolder(View item) {
posterTitle = (TextView) item.findViewById(R.id.poster_title);
posterAuthor = (TextView) item.findViewById(R.id.poster_author);
posterAltAuthors = (TextView) item.findViewById(R.id.poster_altAuthors);
posterSynopsis = (TextView) item.findViewById(R.id.poster_synopsis);
posterCategory = (TextView) item.findViewById(R.id.poster_category);
posterNumber = (TextView) item.findViewById(R.id.poster_number);
posterPresentation = (TextView) item.findViewById(R.id.poster_presentation);
imageView = (ImageView) item.findViewById(R.id.poster_thumb);
}
}
}
I know that is a big chunk of code and it may be ugly.. If you could point me in the right direction as to how to trouble shoot it that would be helpful as well. I am using eclipse.
I bet if you comment out that line File imgFile = new File(... (and the code that depends on imgFile) that your list scrolling will improve.
That's still an I/O operation and since getView() runs in the UI thread, it may cause hiccups.
What you should do is: once you have the ImageView in getView(), you start an AsyncTask to open the file, decode it, and assign the bitmap to the ImageView.
Then you have to handle things like: the ImageView gets recycled, but the task isn't completed.
Read this article: Multithreading for Performance | Android Developers Blog. It's dealing with images from a remote server, but all the principles are still the same.
Also: does this line SparseArray<String> categoryMap = db.getCategoryMap(); do a database lookup? That could cause a hiccup as well.
TL;DR getView() needs to be fast; put slow operations in a non-UI thread.
Database related operations should done in separate thread. as it might take time to get data if number of records are greater.
and getView() method will be called more then one time. we can say it will be called in loop till size of your array list (getCount()).
Second thing: File operation is also should be done in separate thread.because IO task is also some times time consuming.
Solution:
SparseArray<String> categoryMap = db.getCategoryMap();
put this line out of getView() method as i cant see any list dependent parameter in this line. this should be done in either constructor or separate thread.
Run your database operations and File IO in separate thread like using AsyncTask
I agree with previous answer, looks like creating new File is the only heavy operation which hangs your app.
Try to use some image loading library, for example Glide. It will simplify your code, also you won't have to deal with background job. Library will do it all for you.
1.Import library, e.g. if you use gradle insert this into your build.gradle file:
repositories {
mavenCentral() // jcenter() works as well because it pulls from Maven Central
}
dependencies {
compile 'com.github.bumptech.glide:glide:3.7.0'
compile 'com.android.support:support-v4:19.1.0'
}
2.In your adapter replace code related to loading picture with this:
Glide
.with(context)
.load(<full path to your local file>)
.into(mViewHolder.imageView);
3.And it is all. Glide will handle recycling and other things internally, also it is optimized for fast loading and smooth scrolling.
Image related processes really consumes so much memory on the UIThread of the app, you should be setting your image using asynchronous(background) approach. Or if its much of a hassle for you, there are tons of libraries available that will do this. I specifically recommend using Picasso for it is very dynamic, easy to use and is regularly updated. Loading an image would require one line of code:
Picasso.with(getApplicationContext()).load(<your image path or url or resource id>).into(YourImageView);
Picasso also have a variety of image options you can choose from.
The setup I've got a RecyclerView with a custom adapter and custom ViewHolders.
I understand the onCreateViewHolder() method is called when the RecyclerView is running out of ViewHolders to recycle and needs a new one. So I'm just inflating a layout in there and passing it to a new ViewHolder.
Furthermore, onBindViewHolder() is responsible for filling the ViewHolder with data as soon as a new ViewHolder has been created or recycled by the RecyclerView. So what I'm doing in there is calling my method holder.setNode() to pass a data object to the ViewHolder.
The behavior I'm seeing When the activity first launches, all entries are correct. When I'm adding new entries or deleting existing ones, however, things start to get a bit funny.
the title TextView is always set correctly
the background color of the main layout changes seemingly at will, I'm assuming because the RecyclerView is reusing old ones
as does the custom view I have implemented, even though I'm invalidating it and passing it new values which change its appearance noticeably
So I'm wondering: Why aren't those values changed in onBindViewHolder() as soon as views get reused? Or if I'm wrong, what's the real reason for the random switching of layouts?
TaskListAdapter
class TaskListAdapter extends RecyclerView.Adapter<TaskListAdapter.TaskViewHolder> {
private ArrayList<NodeHandler.DbNode> dbNodeList = new ArrayList<>();
...
#Override
public TaskViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.small_task_view, parent, false);
return new TaskViewHolder(v);
}
#Override
public void onBindViewHolder(TaskViewHolder holder, int position) {
final NodeHandler.DbNode dbNode = dbNodeList.get(position);
holder.setNode(dbNode);
holder.wrapper.findViewById(R.id.card_details).setVisibility(View.GONE);
}
...
public static class TaskViewHolder extends RecyclerView.ViewHolder implements ItemTouchHelperViewHolder {
private FrameLayout wrapper;
private TextView title;
private NodeHandler.DbNode dbNode;
public TaskViewHolder(View view) {
...
}
public void setTitle(String str) {
title.setText(str);
}
public void setMarkers(#IntRange(from = 1, to = Node.MAX_URGENCY) int urgency, #IntRange(from = 1, to = Node.MAX_IMPORTANCE) int importance) {
if(!dbNode.isAppointment()) {
wrapper.setBackgroundColor(ContextCompat.getColor(wrapper.getContext(), R.color.lightGray));
}
((QuadrantView) wrapper.findViewById(R.id.quadrant_view)).setDimensions(importance, urgency);
// setDimensions will invalidate the view
}
public void setNode(NodeHandler.DbNode dbNodeObject) {
this.dbNode = dbNodeObject;
setTitle(dbNode.toString());
setMarkers(dbNode.getUrgency(), dbNode.getImportance());
setTips();
}
}
}
Let me know if anything else could matter here. I'd be happy to update the question accordingly.
Values are indeed changed in onBindViewHolder as soon as views get reused.
The real reason for the seemingly random switching of layouts is that onBindViewHolder is currently implemented in a way that assumes that the ViewHolder was freshly created and is being bound for its first time. onBindViewHolder should instead be implemented in a way that assumes that the ViewHolder being bound is being reused so it should either:
reset all the values of the ViewHolder to default values first before setting them to other values or
make sure that everything is set inside onBindViewHolder, so one cannot tell that it was ever previously bound to something else.
Random background color changes:
You are right for suspecting that the random background color problem is caused by the RecyclerView reusing ViewHolders.
The reason why this is happening is because of the following code:
if(!dbNode.isAppointment()) {
wrapper.setBackgroundColor(ContextCompat.getColor(wrapper.getContext(), R.color.lightGray));
}
It only sets the background if the ViewHolder is not an appointment. So if a ViewHolder that is being reused was previously not for an appointment, but is currently for one that is now an appointment, it's background color will be inappropriate.
to fix this, do any of the following:
set the background color of the ViewHolder to some default color before the if statement is executed (as per solution 1 mentioned above):
wrapper.setBackgroundColor(/* default background color */);
if(!dbNode.isAppointment()) {
wrapper.setBackgroundColor(ContextCompat.getColor(wrapper.getContext(), R.color.lightGray));
}
add an else block to the if statement to set the background color of the ViewHolder to the appropriate color (as per solution 2 mentioned above)
if(!dbNode.isAppointment()) {
wrapper.setBackgroundColor(ContextCompat.getColor(wrapper.getContext(), R.color.lightGray));
}
else
{
wrapper.setBackgroundColor(/* appointment background color */);
}
override the RecyclerView.Adapter's getItemViewType to return different view types based on dbNode.isAppointment(), and create different ViewHolder subclasses for displaying each of them
p.s. I don't know what the problem could be regarding the custom views...sorry
I have a RecyclerView adapter that looks like this:
public class RecyclerAdapter extends RecyclerView.Adapter<RecyclerAdapter.ViewHolder> {
private static Context context;
private List<Message> mDataset;
public RecyclerAdapter(Context context, List<Message> myDataset) {
this.context = context;
this.mDataset = myDataset;
}
public static class ViewHolder extends RecyclerView.ViewHolder implements View.OnCreateContextMenuListener, View.OnClickListener {
public TextView title;
public LinearLayout placeholder;
public ViewHolder(View view) {
super(view);
view.setOnCreateContextMenuListener(this);
title = (TextView) view.findViewById(R.id.title);
placeholder = (LinearLayout) view.findViewById(R.id.placeholder);
}
}
#Override
public RecyclerAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.message_layout, parent, false);
ViewHolder vh = new ViewHolder((LinearLayout) view);
return vh;
}
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
Message item = mDataset.get(position);
holder.title.setText(item.getTitle());
int numImages = item.getImages().size();
if (numImages > 0) {
View test = LayoutInflater.from(holder.placeholder.getContext()).inflate(R.layout.images, holder.placeholder, false);
ImageView image = (ImageView) test.findViewById(R.id.image);
Glide.with(context)
.load("http://www.website.com/test.png")
.fitCenter()
.into(image);
holder.placeholder.addView(test);
}
}
#Override
public int getItemCount() {
return mDataset.size();
}
}
However, some of the items in the RecyclerView are showing images when they shouldn't be. How can I stop this from happening?
I do the check if (numImages > 0) { in onBindViewHolder(), but that's still not stopping it from showing images for items that shouldn't have images.
You should set imageView.setImageDrawable (null)
In onBindViewHolder() before setting the image using glide.
Setting image drawable to null fix the issue.
Hope it helps!
The problem is in onBindViewHolder, here:
if (numImages > 0) {
View test = LayoutInflater.from(holder.placeholder.getContext()).inflate(R.layout.images, holder.placeholder, false);
ImageView image = (ImageView) test.findViewById(R.id.image);
Glide.with(context)
.load("http://www.website.com/test.png")
.fitCenter()
.into(image);
holder.placeholder.addView(test);
}
If numImages is equal to 0, you're simply allowing the previously started load into the view you're reusing to continue. When it finishes, it will still load the old image into your view. To prevent this, tell Glide to cancel the previous load by calling clear:
if (numImages > 0) {
View test = LayoutInflater.from(holder.placeholder.getContext()).inflate(R.layout.images, holder.placeholder, false);
ImageView image = (ImageView) test.findViewById(R.id.image);
Glide.with(context)
.load("http://www.website.com/test.png")
.fitCenter()
.into(image);
holder.placeholder.addView(test);
} else {
Glide.clear(image);
}
When you call into(), Glide handles canceling the old load for you. If you're not going to call into(), you must call clear() yourself.
Every call to onBindViewHolder must include either a load() call or a clear() call.
I also had issues with RecyclerView showing wrong images. This happens because RecyclerView is not inflating view for every new list item: instead list items are being recycled.
By recycling views we can ruffly understand cloning views. A cloned view might have an image set from the previous interaction.
This is especially fair if your are using Picasso, Glide, or some other lib for async loading. These libs hold reference to an ImageView, and set an image on that refference when image is loaded.
By the time the image gets loaded, the item view might have gotten cloned, and the image is going to be set to the wrong clone.
To make a long story short, I solved this problem by restricting RecyclerView from cloning my item views:
setIsRecyclable(false)in ViewHolder constructor.
Now RecyclerView is working a bit slower, but at least the images are set right.
Or else cansel loading image in onViewRecycled(ViewHolder holde)
The issue here is that, as you are working with views that are going to be recycled, you'll need to handle all the possible scenarios at the time your binding your view.
For example, if you're adding the ImageView to the LinearLayout on position 0 of the data source, then, if position 4 doesn't met the condition, its view will most likely have the ImageView added when binding position 0.
You can add the content of R.layout.images content inside your
R.layout.message_layout layout's R.id.placeholder and showing/hiding the placeholder depending on the case.
So, your onBindViewHolder method would be something like:
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
Message item = mDataset.get(position);
holder.title.setText(item.getTitle());
int numImages = item.getImages().size();
if (numImages > 0) {
holder.placeholder.setVisivility(View.VISIBLE);
ImageView image = (ImageView)holder.placeholder.findViewById(R.id.image);
Glide.with(context)
.load("http://www.website.com/test.png")
.fitCenter()
.into(image);
}else{
holder.placeholder.setVisibility(View.INVISIBLE);
}
}
Sometimes when using RecyclerView, a View may be re-used and retain the size from a previous position that will be changed for the current position. To handle those cases, you can create a new [ViewTarget and pass in true for waitForLayout]:
#Override
public void onBindViewHolder(VH holder, int position) {
Glide.with(fragment)
.load(urls.get(position))
.into(new DrawableImageViewTarget(holder.imageView,/*waitForLayout=*/ true));
https://bumptech.github.io/glide/doc/targets.html
I also had the same problem and ended with below solution and it working fine for me..
Have your hands on this solution might be work for you too (Put below code in your adapter class)-
If you are using Kotlin -
override fun getItemId(position: Int): Long {
return position.toLong()
}
override fun getItemViewType(position: Int): Int {
return position
}
If you are using JAVA -
#Override
public long getItemId(int position) {
return position;
}
#Override
public int getItemViewType(int position) {
return position;
}
This works for me in onBindViewHolder!
if(!m.getPicture().isEmpty())
{
holder.setIsRecyclable(false);
Picasso.with(holder.profile_pic.getContext()).load(m.getPicture()).placeholder(R.mipmap.ic_launcher_round).into(holder.profile_pic);
Animation fadeOut = new AlphaAnimation(0, 1);
fadeOut.setInterpolator(new AccelerateInterpolator());
fadeOut.setDuration(1000);
holder.profile_pic.startAnimation(fadeOut);
}
else
{
holder.setIsRecyclable(true);
}
I was having same issue I solved by writing holder.setIsRecyclable(false).Worked for me.
#Override
public void onBindViewHolder(#NonNull RecylerViewHolder holder, int position) {
NewsFeed currentFeed = newsFeeds.get(position);
holder.textView.setText(currentFeed.getNewsTitle());
holder.sectionView.setText(currentFeed.getNewsSection());
if(currentFeed.getImageId() == "NOIMG") {
holder.setIsRecyclable(false);
Log.v("ImageLoad","Image not loaded");
} else {
Picasso.get().load(currentFeed.getImageId()).into(holder.imageView);
Log.v("ImageLoad","Image id "+ currentFeed.getImageId());
}
holder.dateView.setText(getModifiedDate(currentFeed.getDate()));
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
override fun getItemViewType(position: Int): Int {
return position
}
This Works for Me
I Had the same issue and i fixed it like this:
GOAL : onViewAttachedToWindow
#Override
public void onViewAttachedToWindow(Holder holder) {
super.onViewAttachedToWindow(holder);
StructAllItems sfi = mArrayList.get(position);
if (!sfi.getPicHayatParking().isEmpty()) {
holder.viewFliperMelk.addSlider(new TextSliderView(mContext.getApplicationContext()).image(T.GET_MELK_IMAGE + '/' + sfi.getPicHayatParking() + ".jpg").setScaleType(BaseSliderView.ScaleType.CenterCrop));
}
if (!sfi.getPicSleepRoom().isEmpty()) {
holder.viewFliperMelk.addSlider(new TextSliderView(mContext.getApplicationContext()).image(T.GET_MELK_IMAGE + '/' + sfi.getPicSleepRoom() + ".jpg").setScaleType(BaseSliderView.ScaleType.CenterCrop));
}
if (!sfi.getPicSalonPazirayi().isEmpty()) {
holder.viewFliperMelk.addSlider(new TextSliderView(mContext.getApplicationContext()).image(T.GET_MELK_IMAGE + '/' + sfi.getPicSalonPazirayi() + ".jpg").setScaleType(BaseSliderView.ScaleType.CenterCrop));
}
if (!sfi.getPicNamayeStruct().isEmpty()) {
holder.viewFliperMelk.addSlider(new TextSliderView(mContext.getApplicationContext()).image(T.GET_MELK_IMAGE + '/' + sfi.getPicNamayeStruct() + ".jpg").setScaleType(BaseSliderView.ScaleType.CenterCrop));
}
}
I had a similar issue when getting pictures from the photo gallery and putting them in a recyclerview with GridLayoutManager(never had the issue with Glide). So in the adapter onBindViewHolder use a HashMap or SparseIntArray to put the current hashcode(this is the common thing that the recycled views have in common) and adapter position inside it. Then call your background task and then once it's done and before you set the image, check to see if the hashcode key - which will always have the current adapter position as the value - still has the same value (adapter position) as when you first called the background task.
(Global variable)
private SparseIntArray hashMap = new SparseIntArray();
onBindViewHolder(ViewHolder holder, int position){
holder.imageView.setImageResource(R.drawable.grey_square);
hashMap.put(holder.hashCode(), position);
yourBackgroundTask(ViewHolder holder, int position);
}
yourBackGroundTask(ViewHolder holder, int holderPosition){
do some stuff in the background.....
*if you want to stop to image from downloading / or in my case
fetching the image from MediaStore then do -
if(hashMap.get(holder.hashCode())!=(holderPos)){
return null;
}
- in the background task, before the call to get the
image
onPostExecute{
if(hashMap.get(holder.hashCode())==(holderPosition)){
holder.imageView.setImageBitmap(result);
}
}
}
So i am just providing an extension to this answer since there is not much space to leave it as comment.
After trying out like mentioned in one of above solutions i found out that, the real issue can still be addressed even if you are using a static resource(is not being downloaded and is available locally)
So basically on onBindViewHolder event i just converted the resource to drawable and added it like below :
imageView.setImageDrawable(ContextCompat.getDrawable(context,R.drawable.album_art_unknown));
this way you wont have an empty space on the view while glide/async downloader is loading the actual image from network.
plus looking at that being reloaded every time i also added below code while calling the recycler adapter class;
recyclerView.setItemViewCacheSize(10);
recyclerView.setDrawingCacheEnabled(true);
so by using above way you wont need to set setIsRecyclable(false) which is degrading if you have larger datasets.
By doing this i you will have a flicker free loading of recyclerview of course except for the initial loads.
I would like to say that if you send the ImageView and any load-async command (for instance loading from S3), the recycler view does get confused.
I did set the bitmap null in the onViewRecycled and tested with attach and detach views etc. the issue never went away.
The issue is that if a holderView gets used for image-1, image-10 and stops at the scroll with image-19, what the user sees is image-1, then image-10 and then image-19.
One method that worked for me is to keep a hash_map that helps know what is the latest image that needs to be displayed on that ImageView.
Remember, the holder is recycled, so the hash for that view is persistent.
1- Create this map for storing what image should be displayed,
public static HashMap<Integer, String> VIEW_SYNCHER = new HashMap<Integer, String>();
2- In your Adapter, onBindViewHolder,
String thumbnailCacheKey = "img-url";
GLOBALS.VIEW_SYNCHER.put(holder.thumbnailImage.hashCode(), thumbnailCacheKey);
3- Then you have some async call to make the network call and load the image in the view right ?
In that code after loading the image from S3, you test to make sure what goes into the View,
// The ImageView in the network data loader, get its hash.
int viewCode = iim.imView[0].hashCode();
if (GLOBALS.VIEW_SYNCHER.containsKey(viewCode))
if (GLOBALS.VIEW_SYNCHER.get(viewCode).equals(bitmapKey))
iim.imView[0].setImageBitmap(GLOBALS.BITMAP_CACHE.get(bitmapKey).bitmapData);
So essentially, you make sure what is the last image key that should go into a view, then when you download the image you check to make sure that's the last image URL that goes in that view.
This solution worked for me.
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){}