ListView实现单选和多选功能,系统自带的貌似不能满足我们需求,一般情况下自定义或者自己控制adapter的逻辑切换显示view、drawable,以前我们最常用的radioButton、checkbox等控件,item赋值一个属性isChecked,提供一个全局的集合记录position的isChecked属性,貌似这样能实现我们需求,也就没多想什么,做出来就好,然而当你重复着编码,编写这些你曾经写过很多次的代码,你不会觉得烦么!!是的我得承认我受不了了,查阅相关资料找到了CustomChoiceList simple.带给了我正确的姿势。
做单选和多选功能这块我们必须的了解以下几点姿势
choiceMode
SparseBooleanArray
Checkable
AbsListView
AbsListView提供了四种模式,默认正常模式
/**
* 正常模式,没有单选多选
*/
public static final int CHOICE_MODE_NONE = 0;
/**
* 单选模式
*/
public static final int CHOICE_MODE_SINGLE = 1;
/**
* 多选模式(不影响其他点击事件)
*/
public static final int CHOICE_MODE_MULTIPLE = 2;
/**
* 多选模式(影响其他点击事件,会屏蔽onItemClick类似监听)
*/
public static final int CHOICE_MODE_MULTIPLE_MODAL = 3;
在单选和多选模式下,对于状态 的保存其实AbsListView已经帮我们做了实现,状态保存到SparseBooleanArray,AbsListView 内部常量定义mCheckStates,SparseBooleanArray实现Cloneable,实现原型拷贝keys values数组,内部主要操作position key 和 isChecked value,提供一些增删改查方法。
AbsListView还定义了一些公开方法方便子View调用
// 获取选择item的总数
getCheckedItemCount()
// 指定位置Item是否选中
isItemChecked(int position)
// 单选模式先获取当前选中位置
getCheckedItemPosition()
//多选模式小获取存储的所有选中位置
SparseBooleanArray getCheckedItemPositions()
// 清楚所有选中项
clearChoices()
// 设置指定位置被选中
setItemChecked(int position, boolean value)
// 设置当前模式
setChoiceMode(int choiceMode)
// 绑定多选变化监听
setMultiChoiceModeListener(MultiChoiceModeListener listener)
// 使用默认的Item背景Selector
useDefaultSelector
// 获取被选中的View视图
getSelectedView
// 根据checked状态强制刷新视图
drawableStateChanged
// ...............略...............
下面再来说一下Checkable接口
public interface Checkable {
/**
* 改变View的选中状态
*
* @param checked The new checked state
*/
void setChecked(boolean checked);
/**
* 返回当前视图是否被选中
*/
boolean isChecked();
/**
* 如果当前视图是选中的则改变为未选中,反之同理
*/
void toggle();
}
我们自定义的容器实现Checkable接口,容器的内部子View可以根据checked状态显示不同的ui,至于怎么自定义这种容器呢,我们通过下面自定义LinearLayout来帮助我们理解。(该控件来自官方simple,一生二生三生万物,跟多ViewGroup子类实现参考自行实现即可)
public class CheckableLinearLayout extends LinearLayout implements Checkable {
private static final int[] CHECKED_STATE_SET = {android.R.attr.state_checked};
private boolean mChecked = false;
public CheckableLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public boolean isChecked() {
return mChecked;
}
public void setChecked(boolean b) {
if (b != mChecked) {
mChecked = b;
refreshDrawableState();
}
}
public void toggle() {
setChecked(!mChecked);
}
@Override
public int[] onCreateDrawableState(int extraSpace) {
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
if (isChecked()) {
mergeDrawableStates(drawableState, CHECKED_STATE_SET);
}
return drawableState;
}
}
你是否会奇怪,这个图片是如何刷新的呢,通过跟踪源码发现setChecked内部调用的refreshDrawableState方法,跟进去发现调用了ViewGroup的drawableStateChanged(方法根源在View),强制刷新了视图,drawableStateChanged方法你是否还有印象呢,上面提到过AbsListViewy自定义的ViewGroup重写了该方法内部跟进发现最终调用了drawableSelector改变state以达到改变ui的目的。那么AbsListViewy又是在何时调用的drawableStateChanged呢,下面来看一段核心代码
private void onTouchUp(MotionEvent ev) {
// ..........省略后剩下的部分代码碎片............
final int motionPosition = mMotionPosition;
final View child = getChildAt(motionPosition - mFirstPosition);
if (child != null) {
if (mTouchMode != TOUCH_MODE_DOWN) {
child.setPressed(false);
}
final AbsListView.PerformClick performClick = mPerformClick;
setSelectedPositionInt(mMotionPosition);
positionSelector(mMotionPosition, child);
setPressed(true);
updateSelectorState();
}
performItemClick内部调用保存状态,setAdapter清除状态,具体不细解,通过setPressed代码跟进发现内部代码flag判断有调用到refreshDrawableState,一切都不言而喻了吧
public void setPressed(boolean pressed) {
final boolean needsRefresh = pressed != ((mPrivateFlags & PFLAG_PRESSED) == PFLAG_PRESSED);
if (pressed) {
mPrivateFlags |= PFLAG_PRESSED;
} else {
mPrivateFlags &= ~PFLAG_PRESSED;
}
if (needsRefresh) {
refreshDrawableState();
}
dispatchSetPressed(pressed);
}
通过上面简短的叙述相信对于ChoiceList相关的开发我们一定得心印手了吧,某某些人一定要simple,我也无奈啊,还是简单的过一个官方simple吧,下面是效果图
xml布局(choiceMode:multipleChoice多选)
"http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:showDividers="middle"
android:divider="?android:dividerHorizontal">
"@style/Widget.DescriptionBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/intro_message" />
"@android:id/list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:paddingLeft="@dimen/page_margin"
android:paddingRight="@dimen/page_margin"
android:scrollbarStyle="outsideOverlay"
android:choiceMode="multipleChoice" />
</LinearLayout>
item布局就一个ImageView和TextView,color和drawable资源使用selector就不贴了吧,代码调用如下
public class MainActivity extends ListActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.sample_main);
setListAdapter(new MyAdapter());
}
}
源码官网simple可以下载,github上有个哥们也上传了一个https://github.com/nlema0/CustomChoiceList
实际开发在o2o电商行业这块知识比较常用,特别是购物车一类,全选、取消等功能,当然用法也不会知识简单的单选多选,跟多的是叠加嵌套布局,当你看了这篇blog后相信你也会感到so easy!!
新增ListView+EditText需要注意控制EditText的焦点冲突问题,核心代码块
holder.getView(R.id.edit).setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) {
touchedPosition = getPosition();
}
return false;
}
});
if (touchedPosition == getPosition()) {
// 如果当前的行下标和点击事件中保存的index一致,手动为EditText设置焦点。
holder.getView(R.id.edit).requestFocus();
}else {
holder.getView(R.id.edit).clearFocus();
}
EditText编辑的数据保存和赋值跟随Item对象变化,addTextChanged..