Android開發(fā)筆記之:ListView刷新順序的問題詳解
更新時(shí)間:2013年05月21日 10:00:19 作者:
本篇文章是對(duì)Android中ListView刷新順序的問題進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下
背景
一個(gè)典型的ListView,每個(gè)Item顯示一個(gè)TextView,代表一個(gè)Task,需要實(shí)現(xiàn)二個(gè)編輯方式:一個(gè)是用CheckBox來標(biāo)識(shí)任務(wù)已經(jīng)完成,另一個(gè)要實(shí)現(xiàn)的編輯是刪除任務(wù)。對(duì)于完成的CheckBox就直接放在布局中就可,但對(duì)于刪除不想使用ContextMenu來實(shí)現(xiàn)編輯,對(duì)于像iOS中那樣的列表,它的刪除都是通過對(duì)列表中每個(gè)項(xiàng)目的手勢(shì)來觸發(fā)。這個(gè)實(shí)現(xiàn)起來并不難,可以用一個(gè)ViewSwitcher,Checkbox和刪除按扭是放入其中,讓ViewSwitcher來控制顯示哪一個(gè),正常情況下顯示Checkbox,隱藏刪除按扭,然后當(dāng)點(diǎn)擊Item時(shí)就顯示刪除按扭,隱藏Checkbox,這樣也更符合操作習(xí)慣,可以一個(gè)一個(gè)條目的刪除。
實(shí)現(xiàn)起來的方式如下:
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>




問題
但這有一個(gè)問題,就是如果其中某個(gè)條目是處于刪除狀態(tài),這時(shí)再添加一個(gè)新任務(wù),或者點(diǎn)擊另外條目的Checkbox時(shí),條目的狀態(tài)會(huì)錯(cuò)亂,本來處于正常狀態(tài)的條目會(huì)處于刪除狀態(tài)!
原因分析
最開始以為是數(shù)據(jù)問題,因?yàn)槭录奶幚矶际悄涿念?,可能?huì)指向不正確的外部數(shù)據(jù),通過打印調(diào)試發(fā)現(xiàn)所有數(shù)據(jù)都是對(duì)的。最后通過在bindView方法中加LOG信息發(fā)現(xiàn)了原因:每次ListView刷新bindView的順序并不相同,原來處在第3的子View,刷新后可能被放在第1位置。ViewSwitcher的顯示狀態(tài)是它自己維護(hù)的,也就是說沒有在View的外部保存其應(yīng)該顯示的狀態(tài),所以當(dāng)數(shù)據(jù)發(fā)生變化(Checkbox會(huì)引發(fā)數(shù)據(jù)變化)刷新列表時(shí),原來處于刪除狀態(tài)的子View(可能在第4位置)現(xiàn)在可能變成了第2位置,造成了第二個(gè)處于刪除狀態(tài),而第四個(gè)處于正常狀態(tài)。
解決方案
這個(gè)問題沒有完美解決方法,只能做一個(gè)Workaround的方法:那就是每次刷新bindView時(shí)把刪除狀態(tài)清掉,都換成默認(rèn)狀態(tài),這樣至少不會(huì)出現(xiàn)狀態(tài)混亂的狀況。但是,還是會(huì)看到刪除會(huì)閃一下。
要想完全解決這個(gè)問題就是避免在有其他方式導(dǎo)致數(shù)據(jù)變化時(shí)使用這種設(shè)計(jì),這種設(shè)計(jì)僅適用于:整個(gè)列表僅有刪除,沒有其他方式能導(dǎo)致列表會(huì)刷新時(shí),這時(shí)每當(dāng)刪除時(shí),直接把子View從ListView中移除,就不會(huì)出現(xiàn)混亂了。
同時(shí)也說明為什么我們每次bindView時(shí)要重新給子View添數(shù)據(jù),而不是僅當(dāng)創(chuàng)建子View添數(shù)據(jù)。因?yàn)槊看嗡⑿耣indView時(shí)順序并不一定是先前的順序,所以一定要重新添數(shù)據(jù)。而數(shù)據(jù)通常是與View分享開來,或是在數(shù)據(jù)庫(kù)中,或是其他形式,會(huì)以特定的順序存在,它不會(huì)因?yàn)閂iew的刷新而改變,所以為了不使用戶感覺狀態(tài)錯(cuò)亂,就必須要重新按照數(shù)據(jù)的順序來給View填充數(shù)據(jù)。
一個(gè)典型的ListView,每個(gè)Item顯示一個(gè)TextView,代表一個(gè)Task,需要實(shí)現(xiàn)二個(gè)編輯方式:一個(gè)是用CheckBox來標(biāo)識(shí)任務(wù)已經(jīng)完成,另一個(gè)要實(shí)現(xiàn)的編輯是刪除任務(wù)。對(duì)于完成的CheckBox就直接放在布局中就可,但對(duì)于刪除不想使用ContextMenu來實(shí)現(xiàn)編輯,對(duì)于像iOS中那樣的列表,它的刪除都是通過對(duì)列表中每個(gè)項(xiàng)目的手勢(shì)來觸發(fā)。這個(gè)實(shí)現(xiàn)起來并不難,可以用一個(gè)ViewSwitcher,Checkbox和刪除按扭是放入其中,讓ViewSwitcher來控制顯示哪一個(gè),正常情況下顯示Checkbox,隱藏刪除按扭,然后當(dāng)點(diǎn)擊Item時(shí)就顯示刪除按扭,隱藏Checkbox,這樣也更符合操作習(xí)慣,可以一個(gè)一個(gè)條目的刪除。
實(shí)現(xiàn)起來的方式如下:
復(fù)制代碼 代碼如下:
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();
}
}
}
復(fù)制代碼 代碼如下:
<?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>
復(fù)制代碼 代碼如下:
<?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>




