ArrayAdapter custom view state gets repeated when scrolling
I have a ListView that gets populated with custom view items. The custom view consists of an icon, a label and a checkbox. When I first create the list, everything displays as it should. If I scroll down the list, the icons and labels continue to be correct further down the list but the checkbox states start to get mixed up, displaying other items as checked besides the ones I chose.
Example: My list starts with no checkboxes set as checked for any items. I see 10 items on screen. I toggle the checkbox on item 10. It updates appropriately. If I scroll down the list, I find that the checkbox for item 20, item 30, etc. start with the checkbox already toggled even though they were never visible to interact with. If I scroll back and forth repeatedly, more and more items in a non-identifiable pattern appear checked.
List setup in my activity:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.default_list);
profile = (Profile) i.getParcelableExtra("profile");
ArrayList<Application> apps = new ApplicationListRetriever(this).getApplications(true);
adapter = new ApplicationsAdapter(this, R.layout.application_list_item, apps, getPackageManager(), profile);
setListAdapter(adapter);
}
ApplicationsAdapter:
public class ApplicationsAdapter extends ArrayAdapter<Application> {
private ArrayList<Application> items;
private PackageManager pm;
private Profile profile;
private ArrayList<ApplicationListener> listeners = new ArrayList<ApplicationListener>();
public ApplicationsAdapter(Context context, int textViewResourceId,
ArrayList<Application> objects, PackageManager pm, Profile profile) {
super(context, textViewResourceId, objects);
this.pm = pm;
items = objects;
this.profile = profile;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View v = convertView;
if (v == null) {
LayoutInflater li = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
v = li.inflate(R.layout.application_list_item, null);
}
final Application info = items.get(position);
if (info != null) {
TextView text = (TextView) v.findViewById(R.id.label);
CheckBox check = (CheckBox) v.findViewById(R.id.check);
ImageView img = (ImageView) v.findViewById(R.id.application_icon);
//see if the app already is associated and mark checkbox accordingly
for (Application app : profile.getApps()) {
if (info.getPackageName().equals(app.getPackageName())) {
check.setChecked(true);
break;
}
}
check.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
for (ApplicationListener listener : listeners) {
listener.applicationReceived(info, isChecked);
}
}
});
try {
text.setText(info.getName());
}
catch (Exception ex) {
Log.e("ApplicationsAdapter", "Label could not be set on adapter item", ex);
}
if (img != null) {
try {
img.setImageDrawable(pm.getApplicationIcon(info.getPackageName()));
} catch (NameNotFoundException e) {
e.printStackTrace();
}
}
}
return v;
}
}
List item layout:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent" android:layout_height="wrap_content">
<ImageView android:id="@+id/application_icon"
android:layout_centerVertical="true" android:layout_alignParentLeft="true"
android:layout_width="36dp" android:layout_height="36dp" android:paddingRight="3dp" android:adjustViewBounds="true" />
<TextView android:layout_width="wrap_content"
android:layout_centerVertical="true" andro开发者_如何学JAVAid:layout_toRightOf="@+id/application_icon"
android:layout_height="wrap_content" android:id="@+id/label"
android:text="Application Name" />
<CheckBox android:id="@+id/check"
android:layout_centerVertical="true" android:layout_alignParentRight="true"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="right" />
</RelativeLayout>
Also worth noting is if I set a breakpoint on the line where I call check.setChecked(true);
it only hits that point if the original item I checked is on screen, never for any of the other items that display as checked.
Any ideas why the later items would display as checked or what I can try to fix it?
It is caused by View recycling. You should completely refresh the state of your View when getView is called. Although you are refreshing almost everything, you've forgotten one little thing.
Suppose the list shows 5 items on screen and the second item is checked. The user then scrolls down a further 5 items - however due to view recycling they're really just the same 5 views as before the user scrolled, so the one of the items on-screen will still be checked even though it shouldn't be, because in your code above if no package is matched you are not setting the checkbox to unchecked (so it will stay checked), you are assuming it is already unchecked (which due to View recycling it may not be).
The fix is simple: simply set the checkbox to unchecked before your logic to check if it need be checked:
// Do not assume the checkbox is unchecked to begin with, it might not be
// if this view was recycled, so force it to be unchecked by default and only
// check it if needed.
check.setChecked(false);
for (Application app : profile.getApps()) {
if (info.getPackageName().equals(app.getPackageName())) {
check.setChecked(true);
break;
}
}
精彩评论