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 !
Related
I am using this code to layout my ListView, using a different layout based on some data:
#Override
public View getView(int i, View convertView, ViewGroup viewGroup) {
ViewHolder viewHolder;
MyInfo myInfo = getItem(i);
String label = myInfo.getLabel();
if (convertView == null) {
if (!"".equals(label)) {
convertView = LayoutInflater.from(getContext()).inflate(R.layout.info_grey, null);
Log.d(SapphireApplication.TAG, "GREY, label=" + label);
} else {
convertView = LayoutInflater.from(getContext()).inflate(R.layout.info_plain, null);
Log.d(SapphireApplication.TAG, "PLAIN, label=" + label);
}
viewHolder = new ViewHolder();
viewHolder.tvLabel = convertView.findViewById(R.id.tvLabel);
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder)convertView.getTag();
}
viewHolder.tvLabel.setText(label);
return convertView;
}
However, the Log.d is never done for some items in the list. Does that mean Android re-uses an existing convertView, causing it (in this case) to use the wrong layout?
Yes. They are being re-used. And that is the reason you are seeing that message log for few items only.
#Override
public View getView(int i, View convertView, ViewGroup viewGroup) {
if (convertView == null) {
// convertView is null. It means the ListView does not have a view to give to you
// This way, you need to create a new one.
// You will enter here until the ListView has enough Views to fill
// the screen.
// So, just inflate the view and set the View holder here.
// Don't customize your view here (set text, contenet etc)
// So, any log message here will be printed only when the ListView becomes visible (and when you scroll to next item)
// After that, views will be re-used so convertView will no longer be null
} else {
// ListView gave a convertView to you. It means that you are receiving a View
// that was created in the past and it is be re-used now.
// At this moment, convertView still has the content of the old item it was
// representing.
// This view was created in the statement above and after user scrolled the ListView
// it becomes hidden and ready to be re-used.
// Don't customize the view here.. just get the ViewHolder from the View
}
// Here you customize the View. Set content, text, color, background etc
// The ViewHolder is just a helpful class to help you to access
// all View inside the convertView without needing to perform the
// findViewById again.
return convertView;
}
In this basic example however, all convertViews are similar. They were inflated from the same layout.
This works fine for when you have a single view type per line. all items are similar but with different content.
This still works if you have small differences. For example, you can inflate same layout and control the visibility of some of its Views according to the position (position 1 has an image and position 2 don't).
However, there are some cases where you really need to inflate different layouts per row.
For example, the "grey" layout is very different from the "plain" layout. On this case, you need to update you
code as follows:
private static final int GREY = 1;
private static final int PLAIN = 2;
private static final int TOTAL_VIEW_TYPES = 2; // Grey and Plain
#Override
public int getViewTypeCount() {
// Tell the list view that you have two types of Views (Grey and plain)
return TOTAL_VIEW_TYPES;
}
#Override
public int getItemViewType(int position) {
// You must inform view type for given position
String label = myInfo.getLabel();
if (!"".equals(label)) {
return GREY;
} else {
return PLAIN;
}
}
#Override
public View getView(int position, View convertView, ViewGroup viewGroup) {
if (convertView == null) {
// If view is null, you must create the view. But you need to create the
// correct view for given position
if(getItemViewType(position) == GREY) {
// Inflate grey;
} else {
// inflate plain
}
} else {
// convertView is not null. It is being reused.
// Android will give you the proper view here. If you are expecting
// a plain type, that's what you will get. Android won't re-use
// plain layout where you are expecting to have the grey layout.
// It will re-use the proper view for each position (following to the getItemViewType()).
// ListView is very robust.
}
// Update the view here.. Just remember that here you may have two different
// types of view.. grey or plain.
return convertView;
}
The most cool about theses concepts is that they are valid for any view that uses this View<->Adapter relation..
RecyclerView, ListView, Spinner, PagerView etc.
My problem is I have a ListView in an Activity and now I load text in rows but in one of those rows, I need to load some thumbnails. These thumbs must be clickable to see larger images in another Activity.
I cannot load text in the first row of the ListView and thumbs in the second.
Anyone can help?
You can do this by defining a custom Adapter, defining different viewType, for instance:
public class MyAdapter extends Adapter {
// [implement required methods]
#Override
public int getViewTypeCount() {
return 2;
}
#Override
public int getItemViewType(int position) {
if (/* specific row */) {
return 1;
}
return 0;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View rowView;
if (/* specific row */) {
rowView = inflater.inflate(R.layout.layout_image,
parent, false);
} else {
rowView = inflater.inflate(R.layout.layout_text,
parent, false);
}
// etc.
}
}
You can do this by making changes in you adapter class. In that class you have to set some logic inside getView() method so that the images get displayed where ever you want. And you have to display the image programmatically because if you set the imagview in your xml then you must have to set image in every imageview.
This is my opinion, there could be a smarter way to do this. I am new to android.
I'm implementing a custom gallery that allows multiple photo selection.
I'm using a GridView with a simple ImageAdapter class extended from BaseAdapter.
Here is my ImageAdapter class:
public class ImageAdapter extends BaseAdapter {
private LayoutInflater mInflater;
public ImageAdapter() {
mInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
public int getCount() {
return count;
}
public Object getItem(int position) {
return null;
}
public long getItemId(int position) {
return 0;
}
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
holder = new ViewHolder();
convertView = mInflater.inflate(R.layout.galleryitem, null);
holder.imageview = (ImageView) convertView
.findViewById(R.id.thumbImage);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
holder.imageview.setId(position);
holder.imageview.setLongClickable(true);
holder.imageview.setOnLongClickListener(new OnLongClickListener() {
public boolean onLongClick(View arg0) {
int id = arg0.getId();
ImageView img = (ImageView) arg0;
if (thumbnailsselection[id]) {
Log.d("PRTAG", "deselecting img with id: " + img.getId());
img.setBackgroundResource(R.drawable.imgview_noborder);
img.setAlpha(255);
thumbnailsselection[id] = false;
} else {
Log.d("PRTAG", "selecting img with id: " + img.getId());
img.setAlpha(128);
img.setBackgroundResource(R.drawable.imgview_border);
thumbnailsselection[id] = true;
}
return true;
}
});
holder.imageview.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
int id = v.getId();
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.parse("file://" + arrPath[id]),
"image/*");
startActivity(intent);
}
});
holder.imageview.setImageBitmap(thumbnails[position]);
holder.id = position;
return convertView;
}
}
All images are added correctly, the onClick() method works fine (it opens up the correct image).
The problem is with onLongClick(). I'm adding a custom background and setting the alpha (128 - image selected, 255 - image not selected) on the image that is long clicked on. The actual selection works fine, it selects the right images.
The actual problem is that the background and alpha are set to multiple (random) images when scrolling the grid view.
Has anyone experienced something like this? Any thoughts on what could be causing this?
Thanks.
Your views are reusable, it means that you need to update alpha every time getView is invoked. Not only on LongPress
Create ArrayList selectedImages ivar for all selected images.
- onLongPress add/remove image to selectedImages
- In your getView method check if image is stored in a list and set according alpha value
You need to get familiar with ListView views reusage concept. Basically GridView reuses views while scrolling. So if You change some view, then it obviously will be changed then reused (until You're not changing the property of it in getView()). Checkout Google I/O video with more explanation about ListView, because most of it applies to GridView also.
So, if You need to have some views with different properties, then You have 2 options:
Make views of another type (in other words use getItemViewType() and getItemViewTypeCount() and change types dynamically with calling notifyDataSetChanged());
Store specific items positions (or some kind of flags in ViewHolder, in Your case it might be thumbnailsselection array information) and setup view property every getView() call using stored before information;
I suggest not to use ImageView's click and long click listeners in getView() method, but to use ListView's or GridView's setOnItemLongClickListener and setOnItemClickListener.
In these listeners you should just save the state of an item, selected or not, and in getView() method you should look-up the item's state and do the following:
if (thumbnailsselection[id]) {
Log.d("PRTAG", "deselecting img with id: " + img.getId());
img.setBackgroundResource(R.drawable.imgview_noborder);
img.setAlpha(255);
} else {
Log.d("PRTAG", "selecting img with id: " + img.getId());
img.setAlpha(128);
img.setBackgroundResource(R.drawable.imgview_border);
}
Basically, in every getView() call you should verify your data object state and always adjust the view's state before returning it.
I am wondering how to manage the views inside a ListView.
I have a custom Adapter that is set on the ListView, this Adapter overrides the getView method
public View getView(int position, View convertView, ViewGroup parent) {
View v = convertView;
if (v == null) {
v = mInflater_.inflate(R.layout.news_newsentry, null);
}
final NewsItem newsItem = getItem(position);
if (newsItem != null) {
// Do stuff
}
return v;
}
But the thing is that when the user clicks on an item, I slightly change the view to make it bigger. It works well, but when the item view is recycled, it keeps the "big" height to display another item.
To prevent that, I changed the code to create a new View each time
Change:
View v = convertView;
if (v == null) {
v = mInflater_.inflate(R.layout.news_newsentry, null);
}
By
View v = mInflater_.inflate(R.layout.news_newsentry, null);
The problem now is that when the item disappears from the list and reappears (the list is scrolled), the view is completely new and the height is set to "small".
My question then: how to manage the items views to keeps their properties, without messing with the other views and the view recycling?
I think you can get the result you want by using the ListView built in support for more than one view type in a list.
In your adapter you would implement additional methods similar to
#Override
public int getItemViewType(int position) {
int type = 0;
if (position == mySelectedPosition) {
type = 1;
}
return type;
}
#Override
public int getViewTypeCount() {
return 2;
}
Then your getView method will be handed a view of the correct type for the position of the item. Ie, the selected item will always be given a "big" view to re-use.
Creating a new View every time is not recommended for performance and memory reasons.
I'm having problems with some BaseAdapter code that I adapted from a book. I've been using variations of this code all over the place in my application, but only just realized when scrolling a long list the items in the ListView become jumbled and not all of the elements are displayed.
It's very hard to describe the exact behavior, but it's easy to see if you take a sorted list of 50 items and start scrolling up and down.
class ContactAdapter extends BaseAdapter {
ArrayList<Contact> mContacts;
public ContactAdapter(ArrayList<Contact> contacts) {
mContacts = contacts;
}
#Override
public int getCount() {
return mContacts.size();
}
#Override
public Object getItem(int position) {
return mContacts.get(position);
}
#Override
public long getItemId(int position) {
return position;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View view;
if(convertView == null){
LayoutInflater li = getLayoutInflater();
view = li.inflate(R.layout.groups_item, null);
TextView label = (TextView)view.findViewById(R.id.groups_item_title);
label.setText(mContacts.get(position).getName());
label = (TextView)view.findViewById(R.id.groups_item_subtitle);
label.setText(mContacts.get(position).getNumber());
}
else
{
view = convertView;
}
return view;
}
}
You are only putting data in the TextView widgets when they are first created. You need to move these four lines:
TextView label = (TextView)view.findViewById(R.id.groups_item_title);
label.setText(mContacts.get(position).getName());
label = (TextView)view.findViewById(R.id.groups_item_subtitle);
label.setText(mContacts.get(position).getNumber());
to be after the if/else block and before the method return, so you update the TextView widgets whether you are recycling the row or creating a fresh one.
To further clarify the answer of CommonsWare, here is some more info:
The li.inflate operation (needed here for parsing of the layout of a row from XML and creating the appropriate View object) is wrapped by an if (convertView == null) statement for efficiency, so the inflation of the same object will not happen again and again every time it pops into view.
HOWEVER, the other parts of the getView method are used to set other parameters and therefore should NOT be included within the if (convertView == null){ }... else{ } statement.
In many common implementation of this method, some textView label, ImageView or ImageButton elements need to be populated by values from the list[position], using findViewById and after that .setText or .setImageBitmap operations.
These operations must come after both creating a view from scratch by inflation and getting an existing view if not null (e.g. on a refresh).
Another good example where this solution is applied for a ListView ArrayAdapter appears in https://stackoverflow.com/a/3874639/978329