android ListView 选择模式

场景描述

有时候场景需要在一堆选项中,选择一个作为选中项。场景类似下图
在这里插入图片描述上图效果是使用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。以及将选中状态保存的成员变量 mCheckStatesmCheckedIdStates

界面响应

当点击界面时,根据触摸响应事件的传递,最终是有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的单选状态的简要流程:

  1. 设置ListVew的mChoiceMode 的标志位,标志位控制了触摸事件的分发和存储选中的item的位置信息。
  2. 当ListView设置了mChoiceMode ,performItemClick会检查是否需要更新界面的选中状态,如果需要改变选中状态则调用接口updateOnScreenCheckedViews
  3. 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才能实现这样的效果。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值