【造轮子系列】一个选择星期的工具——SweepSelect View

1,367 阅读6分钟

简介

首先介绍一下这个自定义View的作用,先看效果图:
单选模式:


单选模式

多选模式:


多选模式
简单来说,就是一个通过滑动的方式来进行选择的工具,这种选择方式多用于星期的选择上,当然也是可以用于其他选项的。

构想

明确了这个View的功能后,我们再来想想应该怎么实现呢。

  1. 先看这个View需要具有一些什么样的属性:首先是待选项目;然后是字体大小和颜色,各自分为选中和未选择两种状态;要能够区分单选模式和多选模式;选中结果以后,要能将选择的结果进行反馈;最后是背景颜色和圆角半径可以调节。
  2. 自定义一个新的View,要让这个view能够正确的显示出来,最重要的就是重写onMeasure和onDraw方法。
  3. 要实现点击滑动的选择效果,必须在onTouchEvent方法中进行处理。
  4. 因为每个待选项其实是相互独立的,可以看成一个个对象,每个对象负责自己的绘制和判断当前选中状态。我们先设定这些对象是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);
}

需要注意的是以下几点:

  1. 当获得按下事件的时候,应该通过调用getParent().requestDisallowInterceptTouchEvent(true)来防止父View争夺焦点,这种情况多发生在嵌套入scrollView使用的情况下:手指按下的位置是在这个View中,但一旦手指移动的范围超出View,就会收到MotionEvent.ACTION_CANCEL事件,被父View截断触摸事件。
  2. 当滑动的时候,需要有一个最小滑动距离MIN_SCROLL_DISTANCE,超过这个距离才算滑动,否则认为是手指的抖动,不应该引起选中状态的变化。
  3. 考虑一种情况,用户按住View以后,左右滑动,这时候用户期望的结果应该是,像效果图中所示,根据他的滑动操作,选中状态会有相应的变化:对于单选模式,选中用户最后按压的那个待选项,所以要实时刷新选中状态;对于多选模式,在用户换一个方向滑动的时候,应该切换选中状态,所以还要判断滑动方向。

源码

SweepSelect
源码会继续更新,博客可能会跟不上源码的进度,以源码为准。