转载注明出处:简书-十个雨点
简介
首先介绍一下这个自定义View的作用,先看效果图:
单选模式:
多选模式:
简单来说,就是一个通过滑动的方式来进行选择的工具,这种选择方式多用于星期的选择上,当然也是可以用于其他选项的。
构想
明确了这个View的功能后,我们再来想想应该怎么实现呢。
- 先看这个View需要具有一些什么样的属性:首先是待选项目;然后是字体大小和颜色,各自分为选中和未选择两种状态;要能够区分单选模式和多选模式;选中结果以后,要能将选择的结果进行反馈;最后是背景颜色和圆角半径可以调节。
- 自定义一个新的View,要让这个view能够正确的显示出来,最重要的就是重写onMeasure和onDraw方法。
- 要实现点击滑动的选择效果,必须在onTouchEvent方法中进行处理。
- 因为每个待选项其实是相互独立的,可以看成一个个对象,每个对象负责自己的绘制和判断当前选中状态。我们先设定这些对象是Item[] items;
实现
这里我只介绍一些重要的步骤和思想,具体实现细节请移步github:SweepSelect
1. 先给View设置attribute属性:
在res/value文件夹下新建attrs.xml文件,在其中添加内容:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="SweepSelect">
<attr name="backgroundColor" format="reference"/>
<attr name="itemString" format="reference"/>
<attr name="selectedColor" format="reference"/>
<attr name="normalColor" format="reference"/>
<attr name="selectedSize" format="dimension"/>
<attr name="normalSize" format="dimension"/>
<attr name="corner" format="dimension"/>
<attr name="multyChooseMode" format="boolean"/>
</declare-styleable>
</resources>
这样在使用SweepSelect的时候,就可以直接在layout文件中进行基本的配置了,配置方法如下:
<com.pl.sweepselect.SweepSelect
android:id="@+id/select_week"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:backgroundColor="#515050"
app:corner="4dp"
app:itemString="@array/multyChooseArray"
app:multyChooseMode="true"
app:normalColor="#ffffff"
app:normalSize="16sp"
app:selectedColor="#f5c824"
app:selectedSize="20sp" />
其中itemString那一项的内容在res/value/arrays.xml文件中,内容如下:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="multyChooseArray">
<item>"周一"</item>
<item>"周二"</item>
<item>"周三"</item>
<item>"周四"</item>
<item>"周五"</item>
<item>"周六"</item>
<item>"周日"</item>
</string-array>
</resources>
给SweepSelect设定好attribute属性以后,在View中如何读取这些属性设置呢?接着看。
2. 重写构造函数
一般我们新建了一个View的子类的时候,AndroidStudio都会提示我们重写构造函数,一共有4个构造函数,分别有1个到4个参数,其中最重要的是一个参数的构造函数(以下简称构造1)和两个参数的构造函数(以下简称构造2)。
- 构造1往往用于在代码中直接new一个对象的时候调用;
- 构造2是在layout中使用这个View的时候,由系统自动调用;
我们在layout中给View设定的attribute就是通过构造2的参数来传递给这个View的,所以我们应该在这里对这些attribute进行解析,直接上代码:
TypedArray typedArray=context.obtainStyledAttributes(attrs,R.styleable.SweepSelect);
int length=typedArray.getIndexCount();
for (int i=0;i<length;i++){
int type = typedArray.getIndex(i);
if (type == R.styleable.SweepSelect_backgroundColor) {
backgroundColor=typedArray.getColor(i, DEFAULT_BG_COLOR);
}else if (type==R.styleable.SweepSelect_itemString){
itemStrings=typedArray.getTextArray(i);
}else if (type==R.styleable.SweepSelect_selectedColor){
selectedColor=typedArray.getColor(i, DEFAULT_SELECTED_COLOR);
}else if (type==R.styleable.SweepSelect_normalColor){
normalColor=typedArray.getColor(i, DEFAULT_NORMAL_COLOR);
}else if (type==R.styleable.SweepSelect_selectedSize){
selectedSize=typedArray.getDimensionPixelSize(i,DEFAULT_TEXT_SIZE);
}else if (type==R.styleable.SweepSelect_normalSize){
normalSize=typedArray.getDimensionPixelSize(i,DEFAULT_TEXT_SIZE);
}else if (type==R.styleable.SweepSelect_corner){
corner=typedArray.getDimensionPixelSize(i,DEFAULT_CORNER);
}else if (type==R.styleable.SweepSelect_multyChooseMode){
isMultyChooseMode=typedArray.getBoolean(i,false);
}
}
typedArray.recycle();
看代码应该很好理解了,R.styleable.SweepSelect_selectedColor之类的名字,就对应我们之前在attrs.xml文件中定义的属性,其中SweepSelect是declare-styleable中的name属性,而selectedColor是其中的每个attr子项的name属性。需要注意的是不同format的属性需要用不同的函数来取值。
3. 重写onMeasure方法
先看代码:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int heightMode=MeasureSpec.getMode(heightMeasureSpec);
if (heightMode==MeasureSpec.AT_MOST){
int atMostHeight=MeasureSpec.getSize(heightMeasureSpec);
int height= textRect.height()*3/2+corner;
heightMeasureSpec=MeasureSpec.makeMeasureSpec(Math.min(height,atMostHeight),heightMode);
}else if (heightMode==MeasureSpec.UNSPECIFIED){
int height= textRect.height()*3/2+corner;
heightMeasureSpec=MeasureSpec.makeMeasureSpec(height,heightMode);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
我们知道onMeasure的这两个参数其实是一个组合值,每个参数都是由mode和size组合而成的,具体的含义请查阅API文档,这里就不详细介绍了。当mode不同的时候,我们应该采取不同的处理。主要思路是:尽量将要展示的文本展示全,但以用户的设置优先,所以我没有对MeasureSpec.EXACTLY的情况做处理,也就是保持用户所设置的高度。我也没有对宽度进行设置,使用默认的宽度设置。
上面的代码中有一个变量是textRect,这是使用设置的文本和字体大小计算出来的,是文字所占的范围。
4.重写onDraw方法
在onDraw中,先绘制背景,然后绘制每一个待选项item。很简单,就不贴代码了,上github看吧。
5.重写onTouchEvent方法
还是看代码说话:
@Override
public boolean onTouchEvent(MotionEvent event) {
float x= event.getX();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
//防止父View抢夺触摸焦点,导致触摸事件失效
lastX=event.getX();
currentDirection =DIRECTION_NON;
checkSelect(event);
invalidate();
return true;
case MotionEvent.ACTION_MOVE:
//当滑动距离小于最低限度时,视为未滑动,防止出现抖动现象
if (Math.abs(x-lastX)<MIN_SCROLL_DISTANCE){
return true;
}
if (x > lastX) {
currentDirection = DIRECTION_RIGHT;
} else {
currentDirection = DIRECTION_LEFT;
}
checkSelect(event);
lastX = event.getX();
invalidate();
return true;
case MotionEvent.ACTION_UP:
checkSelect(event);
onSelectResult();
//清理标记位
lastX=-1;
currentDirection =DIRECTION_NON;
invalidate();
return true;
}
return super.onTouchEvent(event);
}
需要注意的是以下几点:
- 当获得按下事件的时候,应该通过调用getParent().requestDisallowInterceptTouchEvent(true)来防止父View争夺焦点,这种情况多发生在嵌套入scrollView使用的情况下:手指按下的位置是在这个View中,但一旦手指移动的范围超出View,就会收到MotionEvent.ACTION_CANCEL事件,被父View截断触摸事件。
- 当滑动的时候,需要有一个最小滑动距离MIN_SCROLL_DISTANCE,超过这个距离才算滑动,否则认为是手指的抖动,不应该引起选中状态的变化。
- 考虑一种情况,用户按住View以后,左右滑动,这时候用户期望的结果应该是,像效果图中所示,根据他的滑动操作,选中状态会有相应的变化:对于单选模式,选中用户最后按压的那个待选项,所以要实时刷新选中状态;对于多选模式,在用户换一个方向滑动的时候,应该切换选中状态,所以还要判断滑动方向。
源码
SweepSelect
源码会继续更新,博客可能会跟不上源码的进度,以源码为准。