Reputation: 3
I have a ListView
of a custom object with a custom adapter. Clicking on an individual item of list starts a new activity for result that updates that item and then returns back to the list with a green tick image on the right side of the ListView
item which is a TextView
and I just use row.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.tick_green, 0)
However when I scroll down, I see another row with the same drawable. Scrolling back and forth just sets this drawable to different rows randomly. Most answers say that it's because the views are being recycled but what is the way to change one single row irrespective of whether it is visible or not when the other activity returns a result?
code from Activity with the list view -
private ItemAdapter adapter;
private ArrayList<Item> labels;
private ArrayList<Item> updatedItems;
private TextView label;
@Override
protected void onCreate(Bundle savedInstanceState) {
// initializations
updatedItems = new ArrayList<>();
adapter = new ItemAdapter(getBaseContext(), R.layout.label_list_item, labels);
listView.setAdapter(adapter);
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
label = (TextView) listView.getChildAt(position - listView.getFirstVisiblePosition()).findViewById(R.id.text1);
Item item = adapter.getItem(position);
Intent myIntent = new Intent(ProviderPutawayActivity.this, PutawayScanLocationActivity.class);
myIntent.putExtra("ITEM", item);
ProviderPutawayActivity.this.startActivityForResult(myIntent, 1);
}
});
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if(requestCode == 1) {
Item item = (Item) data.getSerializableExtra("ITEM");
label.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.tick_green, 0);
}
v.findViewById(r.id.text1)
and (TextView) listView.getChildAt(position - listView.getFirstVisiblePosition()).findViewById(R.id.text1)
as suggested here both have the same problem.
The ItemAdapter class -
public class ItemAdapter extends ArrayAdapter<Item> {
private View v;
public ItemAdapter(Context context, int resource, List<Item> items) {
super(context, resource, items);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
v = convertView;
TextView label;
if(v == null) {
LayoutInflater vi;
vi = LayoutInflater.from(getContext());
v = vi.inflate(R.layout.label_list_item, null);
}
Item item = getItem(position);
if(item != null) {
label = (TextView)v.findViewById(R.id.text1);
if(label != null) label.setText(String.valueOf(item.getLabel()));
}
return v;
}
}
How do I make sure I only update the row that was clicked?
Upvotes: 0
Views: 905
Reputation: 310
As like above answer you should notifiyDataSetchange whenever you need to update item row in listview or recyclerview. So here you go
First you have to create setter and getter method in item model class to update your list view row. like below
public class Item {
private boolean mIsSelected;
public void setIsSelected(boolean isSelected) {
mIsSelected = isSelected;
}
public boolean isSelected() {
return mIsSelected;
}
}
Then in your ItemAdapter class inside getView() method do like this
@Override
public View getView(int position, View convertView, ViewGroup parent) {
v = convertView;
TextView label;
if(v == null) {
LayoutInflater vi;
vi = LayoutInflater.from(getContext());
v = vi.inflate(R.layout.label_list_item, null);
}
Item item = getItem(position);
if(item != null) {
label = (TextView)v.findViewById(R.id.text1);
label.setText(String.valueOf(item.getLabel()));
if (item.isSelected) {
label.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.tick_green, 0);
} else { // by default it will be false until you select an item here
label.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
}
}
return v;
}
Then in your activity inside setOnItemClickListener update your item model class like below
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
Item item = adapter.getItem(position);
item.setIsSelected(true);
Intent myIntent = new Intent(ProviderPutawayActivity.this, PutawayScanLocationActivity.class);
myIntent.putExtra("ITEM", item);
ProviderPutawayActivity.this.startActivityForResult(myIntent, 1);
}
});
Then inside startActivityResult update your list with notifyDataSetChanged like this
@Override
protected void onActivityResult(int requestCode, in resultCode, Intent data) {
if(requestCode == 1) {
adapter.notifyDataSetChanged();
}
Now you will get expected output
Upvotes: 0
Reputation: 5307
I was a "victim" to this view recycler mechanism too!
Your problem of
However when I scroll down, I see another row with the same drawable. Scrolling back and forth just sets this drawable to different rows randomly.
is basically because
listView with adapter tenses to recycle/reuse viewItems that are no longer visible.
Check this Answer if you want detailed-explanation on this.
For your case, just put a else for your if (when it comes to setting adapter's views):
if(label != null)
label.setText(String.valueOf(item.getLabel()));
else
label.setText("Default text");
Hope this helps~
Upvotes: 0
Reputation: 2353
First, you should store a variable that represent the green tick image in your Item (ex: boolean isSelected
)
Then, in onActivityResult
, check the condition, update the isSelected
and reload the listView
Upvotes: 0
Reputation: 38605
The issue is that you never unset the drawable when the view is recycled. This has nothing to do the ViewHolder pattern, it has to do with the way you are updating the item. Your getView()
method should have something like this:
int check = item.isChecked() ? R.drawable.tick_green : 0;
label.setCompountDrawablesWithIntrinsicBounds(0, 0, check, 0);
This is the correct way to bind the item data to the row view.
What you are doing now is saving some views of a clicked row into member fields of the Activity and updating them once when you get to onActivityResult()
. The problem is if I scroll away and scroll back, there is nothing identifying which item is actually checked and the adapter has no way to show the proper state.
What you should be doing is modifying the actual data source (the Item
in this case, or the structure containing the Item
s), and then call notifyDatasetChanged()
on the adapter. This will cause ListView
to rebuild its rows, so your proper binding logic will take care of showing the correct state.
As an aside, switching to RecyclerView
has a number of benefits (at the cost of a little more complexity). For one thing, with a RecyclerView.Adapter
you can notify that a single item changed, so it will only rebuild one row; this is impossible with ListAdapter
s. For another, it will force you to use viewholders, which is more performant anyway.
Upvotes: 1