场景描述
有时候场景需要在一堆选项中,选择一个作为选中项。场景类似下图
上图效果是使用ListView 自带的item布局来实现的。简要代码如下:
@BindView(R.id.my_list_view)
ListView myListView;
private void initView1(){
List<String> list = new ArrayList<>();
for (int i = 0;i< 50;i++){
list.add("选项 "+i);
}
ArrayAdapter<String> arrayAdapter = new ArrayAdapter<>(getContext(),android.R.layout.simple_list_item_single_choice,list);
myListView.setAdapter(arrayAdapter);
// 必选设置为单选
myListView.setChoiceMode(AbsListView.CHOICE_MODE_SINGLE);
}
布局使用最简单的布局:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/my_list_view" />
</FrameLayout>
使用setChoiceMode的好处是,在界面响应客户点击过程中不需要要监听,改变设置给adapter的数据。如果想要获取被选中的item,只需要通过myListView.getSelectedItemId() 或者 myListView.getCheckedItemPosition()
来获取。
系统实现原理
系统是如何实现这功能的,由于必须调用接口setChoiceMode
因此,可以根据这个接口对源码进行跟踪。简要源码如下:
设置选择模式
setChoiceMode 的源码如下:
public void setChoiceMode(int choiceMode) {
mChoiceMode = choiceMode;
if (mChoiceActionMode != null) {
mChoiceActionMode.finish();
mChoiceActionMode = null;
}
if (mChoiceMode != CHOICE_MODE_NONE) {
if (mCheckStates == null) {
mCheckStates = new SparseBooleanArray(0);
}
if (mCheckedIdStates == null && mAdapter != null && mAdapter.hasStableIds()) {
mCheckedIdStates = new LongSparseArray<Integer>(0);
}
// Modal multi-choice mode only has choices when the mode is active. Clear them.
if (mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL) {
clearChoices();
setLongClickable(true);
}
}
}
从源码可知,这个接口最重要的是设置了一个成员变量mChoiceMode
。以及将选中状态保存的成员变量 mCheckStates
和 mCheckedIdStates
。
界面响应
当点击界面时,根据触摸响应事件的传递,最终是有item来响应这个事件,查看系统布局android.R.layout.simple_list_item_single_choice
源码。部件源码如下:
<CheckedTextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/text1"
android:layout_width="match_parent"
android:layout_height="?android:attr/listPreferredItemHeightSmall"
android:textAppearance="?android:attr/textAppearanceListItemSmall"
android:gravity="center_vertical"
android:checkMark="?android:attr/listChoiceIndicatorSingle"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" />
从布局中确定并没有响应点击事件的接口。那么查看控件CheckedTextView
的源码 。通过源码分析确认CheckedTextView
并没有响应触摸事件。那么触摸事件只能是item的父控件(ListView)消耗了,在父控件(ListView)消耗事件的原因是:由于某个选中的item还需要改变其他的item,将其他item的状态设置为未选中,因此事件在父控件(ListView)消耗就非常合理了。
- ListView响应触摸事件
源码如下:
public boolean performItemClick(View view, int position, long id) {
...
if (mChoiceMode != CHOICE_MODE_NONE){
...
// 选中状态改变
if (checkedStateChanged) {
// 更新界面
updateOnScreenCheckedViews();
}
}
}
// 更新界面相关源码
private void updateOnScreenCheckedViews() {
final int firstPos = mFirstPosition;
final int count = getChildCount();
final boolean useActivated = getContext().getApplicationInfo().targetSdkVersion
>= android.os.Build.VERSION_CODES.HONEYCOMB;
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
final int position = firstPos + i;
if (child instanceof Checkable) {
((Checkable) child).setChecked(mCheckStates.get(position));
} else if (useActivated) {
child.setActivated(mCheckStates.get(position));
}
}
}
根据源码的分析可以得知,设置listView的单选状态的简要流程:
- 设置ListVew的mChoiceMode 的标志位,标志位控制了触摸事件的分发和存储选中的item的位置信息。
- 当ListView设置了mChoiceMode ,performItemClick会检查是否需要更新界面的选中状态,如果需要改变选中状态则调用接口
updateOnScreenCheckedViews
。 updateOnScreenCheckedViews
接口则是遍历ListView的子View,将子View的状态设置为需要的状态,如果子View实现了Checkable接口,则把调用接口‘setChecked’,否则将字View的状态设置为Activated的状态。
自定义选中效果。
由于一般,实现自定义ListView,我们使用的是继承android.widget.BaseAdapter
。同时使用布局文件将多个控件组合为需要的自定义布局,加载布局的代码如下:
public View getView(int position, View convertView, ViewGroup parent){
convertView = LayoutInflater.from(mContext).inflate(R.layout.item_my_list_view,parent,false);
return convertView
}
布局的View是View,View来实现接口Checkable,比较少见,因此只剩下View为Activated时,显示相应的效果来实现选中状态了,实现View不同状态的效果一般使用xml文件来表示,一般代码如下
<?xml version="1.0" encoding="utf-8"?>
<selector android:exitFadeDuration="@android:integer/config_longAnimTime"
xmlns:android="http://schemas.android.com/apk/res/android">
<!--获取焦点状态-->
<item android:state_focused="true" android:drawable="@color/list_choice_pressed_bg_light"/>
<!--被按下状态-->
<item android:state_pressed="true" android:drawable="@color/list_choice_pressed_bg_light"/>
<!--选中状态-->
<item android:state_selected="true" android:drawable="@color/list_choice_pressed_bg_light"/>
<!--激活状态-->
<item android:state_activated="true" android:drawable="@color/list_choice_pressed_bg_light"/>
<!--其他状态-->
<item android:drawable="@android:color/transparent"/>
</selector>
实现自定义的选中效果,可以通过两种方式来实现。
- ListView的属性listSelector
- 在item中设置属性,比如说设置item的背景颜色,或者设置item控件中的selector效果。
方式1
使用ListView的属性listSelector来实现相关的效果。根据源码item被选中,因此布局文件代码如下:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/my_list_view"
android:listSelector="@drawable/selector_item_state"
android:drawSelectorOnTop="true"/>
</FrameLayout>
selector_item_state 的代码如下
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_activated="true" android:drawable="@android:color/holo_green_dark"/>
</selector>
经过测试发现这样无效,但是其他状态是有用的,比如说state_pressed等等。具体原因尚未查明。
方式2
在item中设置相关的状态,比如说设置不同状态下面的背景是不同的,布局代码如下
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/selector_item_state ">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/iv_left"
android:layout_alignParentStart="true" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/tv_center"
android:layout_centerInParent="true"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:id="@+id/iv_right"/>
</RelativeLayout>
经过测试,发现这样设置是有效的。
其他
如果item布局布局中有单选控件,这时候涉及到一个item中的子控件获取到了焦点,因此item无法响应item事件的问题,这时候需要将单选控件设置为失去焦点和不响应clickable才能实现这样的效果。