Here’s what I mean by weird:
- Items usually take 2 presses to have their background colour change correctly.
- When the list is changed, Android doesn’t seem to be correctly invalidating list views as the re-used views that were checked have the checked background on the new items (that I have confirmed through the debugger are not checked).
- Multiple items can have the look of being selected, and in fact “correctly” uncheck when I click on them again, except for the fact that the
ListViewreports only the most-recent item as checked. - The first time I check an item, I can’t uncheck it by clicking it.
- Things work more-or-less fine if I change the choice mode to
ListView.CHOICE_MODE_MULTIPLEexcept, of course, that I don’t want multiple selection.
I’m using a custom adapter and a custom layout. Oh, also, targeting 4.0.3 for now. Here’s 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 the 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's tag as a Checkable, and this layout will delegate
* Checkable's 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's state in this View's 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 type it’s proxying to:
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 the layout that’s being expanded for the items:
<?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 state list, which is where the checked background colour comes from. I feel so lost as to why things aren’t working the way I expect them to. Seems like I’m missing a view.invalidate() somewhere, but I can’t fathom where.
I ended up creating a new
BaseAdapterthat’s saving me a lot of grief with everyListViewin my project. Here’s a link to the tip on github for anyone looking for a solution to similar problems.Adapter base classes:
SmartListAdapterandSimplerSmartListAdapterImplementing class:
OpenFilesAdapterimplementsSimplerSmartListAdapter.The base classes let you choose things like whether each item is clickable, checkable, or neither; the maximum number of checked items at any one time, and provides category headers for free. It also lets you move your click handler and UI updates into the adapter as well.
The difference between the two base classes is that
SmartListAdapterallows you to define a custom mapping from the “backing list” to the list of items that actually get displayed, useful for e.g. keeping the backing list constant and showing/hiding the items based on context.SimplerSmartListAdapterextendsSmartListAdapterby defining this mapping as bijective.