問題
但這有一個(gè)問題,就是如果其中某個(gè)條目是處于刪除狀態(tài),這時(shí)再添加一個(gè)新任務(wù),或者點(diǎn)擊另外條目的Checkbox時(shí),條目的狀態(tài)會(huì)錯(cuò)亂,本來處于正常狀態(tài)的條目會(huì)處于刪除狀態(tài)!
原因分析
最開始以為是數(shù)據(jù)問題,因?yàn)槭录奶幚矶际悄涿念?,可能?huì)指向不正確的外部數(shù)據(jù),通過打印調(diào)試發(fā)現(xiàn)所有數(shù)據(jù)都是對(duì)的。最后通過在bindView方法中加LOG信息發(fā)現(xiàn)了原因:每次ListView刷新bindView的順序并不相同,原來處在第3的子View,刷新后可能被放在第1位置。ViewSwitcher的顯示狀態(tài)是它自己維護(hù)的,也就是說沒有在View的外部保存其應(yīng)該顯示的狀態(tài),所以當(dāng)數(shù)據(jù)發(fā)生變化(Checkbox會(huì)引發(fā)數(shù)據(jù)變化)刷新列表時(shí),原來處于刪除狀態(tài)的子View(可能在第4位置)現(xiàn)在可能變成了第2位置,造成了第二個(gè)處于刪除狀態(tài),而第四個(gè)處于正常狀態(tài)。
解決方案
這個(gè)問題沒有完美解決方法,只能做一個(gè)Workaround的方法:那就是每次刷新bindView時(shí)把刪除狀態(tài)清掉,都換成默認(rèn)狀態(tài),這樣至少不會(huì)出現(xiàn)狀態(tài)混亂的狀況。但是,還是會(huì)看到刪除會(huì)閃一下。
要想完全解決這個(gè)問題就是避免在有其他方式導(dǎo)致數(shù)據(jù)變化時(shí)使用這種設(shè)計(jì),這種設(shè)計(jì)僅適用于:整個(gè)列表僅有刪除,沒有其他方式能導(dǎo)致列表會(huì)刷新時(shí),這時(shí)每當(dāng)刪除時(shí),直接把子View從ListView中移除,就不會(huì)出現(xiàn)混亂了。
同時(shí)也說明為什么我們每次bindView時(shí)要重新給子View添數(shù)據(jù),而不是僅當(dāng)創(chuàng)建子View添數(shù)據(jù)。因?yàn)槊看嗡⑿耣indView時(shí)順序并不一定是先前的順序,所以一定要重新添數(shù)據(jù)。而數(shù)據(jù)通常是與View分享開來,或是在數(shù)據(jù)庫(kù)中,或是其他形式,會(huì)以特定的順序存在,它不會(huì)因?yàn)閂iew的刷新而改變,所以為了不使用戶感覺狀態(tài)錯(cuò)亂,就必須要重新按照數(shù)據(jù)的順序來給View填充數(shù)據(jù)。
您可能感興趣的文章:
- Android開發(fā)框架之自定義ZXing二維碼掃描界面并解決取景框拉伸問題
- Android開發(fā)中遇到端口號(hào)占用問題解決方法
- Fedora14下android開發(fā): eclipse與ibus確有沖突的問題分析
- Android開發(fā)筆記之:深入理解Cursor相關(guān)的性能問題
- 在android開發(fā)中盡量不要使用中文路徑的問題詳解
- android開發(fā)環(huán)境遇到adt無(wú)法啟動(dòng)的問題分析及解決方法
- Android程序啟動(dòng)時(shí)出現(xiàn)黑屏問題的解決方法
- Android中fragment嵌套fragment問題解決方法
- Android Studio的中文亂碼問題解決方法
- Android拍照保存在系統(tǒng)相冊(cè)不顯示的問題解決方法
- Android動(dòng)態(tài)添加View的問題解決方法
- Android 異步獲取網(wǎng)絡(luò)圖片并處理導(dǎo)致內(nèi)存溢出問題解決方法
- Android開發(fā)常見問題總結(jié)
相關(guān)文章
Android實(shí)現(xiàn)時(shí)間倒計(jì)時(shí)功能
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)時(shí)間倒計(jì)時(shí)功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-10-10android鬧鈴簡(jiǎn)單實(shí)現(xiàn)
本文給大家分享的是一段簡(jiǎn)單的實(shí)現(xiàn)Android系統(tǒng)的鬧鈴的代碼,非常實(shí)用,想做Android開發(fā)的小伙伴們可以參考下。2015-03-03Android studio 如何刪除項(xiàng)目 module
本篇文章主要介紹了Android studio 如何刪除項(xiàng)目module的相關(guān)知識(shí),具有很好的參考價(jià)值。下面跟著小編一起來看下吧2017-05-05Android學(xué)習(xí)教程之日歷庫(kù)使用(15)
這篇文章主要為大家詳細(xì)介紹了Android學(xué)習(xí)教程之日歷庫(kù)使用的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-11-11Android編程實(shí)現(xiàn)播放MP3功能示例
這篇文章主要介紹了Android編程實(shí)現(xiàn)播放MP3功能,結(jié)合實(shí)例形式分析了Android播放MP3功能的界面布局與功能實(shí)現(xiàn)相關(guān)操作技巧,需要的朋友可以參考下2017-02-02Android實(shí)現(xiàn)隱藏狀態(tài)欄和標(biāo)題欄
這篇文章主要介紹了Android實(shí)現(xiàn)隱藏狀態(tài)欄和標(biāo)題欄的相關(guān)資料,需要的朋友可以參考下2015-06-06Android app啟動(dòng)時(shí)黑屏或者白屏的原因及解決辦法
這篇文章主要介紹了Android app啟動(dòng)時(shí)黑屏或者白屏的原因及解決辦法的相關(guān)資料,需要的朋友可以參考下2016-09-09