Android开发笔记之:ListView刷新顺序的问题详解
程序员文章站
2023-11-23 22:35:16
背景一个典型的listview,每个item显示一个textview,代表一个task,需要实现二个编辑方式:一个是用checkbox来标识任务已经完成,另一个要实现的编辑...
背景
一个典型的listview,每个item显示一个textview,代表一个task,需要实现二个编辑方式:一个是用checkbox来标识任务已经完成,另一个要实现的编辑是删除任务。对于完成的checkbox就直接放在布局中就可,但对于删除不想使用contextmenu来实现编辑,对于像ios中那样的列表,它的删除都是通过对列表中每个项目的手势来触发。这个实现起来并不难,可以用一个viewswitcher,checkbox和删除按扭是放入其中,让viewswitcher来控制显示哪一个,正常情况下显示checkbox,隐藏删除按扭,然后当点击item时就显示删除按扭,隐藏checkbox,这样也更符合操作习惯,可以一个一个条目的删除。
实现起来的方式如下:
public class listorderactivity extends activity {
private listview mtasklist;
private edittext maddtaskeditor;
private layoutinflater mfactory;
@override
public void oncreate(bundle savedinstancestate) {
super.oncreate(savedinstancestate);
setcontentview(r.layout.list_activity);
mfactory = layoutinflater.from(getapplication());
mtasklist = (listview) findviewbyid(r.id.task_list);
final view headerview = mfactory.inflate(r.layout.header_view, null);
mtasklist.addheaderview(headerview);
maddtaskeditor = (edittext) headerview.findviewbyid(r.id.task_editor);
maddtaskeditor.setonkeylistener(new onkeylistener() {
@override
public boolean onkey(view view, int keycode, keyevent event) {
if (keycode == keyevent.keycode_dpad_center || keycode == keyevent.keycode_enter) {
// finish editing
final inputmethodmanager inputmanager = (inputmethodmanager) getsystemservice(context.input_method_service);
inputmanager.hidesoftinputfromwindow(getcurrentfocus().getwindowtoken(), 0);
final string text = maddtaskeditor.gettext().tostring();
if (!textutils.isempty(text)) {
final contentvalues values = new contentvalues(1);
values.put(taskcolumns.task, text);
values.put(taskcolumns.type, task.type_today);
getcontentresolver().insert(task.content_uri, values);
}
maddtaskeditor.settext("");
}
return false;
}
});
final cursor cursor = getcontentresolver().query(task.content_uri, task.projection, taskcolumns.type + " = " + task.type_today, null, null);
final taskadapter adapter = new taskadapter(getapplication(), cursor);
mtasklist.setadapter(adapter);
}
private class taskadapter extends cursoradapter {
private cursor mcursor;
public taskadapter(context context, cursor c) {
super(context, c);
mcursor = c;
}
@override
public void bindview(view view, context context, cursor cursor) {
if (view == null) {
view = mfactory.inflate(r.layout.today_task_item, null);
}
final viewswitcher switcher = (viewswitcher) view.findviewbyid(r.id.action_switcher);
// if (switcher.getdisplayedchild() == 1) {
// switcher.clearanimation();
// switcher.showprevious();
// switcher.clearanimation();
// }
final checkbox toggle = (checkbox) view.findviewbyid(r.id.action_toggle_done);
final short done = cursor.getshort(projectionindex.done);
final int id = cursor.getint(projectionindex.id);
toggle.setoncheckedchangelistener(null);
toggle.setchecked(done != 0);
toggle.setoncheckedchangelistener(new oncheckedchangelistener() {
@override
public void oncheckedchanged(compoundbutton view, boolean checked) {
final uri uri = contenturis.withappendedid(task.content_uri, id);
final contentvalues values = new contentvalues(1);
values.put(taskcolumns.done, checked ? 1 : 0);
getcontentresolver().update(uri, values, null, null);
}
});
view.setonclicklistener(new onclicklistener() {
@override
public void onclick(view v) {
switcher.shownext();
if (switcher.getdisplayedchild() == 0) {
switcher.getinanimation().setanimationlistener(null);
return;
}
final imageview delete = (imageview) v.findviewbyid(r.id.action_delete_task);
delete.setonclicklistener(new onclicklistener() {
@override
public void onclick(view v) {
switcher.getinanimation().setanimationlistener(new animationlistener() {
@override
public void onanimationend(animation animation) {
switcher.getinanimation().setanimationlistener(null);
final uri uri = contenturis.withappendedid(task.content_uri, id);
getcontentresolver().delete(uri, null, null);
}
@override
public void onanimationrepeat(animation animation) {
}
@override
public void onanimationstart(animation animation) {
}
});
switcher.showprevious();
}
});
}
});
textview task = (textview) view.findviewbyid(r.id.task);
final string taskcontent = cursor.getstring(projectionindex.task);
if (done != 0) {
final spannable style = new spannablestring(taskcontent);
style.setspan(new strikethroughspan(), 0, taskcontent.length(), spannable.span_inclusive_inclusive);
style.setspan(new stylespan(typeface.italic) , 0, taskcontent.length(), spannable.span_inclusive_inclusive);
task.settext(style);
task.settextappearance(getapplication(), r.style.done_task_item_text);
} else {
task.settext(taskcontent);
task.settextappearance(getapplication(), r.style.task_item_text);
}
}
@override
public view newview(context context, cursor cursor, viewgroup parent) {
view view = mfactory.inflate(r.layout.today_task_item, null);
return view;
}
@override
public void oncontentchanged() {
mcursor.requery();
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<linearlayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical"
android:background="#f0f0f0"
android:paddingbottom="5dip"
android:paddingleft="12dip"
android:paddingright="12dip"
android:paddingtop="5dip" >
<listview
android:id="@+id/task_list"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:divider="@color/divider"
android:dividerheight="0.6dip" />
</linearlayout>
<?xml version="1.0" encoding="utf-8"?>
<linearlayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center">
<viewswitcher android:id="@+id/action_switcher"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:inanimation="@anim/action_switcher_in"
android:outanimation="@anim/action_switcher_out">
<checkbox android:id="@+id/action_toggle_done"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_gravity="center" />
<imageview android:id="@+id/action_delete_task"
android:src="@drawable/ic_delete"
android:layout_width="48dip"
android:layout_height="48dip"
android:contentdescription="@string/delete_description"
android:gravity="center"
android:layout_gravity="center"
android:scaletype="center" />
</viewswitcher>
<textview android:id="@+id/task"
style="@style/task_item_text" />
</linearlayout>
问题
但这有一个问题,就是如果其中某个条目是处于删除状态,这时再添加一个新任务,或者点击另外条目的checkbox时,条目的状态会错乱,本来处于正常状态的条目会处于删除状态!
原因分析
最开始以为是数据问题,因为事件的处理都是匿名的类,可能会指向不正确的外部数据,通过打印调试发现所有数据都是对的。最后通过在bindview方法中加log信息发现了原因:每次listview刷新bindview的顺序并不相同,原来处在第3的子view,刷新后可能被放在第1位置。viewswitcher的显示状态是它自己维护的,也就是说没有在view的外部保存其应该显示的状态,所以当数据发生变化(checkbox会引发数据变化)刷新列表时,原来处于删除状态的子view(可能在第4位置)现在可能变成了第2位置,造成了第二个处于删除状态,而第四个处于正常状态。
解决方案
这个问题没有完美解决方法,只能做一个workaround的方法:那就是每次刷新bindview时把删除状态清掉,都换成默认状态,这样至少不会出现状态混乱的状况。但是,还是会看到删除会闪一下。
要想完全解决这个问题就是避免在有其他方式导致数据变化时使用这种设计,这种设计仅适用于:整个列表仅有删除,没有其他方式能导致列表会刷新时,这时每当删除时,直接把子view从listview中移除,就不会出现混乱了。
同时也说明为什么我们每次bindview时要重新给子view添数据,而不是仅当创建子view添数据。因为每次刷新bindview时顺序并不一定是先前的顺序,所以一定要重新添数据。而数据通常是与view分享开来,或是在数据库中,或是其他形式,会以特定的顺序存在,它不会因为view的刷新而改变,所以为了不使用户感觉状态错乱,就必须要重新按照数据的顺序来给view填充数据。
一个典型的listview,每个item显示一个textview,代表一个task,需要实现二个编辑方式:一个是用checkbox来标识任务已经完成,另一个要实现的编辑是删除任务。对于完成的checkbox就直接放在布局中就可,但对于删除不想使用contextmenu来实现编辑,对于像ios中那样的列表,它的删除都是通过对列表中每个项目的手势来触发。这个实现起来并不难,可以用一个viewswitcher,checkbox和删除按扭是放入其中,让viewswitcher来控制显示哪一个,正常情况下显示checkbox,隐藏删除按扭,然后当点击item时就显示删除按扭,隐藏checkbox,这样也更符合操作习惯,可以一个一个条目的删除。
实现起来的方式如下:
复制代码 代码如下:
public class listorderactivity extends activity {
private listview mtasklist;
private edittext maddtaskeditor;
private layoutinflater mfactory;
@override
public void oncreate(bundle savedinstancestate) {
super.oncreate(savedinstancestate);
setcontentview(r.layout.list_activity);
mfactory = layoutinflater.from(getapplication());
mtasklist = (listview) findviewbyid(r.id.task_list);
final view headerview = mfactory.inflate(r.layout.header_view, null);
mtasklist.addheaderview(headerview);
maddtaskeditor = (edittext) headerview.findviewbyid(r.id.task_editor);
maddtaskeditor.setonkeylistener(new onkeylistener() {
@override
public boolean onkey(view view, int keycode, keyevent event) {
if (keycode == keyevent.keycode_dpad_center || keycode == keyevent.keycode_enter) {
// finish editing
final inputmethodmanager inputmanager = (inputmethodmanager) getsystemservice(context.input_method_service);
inputmanager.hidesoftinputfromwindow(getcurrentfocus().getwindowtoken(), 0);
final string text = maddtaskeditor.gettext().tostring();
if (!textutils.isempty(text)) {
final contentvalues values = new contentvalues(1);
values.put(taskcolumns.task, text);
values.put(taskcolumns.type, task.type_today);
getcontentresolver().insert(task.content_uri, values);
}
maddtaskeditor.settext("");
}
return false;
}
});
final cursor cursor = getcontentresolver().query(task.content_uri, task.projection, taskcolumns.type + " = " + task.type_today, null, null);
final taskadapter adapter = new taskadapter(getapplication(), cursor);
mtasklist.setadapter(adapter);
}
private class taskadapter extends cursoradapter {
private cursor mcursor;
public taskadapter(context context, cursor c) {
super(context, c);
mcursor = c;
}
@override
public void bindview(view view, context context, cursor cursor) {
if (view == null) {
view = mfactory.inflate(r.layout.today_task_item, null);
}
final viewswitcher switcher = (viewswitcher) view.findviewbyid(r.id.action_switcher);
// if (switcher.getdisplayedchild() == 1) {
// switcher.clearanimation();
// switcher.showprevious();
// switcher.clearanimation();
// }
final checkbox toggle = (checkbox) view.findviewbyid(r.id.action_toggle_done);
final short done = cursor.getshort(projectionindex.done);
final int id = cursor.getint(projectionindex.id);
toggle.setoncheckedchangelistener(null);
toggle.setchecked(done != 0);
toggle.setoncheckedchangelistener(new oncheckedchangelistener() {
@override
public void oncheckedchanged(compoundbutton view, boolean checked) {
final uri uri = contenturis.withappendedid(task.content_uri, id);
final contentvalues values = new contentvalues(1);
values.put(taskcolumns.done, checked ? 1 : 0);
getcontentresolver().update(uri, values, null, null);
}
});
view.setonclicklistener(new onclicklistener() {
@override
public void onclick(view v) {
switcher.shownext();
if (switcher.getdisplayedchild() == 0) {
switcher.getinanimation().setanimationlistener(null);
return;
}
final imageview delete = (imageview) v.findviewbyid(r.id.action_delete_task);
delete.setonclicklistener(new onclicklistener() {
@override
public void onclick(view v) {
switcher.getinanimation().setanimationlistener(new animationlistener() {
@override
public void onanimationend(animation animation) {
switcher.getinanimation().setanimationlistener(null);
final uri uri = contenturis.withappendedid(task.content_uri, id);
getcontentresolver().delete(uri, null, null);
}
@override
public void onanimationrepeat(animation animation) {
}
@override
public void onanimationstart(animation animation) {
}
});
switcher.showprevious();
}
});
}
});
textview task = (textview) view.findviewbyid(r.id.task);
final string taskcontent = cursor.getstring(projectionindex.task);
if (done != 0) {
final spannable style = new spannablestring(taskcontent);
style.setspan(new strikethroughspan(), 0, taskcontent.length(), spannable.span_inclusive_inclusive);
style.setspan(new stylespan(typeface.italic) , 0, taskcontent.length(), spannable.span_inclusive_inclusive);
task.settext(style);
task.settextappearance(getapplication(), r.style.done_task_item_text);
} else {
task.settext(taskcontent);
task.settextappearance(getapplication(), r.style.task_item_text);
}
}
@override
public view newview(context context, cursor cursor, viewgroup parent) {
view view = mfactory.inflate(r.layout.today_task_item, null);
return view;
}
@override
public void oncontentchanged() {
mcursor.requery();
}
}
}
复制代码 代码如下:
<?xml version="1.0" encoding="utf-8"?>
<linearlayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical"
android:background="#f0f0f0"
android:paddingbottom="5dip"
android:paddingleft="12dip"
android:paddingright="12dip"
android:paddingtop="5dip" >
<listview
android:id="@+id/task_list"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:divider="@color/divider"
android:dividerheight="0.6dip" />
</linearlayout>
复制代码 代码如下:
<?xml version="1.0" encoding="utf-8"?>
<linearlayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center">
<viewswitcher android:id="@+id/action_switcher"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:inanimation="@anim/action_switcher_in"
android:outanimation="@anim/action_switcher_out">
<checkbox android:id="@+id/action_toggle_done"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_gravity="center" />
<imageview android:id="@+id/action_delete_task"
android:src="@drawable/ic_delete"
android:layout_width="48dip"
android:layout_height="48dip"
android:contentdescription="@string/delete_description"
android:gravity="center"
android:layout_gravity="center"
android:scaletype="center" />
</viewswitcher>
<textview android:id="@+id/task"
style="@style/task_item_text" />
</linearlayout>
问题
但这有一个问题,就是如果其中某个条目是处于删除状态,这时再添加一个新任务,或者点击另外条目的checkbox时,条目的状态会错乱,本来处于正常状态的条目会处于删除状态!
原因分析
最开始以为是数据问题,因为事件的处理都是匿名的类,可能会指向不正确的外部数据,通过打印调试发现所有数据都是对的。最后通过在bindview方法中加log信息发现了原因:每次listview刷新bindview的顺序并不相同,原来处在第3的子view,刷新后可能被放在第1位置。viewswitcher的显示状态是它自己维护的,也就是说没有在view的外部保存其应该显示的状态,所以当数据发生变化(checkbox会引发数据变化)刷新列表时,原来处于删除状态的子view(可能在第4位置)现在可能变成了第2位置,造成了第二个处于删除状态,而第四个处于正常状态。
解决方案
这个问题没有完美解决方法,只能做一个workaround的方法:那就是每次刷新bindview时把删除状态清掉,都换成默认状态,这样至少不会出现状态混乱的状况。但是,还是会看到删除会闪一下。
要想完全解决这个问题就是避免在有其他方式导致数据变化时使用这种设计,这种设计仅适用于:整个列表仅有删除,没有其他方式能导致列表会刷新时,这时每当删除时,直接把子view从listview中移除,就不会出现混乱了。
同时也说明为什么我们每次bindview时要重新给子view添数据,而不是仅当创建子view添数据。因为每次刷新bindview时顺序并不一定是先前的顺序,所以一定要重新添数据。而数据通常是与view分享开来,或是在数据库中,或是其他形式,会以特定的顺序存在,它不会因为view的刷新而改变,所以为了不使用户感觉状态错乱,就必须要重新按照数据的顺序来给view填充数据。