I have a custom row_item for ListViews with an image, a pair of TextViews and a checkBox.
From what I have understood, as checkBox is a focusable element it steals the focus from the listView so the OnListItemClicked is never fired unless I set every row_item as not clickable and I block descendants focusability.
Then for managing the checkBoxes changes I set in my adapter getView method an "OnCheckedChangeListener" for my checkBoxes.
Is this a bad way of doing this? (Because I am creating new Listeners every time getView is called)
Is there an other way of doing the same?
I attach some code so it's easier to understand what I mean.
getView method: (inside arrayAdapter)
#Override
public View getView(int position, View convertView, ViewGroup parent) {
/** recycling views */
View row = convertView;
PregoHolder holder = null;
if(row == null)
{
LayoutInflater inflater = ((Activity)context).getLayoutInflater();
row = inflater.inflate(layoutResourceId, parent, false);
holder = new PregoHolder();
holder.imgIcon = (ImageView)row.findViewById(R.id.thumbnail);
holder.txtTitle = (TextView)row.findViewById(R.id.txtTitle);
holder.txtNews = (TextView)row.findViewById(R.id.txtNews);
holder.chBox = (CheckBox) row.findViewById(R.id.checkBox);
row.setTag(holder);
}
else
{
holder = (PregoHolder)row.getTag();
}
Prego Prego = data[position];
holder.txtTitle.setText(Prego.title);
holder.imgIcon.setImageResource(Prego.icon);
holder.txtNews.setText(Prego.news);
holder.chBox.setChecked(Prego.checked);
/** wiring up Listeners...
* (works fine but we are creating new listener each time)
* (Done like this because of the custom list view focusable issue)
*/
holder.chBox.setOnCheckedChangeListener(new OnCheckedChangeListener(){
#Override
public void onCheckedChanged(CompoundButton arg0, boolean checked) {
/** Getting the view position in the ListView */
ListView parent = (ListView)(arg0.getParent()).getParent();
int pos = parent.getPositionForView(arg0);
if (checked){
checkedPregons[pos] = true;
pregonsChecked++;
}
else if (!checked){
checkedPregons[pos] = false;
pregonsChecked--;
}
Toast.makeText(context, pregonsChecked+" is/are checked", Toast.LENGTH_SHORT).show();
}
});
row.setOnClickListener(new OnClickListener(){
#Override
public void onClick(View v) {
//mMode = ((Activity)context).startActionMode(new PregonsActionModes());
ListView parent = (ListView)v.getParent();
int pos = parent.getPositionForView(v);
Toast.makeText(context, "getView should show prego "+pos,Toast.LENGTH_SHORT).show();
}
});
return row;
}
Thanks in advance.
i suppose there could be a better way to go about it, but this isn't something i'd lose sleep over. Your method is wildly implemented by many android developers new and old and it's just a plain natural way to go about it. Moreover, keep in mind that the ListView only has as many rows as you can see on the screen at one time (and constantly is calling getView when you scroll), so you're talking about 8 - 12 made objects at any given time (obviously totally guessing), so your bottle-neck won't be here. In fact, depending on where the objects that I need to modify are, i don't even think about it.
But using OnCheckedChangeListener in a ListView is something i'd heavily warn against. like i said, getView is called for every row as it's being scrolled and made. This checked changed listener is being fired just as it's being made and notwithstanding there's absolutely no guarantees that getView is being called just once for each row, in fact i guarantee otherwise. Thus those code blocks are probably getting fired up the wazoo, even though you probably only intended for if the CheckBox box tick was handled manually. Put a relatively more intensive command like notifyDataSetChanged() and you'll see your ListView lock up from this phenomenom.
The solution is to simply use a checkbox onClickListener instead and check for isChecked() inside.
Related
I know about recycling rows in a listview. I have a listview with toggle buttons. I'm saving the states of the toggle buttons in a SparseBooleanArray as lot of posts suggest. My problem is the toggle button row gets on and off on scroll anyway. In my code I have saved the state of togglebutton and their respective position in the array and them I get their states from the same array.
Thanks.
#Override
public View getView(final int position, View convertView, ViewGroup parent) {
View row = convertView;
final ViewHolderBrandAvailability holder;
if(row == null){
dbHelper = new DBHelper(con);
database = dbHelper.getWritableDatabase();
LayoutInflater mInflater = (LayoutInflater) con.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
//viewHolderBrandAvailability = new ViewHolderBrandAvailability();
row = mInflater.inflate(R.layout.brand_availability_listview, parent, false);
holder = new ViewHolderBrandAvailability();
holder.brandNameTextView = (TextView) row.findViewById(R.id.brandAvailabilityNameText);
holder.radioGroup = (ToggleButton) row.findViewById(R.id.brandAvailable);
/*viewHolderBrandAvailability.unavailableRadioBtn = (RadioButton) convertView.findViewById(R.id.brandUnavailable);*/
row.setTag(holder);
}else {
holder = (ViewHolderBrandAvailability) row.getTag();
}
holder.radioGroup.setTag(position);
holder.radioGroup.setChecked(mCheckStates.get(position, false));
holder.radioGroup.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
#Override
public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) {
if(isChecked){
selectedBrandStatus.put(((BrandAvailability)list.get(position)).getBrand_id(), "Yes");
}else {
selectedBrandStatus.put(((BrandAvailability)list.get(position)).getBrand_id(), "No");
}
mCheckStates.put((Integer) compoundButton.getTag(), isChecked);
}
});
String brandTitle = ((BrandAvailability)list.get(position)).getBrand_title();
holder.brandNameTextView.setText(brandTitle);
//holder.radioGroup.setChecked();
return row;
}
static class ViewHolderBrandAvailability {
private TextView brandNameTextView;
private ToggleButton radioGroup;
//RadioButton unavailableRadioBtn;
//int position;
}
From the code you've posted, I see nothing that indicates a problem with the checked state of the ToggleButton changing on scroll. In fact, I created my own adapter to test out your code, and it worked perfectly fine for me.
Often, people run into a similar issue because they don't realize that the OnCheckedChangeListener they set in one call to getView() will stick around during the next call to getView(), and so the setChecked() call winds up triggering it. However, in your case, you're using compoundButton.getTag() to determine the index into the mCheckStates array, so there's no problem here.
That being said, you are using the getView() position argument for other operations in your listener, and this will cause the issue I described above.
selectedBrandStatus.put(((BrandAvailability)list.get(position)).getBrand_id(), "Yes");
Imagine getView() is called for the very first time, for position 0. Your code runs, and assigns the listener. You wind up checking the ToggleButton at position 0, so you call selectedBrandStatus.put(list.get(0).getBrand_id(), "Yes"). Now you scroll the list until this view is recycled: getView() is called again and this view is passed as convertView. Let's call this position 20. Your listener is still in place, so when the ToggleButton is unchecked by holder.radioGroup.setChecked(mCheckStates.get(20, false)), it is triggered again. Now it will overwrite the previous "Yes" with "No", since the listener was created using position = 0.
You can just change all of the indexes in your listener to be (Integer) compoundButton.getTag() and that will fix this problem.
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 a custom list view with custom array adapter. I am using list view for multi rows selection. But when I select the rows of visible children, and scroll up/down the list view, other child which appear they are also selected.
#Override
public View getView(final int position, View convertView, ViewGroup parent) {
LayoutInflater layoutInflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if(convertView == null)
convertView = layoutInflater.inflate(R.layout.addbill_row,parent,false);
initUI(convertView);
assignValues(position);
cbAddBillSelected.setOnCheckedChangeListener(new OnCheckedChangeListener() {
#Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
// TODO Auto-generated method stub
if(isChecked)
{
Data.arrlstSelectedUsers.get(position).isChecked = true;
}
else
{
Data.arrlstSelectedUsers.get(position).isChecked = false;
}
}
});
return convertView;
}
Attaching screen shots also. No idea why this is happening , I have another work around by saving selected item in shared preferences, but its a long solution.
Looking for a better solution.
why dont you use built-in ListView with Checkbox layout? android.R.layout.simple_list_item_multiple_choice will work for you.
It is something in the assign values function. you must also assign the check box in it. If this not enough as an answer plz modify your question and include the assign values function code to it. I will modify it.
This happen when convertView is not null. it gets you a used view. that's how this method work "getView". so you must modify every ui element everytime when convertView is not null.
Followed Vogella List View tutorial, and it worked like a charm. Using Tags.
Vogella List View
I have a weird problem! I'm trying to create a listview with checkboxes. In my other thread I was told that I should use an array that keeps track of the rows that are checked. I did that and it worked fine-ish but the logic is wrong and I run into another problem now.
public View getView(final int position, View convertView, ViewGroup parent) {
View v = convertView;
if (v == null) {
LayoutInflater vi = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
v = vi.inflate(R.layout.row, null);
}
CheckBox checkbox = (CheckBox)v.findViewById(R.id.checkbox);
checkbox.setChecked(checked[position]);
final LinearLayout rowLayout = (LinearLayout) v.findViewById(R.id.individualRow);
checkbox.setOnCheckedChangeListener(new OnCheckedChangeListener(){
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if(isChecked)
{
rowLayout.setBackgroundColor(Color.GRAY);
checked[position] = false;
}
else
{
rowLayout.setBackgroundColor(Color.DKGRAY);
checked[position] = true;
}
}
});
}
Having all the checkboxes unchecked initially it works fine it keeps the ones that i select checked even if I scroll down and back up again but the checked array is not properly set up. Basically the if test should be the other way arround!
if(isChecked)
{
rowLayout.setBackgroundColor(Color.GRAY);
checked[position] = true;
}
else
{
rowLayout.setBackgroundColor(Color.DKGRAY);
checked[position] = false;
}
The problem is with the scroll really because every time I scroll the onCheckedChanged method is called and since its recycling the rows it passes in the position of the new row that its not selected but since it has the same index as the one that was previously selected it changes its value. for example if I check the box with index 2 (set it to true) and then scroll down a new row becomes row with index 2, the method is called again and it unsets the checkbox(the field in the checked array).
I need it to "remember" all the boxes that are checked. In other words I want the checked array to be initialised properly. And also to remember which boxes are checked and not lose them everytime I scroll!
What am I doing wrong?
Can you please help me?
Thanks in advance -- Mike
This was tricky.
The problem is that you are calling setChecked, activating the old OnCheckedChangeListener.
The fix is quite simple: call setOnCheckedChangeListener before calling setChecked. This way you sever the link to the old listener from the recycled view.
Check this for Single selection of Checkbox in Listview
AdpList adapter=new AdpList(this,array,false);
listview.setAdapter(adapter);
When you select any Item then on item CLickListener do as Follows
listview.setonItemClickListener(new OnItemClickListener)
Here You will Get the Position of the Current Item
array.get(Position);
Get the Checkbox view and On clicking the Checkbox, create a method in Adapter, Like
adapter.setList(true);
adapter.notifyDataSetChanged();
And In Adapter check the Parameter that I have Passed in the Setlist method
then put condition:
if (isChecking==true)
{
checkbox1.setChecked(true);
}else{
checkBox1.setChecked(false);
}
I have an application in which I'd like one row at a time to have a certain color. This seems to work about 95% of the time, but sometimes instead of having just one row with this color, it will allow multiple rows to have the color. Specifically, a row is set to have the "special" color when it is tapped. In rare instances, the last row tapped will retain the color despite a call to setBackgroundColor attempting to make it otherwise.
private OnItemClickListener mDirectoryListener = new OnItemClickListener(){
public void onItemClick(AdapterView parent, View view, int pos, long id){
if (stdir.getStationCount() == pos) {
stdir.moreStations();
return;
}
if (playingView != null)
playingView.setBackgroundColor(Color.DKGRAY);
view.setBackgroundColor(Color.MAGENTA);
playingView = view;
playStation(pos);
}
};
I have confirmed with print statements that the code setting the row to gray is always called.
Can anyone imagine a reason why this code might intermittently fail? If there is a pattern or condition that causes it, I can't tell.
I thought it might have something to do with the activity lifecycle setting the "playingView" variable back to null, but I can't reliably reproduce the problem by switching activities or locking the phone.
private class DirectoryAdapter extends ArrayAdapter {
private ArrayList<Station> items;
public DirectoryAdapter(Context c, int resLayoutId, ArrayList<Station> stations){
super(c, resLayoutId, stations);
this.items = stations;
}
public int getCount(){
return items.size() + 1;
}
public View getView(int position, View convertView, ViewGroup parent){
View v = convertView;
LayoutInflater vi = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (position == this.items.size()) {
v = vi.inflate(R.layout.morerow, null);
return v;
}
Station station = this.items.get(position);
v = vi.inflate(R.layout.songrow, null);
if (station.playing)
v.setBackgroundColor(Color.MAGENTA);
else if (station.visited)
v.setBackgroundColor(Color.DKGRAY);
else
v.setBackgroundColor(Color.BLACK);
TextView title = (TextView)v.findViewById(R.id.title);
title.setText(station.name);
return v;
}
};
ListViews don't create instances of contained views for every item in the list, but only for ones that are actually visible on the screen. For performance reasons, they try and maintain as few views as possible, and recycle them. That's what the convertView parameter is.
When a view scrolls off the screen, it may be recycled or destroyed. You can't hold a reference to an old view and assume that it will refer to the item you expect it to in the future. You should save the ID of the list item you need and look that up instead.
Moreover, there are a couple of other issues with your implementation (from a best practices standpoint). You seem to be ignoring the convertView parameter and creating a new view from scratch each time. That can cause your application to bog down a bit while scrolling if you have a long list. Secondly, instead of adding the "more" element the way you do, you're better of setting it with setFooterView().
There's an excellent talk on the ListView from Google I/O 2010 that covers these and other issues. It's an hour long, but definitely worth watching in its entirety.