I am trying to load images dynamically in a list view in android. The code below loads the image according to the viewed position and loads the image in that array position. However, as it gets the data in background, sometimes the image in the first array is loaded in the second imageview position, second in third etc. I guess when the getDataInBackground part of the first item in the array is already finished the sytem is trying to create the second cell at that time and loads it to the second cell. How can I handle this, I am using android studio AVD with version kitkat and nexus 5 emulator.
private class MyListAdapter extends ArrayAdapter<String> {
public MyListAdapter() {
super(getActivity().getApplicationContext(), R.layout.fragment_users_cell, myItemList);
}
#Override
public View getView(final int position, View convertView, ViewGroup parent) {
View cellView = convertView;
if (cellView == null){
cellView = getActivity().getLayoutInflater().inflate(R.layout.fragment_users_cell, parent, false);
}
cellProfileImage = (ImageView) cellView.findViewById(R.id.fragment_users_cell_profileImg);
System.out.println("The current position" + position);
if (resultsImageFiles.get(position)==null) {
Bitmap image = BitmapFactory.decodeResource(getActivity().getResources(), R.drawable.ph2);
cellProfileImage.setImageBitmap(image);
} else {
ParseFile file = resultsImageFiles.get(position);
file.getDataInBackground(new GetDataCallback() {
#Override
public void done(byte[] data, ParseException e) {
if (e == null) {
Bitmap image = BitmapFactory.decodeByteArray(data, 0, data.length);
cellProfileImage.setImageBitmap(image);
}
}
});
}
return cellView;
}
}
Listview reuse convertview --> Some itemView has same value for view. In this case, you have to remove cellProfileImage before Bitmap loaded.
try to remove all cellProfileImage.setImageBitmap(image); and move it above return cellView; don't forget to declare Bitmap image after View cellView = convertView; but you can also create a class viewHolder that contain your imageview like this link:http://lucasr.org/2012/04/05/performance-tips-for-androids-listview/
Related
I'm developing an android application. One of my fragments contains a simple listview showing friend list. Each friend can have its own profile image - it is set by the Glide library. When user has no profile pic set the default image is shown. My problem is, that every time, first element on the list gets the same picture which is set on the last element of the list which is not default picture. What i mean is shown on pic:
user with name wiktor has set profile picture and as you see the first position bonzo has wiktor's profile pic ( bonzo should have default pic )
there is also a problem with deleting user form list:
as you see, i removed majka from friend list and next elements gets her picture.
The default profile picture is set in inside row layout xml from drawables.
Here is code of my listview adapter:
public class FriendsAdapter extends ArrayAdapter<FriendData> {
static class FriendHolder {
TextView friendName;
TextView friendRank;
ImageView friendIcon;
ImageButton deleteFriendBtn;
ImageButton banFriendBtn;
}
private List<FriendData> list;
public FriendsAdapter(Context context, int resource, List<FriendData> objects) {
super(context, resource, objects);
list = objects;
}
#Override
public View getView(final int position, View convertView, final ViewGroup parent) {
final FriendData element = getItem(position);
final FriendHolder viewHolder;
if (convertView == null) {
viewHolder = new FriendHolder();
LayoutInflater inflater = LayoutInflater.from(getContext());
convertView = inflater.inflate(R.layout.friend_layout, parent, false);
viewHolder.friendIcon = (ImageView) convertView.findViewById(R.id.friendIcon);
viewHolder.friendName = (TextView) convertView.findViewById(R.id.friendName);
viewHolder.friendRank = (TextView) convertView.findViewById(R.id.friendRank);
viewHolder.deleteFriendBtn = (ImageButton) convertView.findViewById(R.id.deleteFriendBtn);
viewHolder.banFriendBtn = (ImageButton) convertView.findViewById(R.id.banFriendBtn);
convertView.setTag(viewHolder);
} else {
viewHolder = (FriendHolder) convertView.getTag();
}
if (element.getPhoto() != null) {
String photo = S3ImageHandler.SMALL_PROFILE_ICON_PREFIX + element.getPhoto();
String url = String.format(S3ImageHandler.AMAZON_PROFILE_DOWNLOAD_LINK, photo);
Glide.with(getContext())
.load(url)
.asBitmap()
.diskCacheStrategy(DiskCacheStrategy.ALL)
.placeholder(R.drawable.user_small)
.into(new BitmapImageViewTarget(viewHolder.friendIcon) {
#Override
protected void setResource(Bitmap resource) {
RoundedBitmapDrawable circularBitmapDrawable = RoundedBitmapDrawableFactory.create(getContext().getResources(), resource);
circularBitmapDrawable.setCircular(true);
viewHolder.friendIcon.setImageDrawable(circularBitmapDrawable);
}
});
}
viewHolder.friendName.setText(element.getId());
viewHolder.friendRank.setText(String.format("%s %d", getContext().getString(R.string.text_rank), element.getRank()));
viewHolder.deleteFriendBtn.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
confirmDelete(element, position, parent);
}
});
viewHolder.banFriendBtn.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
confirmBan(element, position, parent);
}
});
return convertView;
}
removing user from friend list:
remove(element);
notifyDataSetChanged();
does any of you see what i do wrong ? i would be very grateful for some help. thank you :)
You're resuing list items (by definition of recycler view) which means that if an image was set to an item and you don't clear it, the image will remain there. So even though setText changes the labels viewHolder.friendIcon is not touched. The fix is really simple:
if (element.getPhoto() != null) {
Glide.with(getContext())...
} else {
Glide.clear(viewHolder.friendIcon); // tell Glide that it should forget this view
viewHolder.friendIcon.setImageResource(R.drawable.user_small); // manually set "unknown" icon
}
Also remove the drawable from the xml, or at least change to tools:src which will help reducing the inflation time; the value is overwritten by Glide every time anyway.
To reduce complexity there's an alternative:
class FriendData {
// if you can't modify this class you can also put it in a `static String getPhotoUrl(Element)` somewhere
public void String getPhotoUrl() {
if (this.getPhoto() == null) return null;
String photo = S3ImageHandler.SMALL_PROFILE_ICON_PREFIX + this.getPhoto();
String url = String.format(S3ImageHandler.AMAZON_PROFILE_DOWNLOAD_LINK, photo);
return url;
}
}
and then replace the whole if (element.getPhoto() != null) {...} with:
Glide.with(getContext())
.load(element.getPhotoUrl()) // this may be null --\
.asBitmap() // |
.diskCacheStrategy(DiskCacheStrategy.ALL) // |
.placeholder(R.drawable.user_small) // |
.fallback(R.drawable.user_small) // <--------------/
.into(new BitmapImageViewTarget(viewHolder.friendIcon) { ... })
;
This will also result in proper behavior because even though there's no image url Glide will take care of setting something, see JavaDoc or source of fallback.
As a sidenote also consider using CircleCrop. Aside from caching benefits it would also support GIFs because you can remove the .asBitmap() and the custom target.
I have a ListView of receipt data, which includes a vendor, value of the receipt, and the date on the receipt.
I'd like to add an lazy-loaded image of the receipt to my ListView. I have thumbnail images of the receipt stored in an AWS location, but the receipt images could be 'processing' and not immediately available. Unfortunately, the URL for the remote image is not available in my current ListView dataset, which means I have to call an async task to retrieve the receipt image data for each displayed row in my adapter.
To get the receipt image data, I have created a service call (I'm using Retrofit) to hit my API to pull down the JsonObject that contains the receipt image thumbnail location OR an indication that the image is still processing/not available. The receipt image data is requested via a receipt id.
Here's what I currently have:
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View row = convertView;
ReceiptHolder holder = null;
if (row == null) {
LayoutInflater inflater = ((Activity)context).getLayoutInflater();
row = inflater.inflate(layoutResourceId, parent, false);
holder = new ReceiptHolder();
holder.txtVendor = (TextView)row.findViewById(R.id.vendor_field);
holder.txtAmount = (TextView)row.findViewById(R.id.amount_field);
holder.txtDate = (TextView)row.findViewById(R.id.date_field);
row.setTag(holder);
} else {
holder = (ReceiptHolder)row.getTag();
}
Receipts r = data[position];
String fullValue = r.amount_symbol + r.amount;
holder.txtVendor.setText(r.vendor);
holder.txtAmount.setText(fullValue);
holder.txtDate.setText(r.date);
return row;
}
public class ReceiptHolder {
public TextView txtVendor;
public TextView txtAmount;
public TextView txtDate;
public ProgressBar receiptFetch;
public ImageView receiptThumb;
}
So, when my custom adapter is performing the getView, I'd like the following to happen:
The ProgressBar receiptFetch to be set as visible on initial draw to indicate that the image is still being retrieved/processed. I'd like the receipt data to be visible and a ProgressBar indicating that the image is still being retrieved or processed.
Some kind of non-blocking mechanism to go back and set ProgressBar's visibility to GONE and simultaneously setting ImageView receiptThumb with the asynchronously retrieved URL for each row item as the task completes for that particular row.
Now, here's my question:
How do I get the View holder pattern to play nice without hanging up the UX? Ideally when the adapter is doing its thing, I could kick off an async task to retrieve the receipt image data for each receipt while populating the other fields inherent to receipt data... but I'm not sure how to capture the response to turn off the ProgressBar and go back to populate the ImageView.
I've been unsuccessfully dabbling at this for a bit, and I'm stumped. Does my question make sense? Any ideas?
You can use UniversalImageLoader library to load your remote image.
Refer this: https://github.com/nostra13/Android-Universal-Image-Loader
final View imageLayout = inflater.inflate(R.layout.list_item, null);
final ImageView imageView = ...
imageLoader.displayImage(images[position], imageView, options, new SimpleImageLoadingListener() {
#Override
public void onLoadingStarted(String imageUri, View view) {
spinner.setVisibility(View.VISIBLE);
}
#Override
public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
spinner.setVisibility(View.GONE);
}
#Override
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
spinner.setVisibility(View.GONE);
}
});
Picasso. Picasso is the answer.
Universal Image Loader would also work, but was simply overkill for the problem I was trying to solve. Picasso does inline lazy loading, and has options to allow for a temporary #Drawable to be shown while the thumbnail is retrieved.
I ended up doing the following - didn't need the ProgresBar anymore since Picasso took care of it. Note that Picasso can be used in a static way - but I had to create a custom OkHttpClient for our networking configuration :
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View row = convertView;
ReceiptHolder holder = null;
if (row == null) {
LayoutInflater inflater = ((Activity) context)
.getLayoutInflater();
row = inflater.inflate(layoutResourceId, parent, false);
holder = new ReceiptHolder();
holder.txtVendor = (TextView) row
.findViewById(R.id.vendor_field);
holder.txtAmount = (TextView) row
.findViewById(R.id.amount_field);
holder.txtDate = (TextView) row.findViewById(R.id.date_field);
holder.receiptThumb = (ImageView) row
.findViewById(R.id.receipt_image);
row.setTag(holder);
} else {
holder = (ReceiptHolder) row.getTag();
}
Receipts r = data[position];
String fullValue = r.amount_symbol + r.amount;
holder.txtVendor.setText(r.vendor);
holder.txtAmount.setText(fullValue);
holder.txtDate.setText(r.date);
picasso.with(getActivity())
.load(r.medium_jpg_url)
.placeholder(R.drawable.ic_empty)
.error(R.drawable.ic_error)
.resize(100, 100)
.centerCrop()
.into(holder.receiptThumb);
return row;
}
I have implemented lazy loading using onScrollListener stuff. One issue I am having is for the first time when activity is started the images don't display. Images get displayed when I scroll the listview. Any reason why the images don't load for the first time. Please let me know. Thanks.
The getView() code is as follows:
public View getView(int position, View convertView, ViewGroup parent) {
View vi=convertView;
ContentListHolder contentHolder = null;
if(convertView==null)
{
vi = inflater.inflate(layoutItem, null);
contentHolder = new ContentListHolder();
contentHolder.textview = (TextView)vi.findViewById(idText);
contentHolder.imageView =(ImageView)vi.findViewById(idImage);
vi.setTag(contentHolder);
}
else
{
contentHolder = (ContentListHolder) convertView.getTag();
}
contentHolder.textview.setText("item "+position);
contentHolder.imageView.setImageResource(layoutstub);
Bitmap bitmap = imageLoader.getBitmapFromCache(data[position]);
notifyDataSetChanged();
if(bitmap != null)
{ contentHolder.imageView.setImageBitmap(bitmap);
}
return vi;
}
It happens just because you haven't set the tag for your ImageView.
Try:
contentHolder.imageView.setTag(bitmap);
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();
}
}
I have a grid view which is populated using a custom ImageAdapter class extending BaseAdapter.
The images are dynamically loaded from a particular folder in the SD card. I have named the images according to their postition (1.png, 2.png etc.). I have also set an OnClickListener for the grid items: an audio file with the same name as the image is played from the SD card.
It works well when the number of images is less and fits on a screen.
But when the number is large and the images doesn't fit on a screen, the next set of rows displayed by scrolling the screen downwards is mostly repetition of images from the first few rows rather than the images at the corresponding position.
I find from the logcat that the getView() function of the adapter class gets called initially only for the images which are visible on the screen and while scrolling downwards, its not being called properly for further positions
Also sometimes the entire set of images gets re-arranged.
Should I do anything different from the basic implementation of grid view for properly displaying large number of images? Is there anything else I must be taking care of?
EDIT - CODE
I'm setting each tab using
tabGrid[i].setAdapter(new ImageAdapter(this,i));
This is the image adapter class
#Override
public int getCount() {
// fileNames is a string array containing the image file names : 1.png, 2.png etc
return fileNames.length;
}
#Override
public Object getItem(int position) {
return null;
}
#Override
public long getItemId(int position) {
// I did not use this function
return 0;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
// TODO Auto-generated method stub
View v;
if(convertView==null) {
LayoutInflater inflater = (LayoutInflater)mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
v = inflater.inflate(R.layout.grid_image, null);
ImageView iv = (ImageView)v.findViewById(R.id.icon_image);
String bitmapFileName = fileNames[position];
Bitmap bmp =(Bitmap)BitmapFactory.decodeFile(dir.getPath() + "/" + bitmapFileName);a
iv.setImageBitmap(bmp);
}
else {
v = convertView;
}
return v;
}
Does the getItem() and getItemId() functions matter? The directories and file names are all valid.
Here's a quick fix which should be better.
#Override
public String getItem(int position) {
return fileNames[position];
}
#Override
public long getItemId(int position) {
return position;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View v;
if(convertView==null) {
LayoutInflater inflater = (LayoutInflater)mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
v = inflater.inflate(R.layout.grid_image, parent, false);
}
else {
v = convertView;
}
ImageView iv = (ImageView)v.findViewById(R.id.icon_image);
String bitmapFileName = getItem(position);
Bitmap bmp =(Bitmap)BitmapFactory.decodeFile(dir.getPath() + "/" + bitmapFileName);a
iv.setImageBitmap(bmp);
return v;
}
I filled getItem, it's not 100% needed but it's always better to have it. The rest of your adapter code can then rely on it
The item id should be different for every entry, you could either use getItem(position).hashCode() (might be slower) or just return position (which I did here).
The getView method is a bit more tricky. The idea is that if the convertView is null, you create it. And then, in every case, you set the view's content.
The inflate in the getView item should use the parent as parent, and the "false" is there to tell the system not to add the new view to the parent (the gridview will take care of that). If you don't, some layout parameters might get ignored.
The erorr you had was because the views were getting recycled (convertView not null) and you weren't setting the content for those. Hope that helps !