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.
Related
I have read and tried several of the solutions to similar problems here on stack overflow, but none of them solved my problem. Here is the thing.
I have a listview which uses CustomListAdapter, each list item has a progress bar, a download button, title text and so on. When the download button is clicked a download operation is performed, and based on the result of the download(whether success or failure) the list item concerned is update(UI changes, such as if complete hide download button, update progress of the progress bar during download)
The listview displays four items at every given time
The problem is that whenever a UI change is made to an item say item 1(with index 0) the item 5 will also have the same changes, likewise if a change is made to item 3, the item 7 takes up those changes. In summary the item N+4 always imitates item N.
A look at my getView() will tell that I have checked all the known boxes.
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View view = convertView;
ViewHolder holder;
if (convertView == null) {
view = inflater.inflate(R.layout.item_mylibrarylist, null);
holder = new ViewHolder();
holder.name = (TextView)view.findViewById(R.id.name);
holder.name.setTypeface(MainActivity.font_bahamas);
holder.author = (TextView)view.findViewById(R.id.author);
holder.author.setTypeface(MainActivity.font_bahamas);
holder.worktype = (TextView)view.findViewById(R.id.worktype);
holder.worktype.setTypeface(MainActivity.font_bahamas);
holder.coverPic = (TextView)view.findViewById(R.id.coverPic);
holder.downloadBt = (TextView)view.findViewById(R.id.downloadBt);
holder.progressBar = (ProgressBar)view.findViewById(R.id.progressBar2);
holder.menuBt = (ImageView)view.findViewById(R.id.menuBt);
holder.position = position;
view.setTag(holder);
} else {
holder = (ViewHolder) view.getTag();
}
if(holder.position == position) {
setValuesForListItemViews(holder, position, view);
}
return view;
}
The method to set each of the list items..
private void setValuesForListItemViews(ViewHolder holder, int position, View view) {
if (!data.isEmpty()) {
// set the list item elements here
final CreativeWork creativeWork = data.get(position);
holder.name.setText(creativeWork.getName().toLowerCase());
holder.author.setText("by " + creativeWork.getOriginal_authors().toLowerCase());
holder.worktype.setText(creativeWork.getWork_type().toLowerCase());
Drawable draw = res.getDrawable(R.drawable.custom_progressbar2);
holder.progressBar.setProgressDrawable(draw);
holder.progressBar.setMax(100);
holder.progressBar.setVisibility(View.INVISIBLE);
holder.menuBt.setOnClickListener(new OnItemClickedListener(view, position, 1, creativeWork, holder.progressBar, holder.downloadBt));
holder.menuBt.setOnCreateContextMenuListener(new MContextMenuListener(creativeWork, holder.progressBar, holder.downloadBt, false));
//load image url
ImageLoader2 imgLoader12 = new ImageLoader2(activity);
imgLoader12.DisplayImage(creativeWork.getName(), R.drawable.downloads, holder.downloadBt);
ImageLoader imgLoader = new ImageLoader(activity);
imgLoader.DisplayImage(SLService.END_POINT + creativeWork.getImage_url(), R.drawable.soul_lounge, holder.coverPic);
//check if file already exist and switch off download button
DBHelper helper = new DBHelper(activity);
CreativeWork cw = helper.getCreativeWork(creativeWork);
if (cw != null) {
File file = new File(cw.getFilePath());
if (file.exists()) {
holder.menuBt.setOnCreateContextMenuListener(new MContextMenuListener(creativeWork, holder.progressBar, holder.downloadBt, true));
//check if the file download was complete
if (cw != null) {
if (cw.getFileSize() > file.length()) {
holder.progressBar.setProgressDrawable(activity.getResources().getDrawable(R.drawable.custom_progressbar3));
ImageLoader2 imgLoader2 = new ImageLoader2(activity);
imgLoader2.DisplayImage(cw.getName(), R.drawable.restart, holder.downloadBt);
holder.progressBar.setProgress((int) ((file.length() * 100) / cw.getFileSize()));
holder.progressBar.setVisibility(View.VISIBLE);
} else {
holder.downloadBt.setVisibility(View.INVISIBLE);
}
}
}
}
holder.downloadBt.setOnClickListener(new OnItemClickedListener(view, position, creativeWork, holder.progressBar, holder.downloadBt, 0));
}
}
Easiest way to solve this is adding some extra fields to your model class whose list you are passing to your adapter.
like
boolean showDownloadButton; //default is true
int progress;// default is 0
So when user clicks on download button (or any other desired event) change the boolean value of showDownloadButton to false for model object at the given position and call adapter.notifyDatasetChanged() and manage the button visibility accordingly in your adapter.
and do make sure to add both visible and gone condition for the view aswell
if(modelList.get(position).getShowDownloadButton())
{
btnDownload.setVisibility(View.VISIBLE)
}
else
{
btnDownload.setVisibility(View.GONE)
}
Nitesh's answer is right apart from that there is another way to do if you don't want to create Model class
Use setId and getId with position to identify unique row and assign that id to progressbar for the solution
I have a listview with a checked textview and two textviews,however, my getView method keeps changing the listview items while scrolling, the values and checkbox states are both saved into sqlite database. I tried every possible solution and spent 4 hours trying to fix that.
Any help appreciated.The only solution that worked was setting convertview to null at beginning of getView() which lags the listview.
GOAL:to make listview display items properly without changing its positions randomly.
Final working code for anyone in need:
#Override
public View getView( final int position, View convertView, ViewGroup parent) {
viewHolder = null;
if(convertView == null){
convertView = inflater.inflate(R.layout.sin_item,null);
viewHolder = new HolderCo();
viewHolder.box = (CheckBox)convertView.findViewById(R.id.coco);
viewHolder.subject = (TextView)convertView.findViewById(R.id.subject_com);
viewHolder.date = (TextView)convertView.findViewById(R.id.date_co);
convertView.setTag(viewHolder);
}
else{
viewHolder = (HolderCo)convertView.getTag();
}
viewHolder.position = position;
viewHolder.box.setText(list.get(viewHolder.position).getWhats());
viewHolder.subject.setText(list.get(viewHolder.position).getSubject());
if(list.get(viewHolder.position).isSelected()) {
viewHolder.box.setOnCheckedChangeListener(null);
viewHolder.box.setChecked(true);
viewHolder.box.setPaintFlags(viewHolder.box.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
}else{
viewHolder.box.setOnCheckedChangeListener(null);
viewHolder.box.setChecked(false);
viewHolder.box.setPaintFlags(viewHolder.box.getPaintFlags() & (~Paint.STRIKE_THRU_TEXT_FLAG));
}
if(dator.equals("d"))
viewHolder.date.setText(list.get(viewHolder.position).getDay()+"/"+list.get(viewHolder.position).getMonth()+"/"+list.get(viewHolder.position).getYear());
if(dator.equals("m"))
viewHolder.date.setText(list.get(viewHolder.position).getMonth()+"/"+list.get(viewHolder.position).getDay()+"/"+list.get(viewHolder.position).getYear());
if(dator.equals("y"))
viewHolder.date.setText(list.get(viewHolder.position).getYear()+"/"+list.get(viewHolder.position).getMonth()+"/"+list.get(viewHolder.position).getDay());
viewHolder.box.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
#Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if(buttonView.isChecked()) {
list.get(position).setSelected(true);
db.updateState(list.get(position),true);
buttonView.setPaintFlags(buttonView.getPaintFlags()| Paint.STRIKE_THRU_TEXT_FLAG);
if(PreferenceManager.getDefaultSharedPreferences(ctx).getBoolean("add_mark_dialog",true))
buttonView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
dialoging(viewHolder.position);
}
});
}else{
buttonView.setOnClickListener(null);
list.get(position).setSelected(false);
db.updateState(list.get(position), false);
buttonView.setPaintFlags(buttonView.getPaintFlags()&(~Paint.STRIKE_THRU_TEXT_FLAG));
}
}
});
return convertView;
}
By doing this:
viewHolder.box.setTag(position);
viewHolder.date.setTag(position);
viewHolder.subject.setTag(position);
you set the tags to the views to the first position they were created with.
So when getView() is called with non-null convertView (previously recycled), the tags in its viewHolder still point to that position.
Move these setTag() calls outside if(), to set new position to recycled view.
BTW I would rather replace all this with
viewHolder.position = position; // outside if()
and using it everywhere you use (Integer)x.getTag()
UPDATE: Also you have to do this:
viewHolder.box.setOnCheckedChangeListener(null);
before this:
viewHolder.box.setChecked(...);
Because otherwise it can trigger previous listener which most likely you don't want.
You're updating the view conditionally with if conditions. You need to provide corresponding else blocks where you reset the view to their default values.
For example,
if(dator.equals("d"))
viewHolder.date.setText(...);
if(dator.equals("m"))
viewHolder.date.setText(...);
if(dator.equals("y"))
viewHolder.date.setText(...);
needs to be something like
if(dator.equals("d"))
viewHolder.date.setText(...);
else if(dator.equals("m"))
viewHolder.date.setText(...);
else if(dator.equals("y"))
viewHolder.date.setText(...);
else
viewHolder.date.setText("some default value");
Similarly reset defaults in viewHolder.box.setPaintFlags().
The reason is that ListView views are recycled. Recycled views are not in their pristine state like they were immediately after inflation. Instead they will be in a state they were before they were recycled, possibly containing data from the list row previously using that view.
I have an Android ListView that is frequently updated. When I click on an item in the list, some subset of the time the wrong object gets the click event. This occurs even when the update does not change the list.
Am I just coding this wrong, or is there a race condition in event handling through the view hierarchy?
The ListView uses an adapter that extends BaseAdapter, and its getView() method looks like the following. But the problem only appears when USE_LAYOUT=true.
#Override
public View getView(final int position, View convertView, ViewGroup parent) {
View view;
TextView textView;
ViewHolder viewHolder;
if (convertView == null) {
if (USE_LAYOUT) {
view = context.getLayoutInflater().inflate(R.layout.row, parent, false);
textView = (TextView) view.findViewById(R.id.rowTextView);
} else {
view = new TextView(context);
textView = (TextView) view;
}
viewHolder = new ViewHolder();
viewHolder.textView = textView;
view.setTag(viewHolder);
} else {
view = convertView;
viewHolder = (ViewHolder) view.getTag();
textView = viewHolder.textView;
}
final String item = getItem(position);
textView.setText(position + " " + item);
textView.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
Log.d("Adapter", position + " " + item);
}
});
return view;
}
So in terms of the code above, the activity displays a list view showing "1 item1", "2 item2", "3 item3", etc. If I click on, say item3, some percentage of the time, the log will show "21 item21".
A couple of other points about the code:
The item is just a String, the adapter maintains a List<String>, getItem() just calls items.get(index), and setItems() sets the list and calls notifyDataSetChanged().
The problem occurs if the list is not changed. I.e., the timer calls setItems(new ArrayList<String>(getItems()).
setItems() is called from the UI thread via runOnUiThread().
The timer goes off every 500ms or so, but the problem occurs with other frequencies.
The row layout is simply a TextView inside a LinearLayout.
I have observed that when views are recycled through getView, they are recycled in reverse order. In other words, whennotifyDataSetChanged() is called and the list has not scrolled, the first item is recycled with the last item, the second item is recycled with the second to last item, etc. Hence my suspicion of a race condition. Also, I observe it on 2.1 and 2.3, but not 4.0 (although I'm not sure it never occurs there).
The problem is that the code in the onClick() method is run at a different time than when getView() executes, so position is probably not what you expect it to be... Try this:
final String item = getItem(position);
textView.setTag(position);
textView.setText(position + " " + item);
textView.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
int position = (Integer) v.getTag();
Log.d("Adapter", position + " " + getItem(position));
}
});
In other words everything inside new OnClickListener() { ... } cannot see the local variables inside getView() (like position and item), but it can see the class variables and methods (like getItem()).
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 !
I am using a customised BaseAdapter to display items on a ListView. The items are just strings held in an ArrayList.
The list items have a delete button on them (big red X), and I'd like to remove the item from the ArrayList, and notify the ListView to update itself.
However, every implementation I've tried gets mysterious position numbers given to it, so for example clicking item 2's delete button will delete item 5's. It seems to be almost entirely random.
One thing to note is that elements may be repeated, but must be kept in the same order. For example, I can have "Irish" twice, as elements 3 and 7.
My code is below:
private static class ViewHolder {
TextView lang;
int position;
}
public View getView(final int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
convertView = mInflater.inflate(R.layout.language_link_row, null);
holder = new ViewHolder();
holder.lang = (TextView)convertView.findViewById(R.id.language_link_text);
holder.position = position;
final ImageView deleteButton = (ImageView)
convertView.findViewById(R.id.language_link_cross_delete);
deleteButton.setOnClickListener(this);
convertView.setTag(holder);
deleteButton.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
holder.lang.setText(mLanguages.get(position));
return convertView;
}
I later attempt to retrieve the deleted element's position by grabbing the tag, but it's always the wrong position in the list. There is no noticeable pattern to the position given here, it always seems random.
// The delete button's listener
public void onClick(View v) {
ViewHolder deleteHolder = (ViewHolder) v.getTag();
int pos = deleteHolder.position;
...
...
...
}
I would be quite happy to just delete the item from the ArrayList and have the ListView update itself, but the position I'm getting is incorrect so I can't do that.
Please note that I did, at first, have the deleteButton clickListener inside the getView method, and used 'position' to delete the value, but I had the same problem.
Any suggestions appreciated, this is really irritating me.
You have to set the position each time. Your implementation only sets the position on the creation of the view. However when the view is recycled (when convertView is not null), the position will not be set to the correct value.
public View getView(final int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
convertView = mInflater.inflate(R.layout.language_link_row, null);
holder = new ViewHolder();
holder.lang = (TextView)convertView.findViewById(R.id.language_link_text);
final ImageView deleteButton = (ImageView)
convertView.findViewById(R.id.language_link_cross_delete);
deleteButton.setOnClickListener(this);
convertView.setTag(holder);
deleteButton.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
holder.lang.setText(mLanguages.get(position));
holder.position = position;
return convertView;
}
you need to implement OnItemClickListener interface, and delete the item in the onItemClick method, one parameter of the method is the position.
My final solution was to use the accepted answer by Greg and the following:
Store the holders in a HashMap, with the item positions as the keys (this is initialised as empty in the constructor)
private HashMap mHolders;
Use this as the onClickListener method:
public void onClick(View v) {
ViewHolder deleteHolder = (ViewHolder) v.getTag();
int pos = deleteHolder.position;
mHolders.remove(pos);
ViewHolder currentHolder;
// Shift 'position' of remaining languages
// down since 'pos' was deleted
for(int i=pos+1; i<getCount(); i++){
currentHolder = mHolders.get(i);
currentHolder.position = i-1;
}
mLanguages.remove(pos);
notifyDataSetChanged();
}
[Please excuse the weird formatting. The code embedding isn't working properly]