I've a question about ListView and listeners.
Suppose i have a listview in my app. Each item of it contains a checkbox.
And i've the following code:
public View getView(final int position, View convertView, ViewGroup parent) {
CheckBox checkbox = (CheckBox)v.findViewById(R.id.checkbox);
checkbox.setOnCheckedChangeListener(new OnCheckedChangeListener(){
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
// some code
}
});
}
As you can see I set setOnCheckedChangeListener every time when getView method is called.
So is it right way to set listener? Or should I set it once? Is it bad in terms of performance? Or it doesn't matter? And if I set the listener multiple times whether it will overwrite previous listener or will i have multiple listeners for this event?
Each listener that you set will overwrite the previous one (if any). Setting a listener, or anything else, will consume a super small amount of time, however here you're also creating a new anonymous class which will take longer.
For the maximum performance, I would make a few adjustments:
Use convertViews to cache the views
Use ViewHolder pattern to avoid multiple findViewById calls
Use a single listener for all checkboxes and set it only on the cached views (if convertView == null)
Save the position of the item that the CheckBox is located in as the CheckBox's tag
Here's an example:
private static class ViewHolder {
CheckBox checkBox;
}
private CompoundButton.OnCheckedChangeListener mCheckListener = new CompoundButton
.OnCheckedChangeListener() {
#Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
Log.e("TAG", "CheckBox position: " + buttonView.getTag());
}
};
#Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
/* Inflate the layout here */
holder = new ViewHolder();
holder.checkBox = (CheckBox) convertView.findViewById(R.id.checkbox);
/* Find other views in you layout here */
holder.checkBox.setOnCheckedChangeListener(mCheckListener);
// Set the holder as tag, so you can fetch it later for re-use
convertView.setTag(holder);
} else {
// Fetch the holder
holder = (ViewHolder) convertView.getTag();
}
// Set the checkbox position
holder.checkBox.setTag(position);
return convertView;
}
If you're not familiar with using convertViews and the ViewHolder pattern you should watch The World of ListView that explains it all.
Is it bad in terms of performance? Or it doesn't matter?
It does not matter. The setter just assign the object.
And if I set the listener multiple times whether it will overwrite
previous listener or will i have multiple listeners for this event?
you will have just one for each instance of the checkbox upon you called setOnCheckedChangeListener. The ListView is particular case due of its recycling mechanism
Related
I'm in the process of making a grocery list app and I wrote the code to where when I click an item, it'll mark it off.
This is my code for that section:
lv.setOnItemClickListener(new AdapterView.OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
TextView text = (TextView) view;
if (!text.getPaint().isStrikeThruText()) {
text.setPaintFlags(text.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
}else{
text.setPaintFlags(text.getPaintFlags() & (~Paint.STRIKE_THRU_TEXT_FLAG));
}
}
});
}
It works exactly like i want but when I add another item after an item is marked off, all of the items that are marked off, the marks disappear.
When I add an item, it's like it resets. it doesn't delete any of my items, just the strike_thru part of it. any help would be greatly appreciated! thanks
Your ListView's Adapter contains a method called getView, which is called when a list view item needs to be displayed in an actual View. The Views in your ListView will be discarded if you scroll too far off screen, or invalidate the whole ListView.
My guess is that adding an item is invalidating the ListView.
Your getView method should set the paint flags on the view that it returns. Assuming your list view is displaying a String[], you will also need a boolean[] to hold whether or not an item is complete. You would need to initialize this to all falses, add a completed[i] = !completed[i] at the beginning of your onItemClick. Then you can check competed[i] instead of isStrikeThruText in your if statement, later in that method. Finally, your getView can look like this
#NonNull
#Override
public View getView(int position, #Nullable View convertView, #NonNull ViewGroup parent) {
if (convertView == null) {
convertView = LayoutInflater.from(getContext()).inflate(android.R.layout.simple_list_item_1, parent, false);
}
TextView textView = (TextView) convertView;
textView.setText(items[position]);
if (completed[position]) {
textView.setPaintFlags(textView.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
} else {
textView.setPaintFlags(textView.getPaintFlags() & (~Paint.STRIKE_THRU_TEXT_FLAG));
}
return textView;
}
I want my Adapter to choose either one row_layout or another row_layout, depending on which Activity is sending the data. There are two Activities sending data.
At the moment I only know how to make the Adapter use one row_layout. I can't work out how to write the extra code to make it choose a different row_layout, depending on which Activity is sending the data. (There wont be any checkbox in the second row_layout).
Here is my Adapter:
public class ShopItemAdapter extends ArrayAdapter<ShopItem> {
public ShopItemAdapter(Context context, ArrayList<ShopItem> shopItem){
super(context, 0, shopItem);
}
#NonNull
#Override
public View getView(int position, View convertView, #NonNull ViewGroup parent) {
if(convertView == null){
convertView = LayoutInflater.from(getContext()).inflate(R.layout.row_layout, parent, false);
}
TextView tvItem = (TextView)convertView.findViewById(R.id.tvItemName);
TextView tvQty = (TextView)convertView.findViewById(R.id.tvQuantity);
CheckBox cbIsPurchased = (CheckBox)convertView.findViewById(R.id.cbIsPurchased);
ShopItem _shopItem = getItem(position);
tvItem.setText(_shopItem.getItemName());
tvQty.setText(String.valueOf(_shopItem.getQuantity()));
if(_shopItem.getIsPurchased() == 1){
cbIsPurchased.setChecked(true);
}
else{
cbIsPurchased.setChecked(false);
}
return convertView;
}
}
You can add some parameter to the ShopItemAdapter constructor to distinguish from which Activity is called, but if that implies to use many if conditionals in getView code then i think it's better to write several adapters, you will have a much clear code.
I faced with a weird problem. I have an android application and it contains activity that includes ListViewAdapter. Each element contains a checkbox.
And I've the following code -
public View getView(final int position, View convertView, ViewGroup parent) {
CheckBox check = (CheckBox) view.findViewById(R.id.myCheckBox);
if(check.getTag() == null) {
check.setTag(position);
check.setOnCheckedChangeListener(myCheckChangList);
}
check.setChecked(holders.get(position).isChecked);
}
private OnCheckedChangeListener myCheckChangList = new OnCheckedChangeListener() {
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
holders.get((Integer) buttonView.getTag()).isChecked = isChecked;
}
};
And when I scroll my listview down my OnCheckedChangeListener fires and reset my checkbox if it was set. Why does it happend? I excpeted OnCheckedChangeListener will be triggered when I click checkbox manually or call setChecked programmatically but it does when i scroll down too. Why?
Karn is correct, it is getting recycled, so when scrolling, it generates the view and:
check.setChecked(holders.get(position).isChecked);
makes it so your listener is called every time you scroll because you set your listener to listen when the check is changed. Change your setOnCheckedChangeListener to setOnClickListener.
When you scroll getView method is called due to view recycling behavior of adapter. and setChecked method will call which triggers OnCheckedChangeListener.
I am developing an app using a ListView with a simple custom adapter, each row containing a CheckBox object. However, due to ListView's recycling feature (that I don't plan on turning off), when any of the boxes are checked, others below or above in the ListView are also checked.
The following is my getView() in the adapter, along with the custom ViewHolder class:
public View getView(final int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
if (convertView == null) {
LayoutInflater mInflater = (LayoutInflater) c.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = mInflater.inflate(R.layout.populate_friends_row, null);
holder = new ViewHolder();
holder.nameCheckBox = (CheckBox) convertView.findViewById(R.id.isFriend);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
holder.nameCheckBox.setText(data.get(position).contactLabel);
holder.nameCheckBox.setChecked(checked.get(position));
holder.nameCheckBox.setTag(String.valueOf(position)); // to properly track the actual position
holder.nameCheckBox.setOnCheckedChangeListener(new OnCheckedChangeListener(){
#Override
public void onCheckedChanged(CompoundButton buttonView,
boolean isChecked) {
int pos = Integer.parseInt(buttonView.getTag().toString());
checked.set(pos, isChecked);
}
});
return convertView;
}
public static class ViewHolder {
public CheckBox nameCheckBox;
}
I am already holding the checked boxes in the ArrayList of booleans: checked.
Any help would be appreciated, thank you.
When you're calling holder.nameCheckBox.setChecked(checked.get(position)); to configure the view to be displayed for this view, the listener is called while the tag still has the position of the previous checkbox.
Try removing the listener (setting it to null) before calling setChecked
I've searched around for a solution to my problem but couldn't find one so here it is.
I have a arraylist of data that i display using a combination of textview and checkbox. This is done using custom adapter which basically inflates it row by row. Everything up to this point is fine until i want to retrieve the checked data.
What i read from other people is that their checked boxes becomes unchecked when scrolled due to the recycling of the view. For me the state of the checkboxes remain but i don't know how to retireve them without scrolling it. When i check a box it is "check" on the UI but actaully it is not recognized until i scroll the checked item out of view and back in again.
Currently i use a global arraylist to populate with list.get(position).getName() if the list.get(position).isSelected() == true. But you see this arraylist remains empty unless i scroll the item out of view and back again. the same applies when the boxes are unchecked, they remain in the arraylist until i scroll. What i have in mind is a submit button that takes all the checkbox's current state when it is clicked then construct the arraylist.
any idea how to create such thing?
public View getView(int position, View convertView, ViewGroup parent) {
View view = null;
if (convertView == null) {
LayoutInflater inflator = context.getLayoutInflater();
view = inflator.inflate(R.layout.qlist, null);
final ViewHolder viewHolder = new ViewHolder();
viewHolder.text = (TextView) view.findViewById(R.id.label);
viewHolder.checkbox = (CheckBox) view.findViewById(R.id.check);
viewHolder.checkbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
#Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
Model element = (Model) viewHolder.checkbox.getTag();
//
if(isChecked){
selctionCount++;
}
else if (!isChecked){
selctionCount--;
}
if(selctionCount > 2)
{
Toast.makeText(context, "error, you checked more than 2!", Toast.LENGTH_LONG).show();
element.setSelected(false);
}
else
{
element.setSelected(buttonView.isChecked());
}
System.out.println(selctionCount);
}
});
view.setTag(viewHolder);
viewHolder.checkbox.setTag(list.get(position));
}
else {
view = convertView;
((ViewHolder) view.getTag()).checkbox.setTag(list.get(position));
}
ViewHolder holder = (ViewHolder) view.getTag();
holder.text.setText(list.get(position).getName());
//This is the problem here! the selected arraylist (global) is empty even when i check the boxes,
//it only populates when i scroll the checked boxes out of view and back in! helphelp!
holder.checkbox.setChecked(list.get(position).isSelected());
if (list.get(position).isSelected() == true){
selected.add(list.get(position).getName());
}
else if (list.get(position).isSelected() == false){
selected.remove(list.get(position).getName());
}
return view;
}
}
I have already messed up with the similar problem. You can initially call a function in your constructor which initialize the states of your checkboxes as false and save all states in your arraylist. Whenever checkbox will be clicked just check the box and save its state in boolean arraylist.
Later you can retrieve your states from arraylist.
If you havn't understand then let me know I will try to send some lines of code to help you...
Here is a little code for your help:
I will create a boolean arraylist.
private ArrayList<Boolean> itemChecked = null;
Then I will set states of all checkboxes as false in my constructor:
for (int i=0; i < no_of_elements.size(); i++) {
itemChecked.add(i, false);
}
Set the actual checked state when checkbox clicked:
holder.cb.setOnCheckedChangeListener(new OnCheckedChangeListener() {
#Override
public void onCheckedChanged(CompoundButton buttonView,
boolean isChecked) {
itemChecked.set(position, isChecked);
}
});