Android Listview: Can't get reliable check behavior for CHOICE_MODE_SINGLE
Here's what I mean by weird:
- Elements usually take 2 clicks to change the background color correctly.
- When the list is modified, Android does not appear to be correctly invalidated for list views, as checked previously viewed views have a checked background for new items (which I confirmed with the debugger are not checked).
- Multiple items may appear to be a selection, but it is actually "correct" to uncheck the box when I click on them again, except for the fact that it
ListView
only sends the most recent item, as noted. - The first time I check an item, I cannot uncheck it by clicking on it.
- Things work more or less subtly if I change the selection mode to
ListView.CHOICE_MODE_MULTIPLE
, except, of course, that I don't need multiple selection.
I am using a custom adapter and custom layout. Oh, also, targeting 4.0.3. Here is the code for the list:
ListView categoryList = (ListView) findViewById(R.id.categoryList);
categoryList.setAdapter(categoryAdapter);
categoryList.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
categoryList.setItemsCanFocus(false);
categoryList.setOnItemClickListener(categoryAdapter);
Here's a click listener:
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
ListView listView = (ListView) parent;
RemoteListItem remoteListItem = (RemoteListItem) view.getTag();
if (remoteListItem.isEnabled()) {
remoteListItem.action(view);
}
view.invalidate(); /added out of sheer desperation
}
Here's the extended version of RelativeLayout I'm using:
package com.sastraxi.machineshop.ui;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.Log;
import android.widget.Checkable;
import android.widget.RelativeLayout;
/**
* RelativeLayout that implements the Checkable interface.
* Set this view tag as a Checkable, and this layout will delegate
* Checkable interface methods to the tag object.
*/
public class CheckableRelativeLayout extends RelativeLayout implements Checkable {
@Override
public boolean isClickable() {
return false;
}
public CheckableRelativeLayout(Context context) {
super(context);
}
public CheckableRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CheckableRelativeLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
/**
* Delegates to (Checkable) getTag().
*/
public boolean isChecked() {
try {
Checkable checkableTag = (Checkable) getTag();
return checkableTag.isChecked();
} catch (ClassCastException e) {
Log.w("CheckableRelativeLayout", "Tag is not an instance of Checkable; this object won't do anything useful.");
} catch (NullPointerException e) {
Log.w("CheckableRelativeLayout", "Tag is null; this object won't do anything useful.");
}
return false;
}
/**
* Delegates to (Checkable) getTag().
*/
public void setChecked(boolean checked) {
try {
Checkable checkableTag = (Checkable) getTag();
checkableTag.setChecked(checked);
invalidate();
} catch (ClassCastException e) {
Log.w("CheckableRelativeLayout", "Tag is not an instance of Checkable; this object won't do anything useful.");
} catch (NullPointerException e) {
Log.w("CheckableRelativeLayout", "Tag is null; this object won't do anything useful.");
}
}
/**
* Delegates to (Checkable) getTag().
*/
public void toggle() {
try {
Checkable checkableTag = (Checkable) getTag();
checkableTag.toggle();
invalidate();
} catch (ClassCastException e) {
Log.w("CheckableRelativeLayout", "Tag is not an instance of Checkable; this object won't do anything useful.");
} catch (NullPointerException e) {
Log.w("CheckableRelativeLayout", "Tag is null; this object won't do anything useful.");
}
}
private static final int[] CHECKED_STATE_SET = {
android.R.attr.state_checked
};
/**
* Reflect the delegate Checkable state in this View state set.
*/
@Override
protected int[] onCreateDrawableState(int extraSpace) {
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
if (isChecked()) {
mergeDrawableStates(drawableState, CHECKED_STATE_SET);
}
return drawableState;
}
}
Here's the list item that it proxies:
public abstract class RemoteListItem implements Checkable {
private final String name;
private final String extra;
private boolean enabled = true;
private boolean selected = false;
public boolean isChecked() {
return selected;
}
public void toggle() {
selected = !selected;
}
public void setChecked(boolean checked) {
selected = checked;
}
public RemoteListItem(String name, String extra) {
this.name = name;
this.extra = extra;
}
public String getExtra() {
return extra;
}
public String getName() {
return name;
}
public abstract void action(View viewInList);
public boolean isEnabled() {
return enabled;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
RemoteListAdapter.super.notifyDataSetChanged();
}
public boolean isSelectable() {
return true;
}
}
Here's a layout that expands for elements:
<?xml version="1.0" encoding="utf-8"?>
<com.sastraxi.machineshop.ui.CheckableRelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="?android:attr/listPreferredItemHeightSmall"
android:padding="12dp"
android:gravity="center_vertical"
android:background="@drawable/listitem_background">
<TextView
android:id="@+id/key"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:layout_alignParentLeft="true"
android:inputType="none"
/>
<TextView
android:id="@+id/value"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:gravity="right"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="@color/faded_text_colour"
android:layout_alignParentRight="true"
android:inputType="none"
/>
<ProgressBar
android:id="@+id/progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@android:style/Widget.ProgressBar.Small"
android:layout_marginTop="5dip"
android:layout_marginRight="2dip"
android:gravity="right"
android:visibility="gone"
android:layout_alignParentRight="true"/>
</com.sastraxi.machineshop.ui.CheckableRelativeLayout>
Also, @ drawable / listitem_background is a list of states in which the background color is drawn. I feel so lost that it doesn't work the way I expect them to. Looks like I missed somewhere view.invalidate()
, but I can't figure out where.
source to share
I ended up creating a new BaseAdapter
one that saved me a lot of grief with everyone ListView
in my project. Here's a link to a hint on github for anyone looking for a solution to similar problems.
Adapter base classes: SmartListAdapter
andSimplerSmartListAdapter
Class OpenFilesAdapter
implementation : implements SimplerSmartListAdapter
.
Base classes allow you to select things like each element is clickable, checkable or not; maximum number of checked items at any given time and provides category headings for free. It also allows you to move your click and UI update handler to the adapter.
The difference between the two base classes is that it SmartListAdapter
allows you to define a custom mapping from a "support list" to a list of items that actually appear useful, for example. keeping a persistent support list and showing / hiding items based on context. SimplerSmartListAdapter
continues SmartListAdapter
, defining this mapping as bijective.
source to share