【轮子】自定义控件,范围选择器
写自定义控件的时候,就想把公司项目里,我自己写的自定义控件挑一个发出来,然而我懒。
写在开头
使用自定义控件的目的一般有两个
- 实现特别的界面效果,若要实现漂亮自由的界面,靠系统的控件通常是很难做到的。
- 或者是简化重复的无意义劳动。页面中一些相似但不完全相同的控件,例如一个页面的头部通常包含一个返回键,一个title,有时候还有一个右侧的按钮。虽然这种情况通过include引入一个布局文件也可以解决问题,但是每次都需要在activity中设置一次title,并且这个方法很不直观。
自定义View的分类
- 继承View
- 继承ViewGroup,
- 继承特定的View,比如TextView
- 继承特定的ViewGroup,比如LinearLayout
继承View或者ViewGroup,好处是更加灵活,但是有比较多的东西需要自定义,比如padding的处理。
继承特定的View,比如TextView,或者LinearLayout,好处是实现起来比较快,很多地方不需要管,但是灵活性差一点。
自己实现自定义控件
本文通过一个自定义的范围选择器,来展示自定义控件的实现思路。
大概就是下面这个样子。
一个或者两个滑块的选择器.png
很简单的控件,支持一个滑块或者两个滑块。
是不是真的需要自定义
在写自定义控件的时候,首先要确定的是,这个控件是否真的需要自己实现。
毕竟android自带控件是经过时间与性能的考验的,TextView动辄一万来行,性能与功能都可以得到保障。
一般的动画需求,通过属性动画加上系统控件都可以得到实现。
但是难保碰到一个脑洞大开的产品非要让你根据手机壳动态切换主题。那就没办法,自己写喽。
三个构造函数
自定义控件时,系统会为我们生成3个构造函数,对应本文中的
public RangeSelectBar(Context context){}
public RangeSelectBar(Context context, @Nullable AttributeSet attrs){}
public RangeSelectBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr){}
区别是
如果在代码中,通过new来实例化一个控件,就是调用第一个构造函数
如果在布局文件中,添加一个控件,就会调用第二个构造函数
而第三个构造函数不常用,它可以由我们自己调用,并且传入一个style
我比较喜欢通过this函数,在第一个构造中调用第二个,在第二个构造中调用第三个,或者看情况忽视第三个。
自定义属性
通过自定义属性可以让我们的自定义控件更具有灵活性与实用性,而非一次性的产品。
声明
首先是声明自定义属性。
在res-values文件夹下新建一个Value resource file,命名为attrs.xml
接下来,按照以下格式往文件里写入自定义属性规则
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="自定义属性组名">
<attr name="属性名,会展示在XML文件中" format="该属性允许接接收的值的类型" /><!--注释-->
</declare-styleable>
</resources>
在当前项目中,使用了以下几种格式
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="RangeSelectBar">
<attr name="RSBSelectColor" format="color" /><!--选中的范围颜色。-->
<attr name="RSBHeight" format="dimension" /><!--进度条高度-->
<attr name="RSBSliderRes" format="reference|color" /><!--滑块资源id,左右两边相同。-->
<attr name="RSBLeftSliderPosition" format="integer" /><!--左方滑块位置-->
<attr name="RSBHideLeftSlide" format="boolean" /><!-- 隐藏左侧滑块-->
</declare-styleable>
</resources>
其中
color代表可以使用颜色id或者色值
dimension可以填入长度数字,dp,px之类
reference代表图片或者shape资源文件
integer代表整形数值
boolean代表布尔类型的数值
此外,比较常用的还有string字符型,以及float浮点型
并且接受的属性类型,允许同时接收两种或者两种以上的类型,比如可接受资源文件的属性一般会兼容颜色类型,就是代码中的reference|color
源码中,各个view的background属性以及imageview的src属性便是这样。
使用
声明完成之后便是使用了,一般声明完成的属性,便可以在xml中直接敲出来,但是需要添加命名空间“app”,自定义属性使用时,也是需要使用app这个命名空间而非系统控件中的android命名空间。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
...
>
<com.zx.rangselectbar.RangeSelectBar
...
app:RSBBackgroundColor="@color/gray_word_light"
...
/>
</LinearLayout>
不过这个命名空间不需要手写,在xml中写入自定义属性,系统便会提示添加命名空间了。
读取
最后也是最重要的,就是读取自定义属性值了
系统读取到布局文件中的属性以及属性值以后,会通过第二个构造函数中的第二个属性“attrs”将该集合传入。
TypedArray t = context.obtainStyledAttributes(attrs, R.styleable.RangeSelectBar);
barHeight = t.getDimension(R.styleable.RangeSelectBar_RSBHeight, 4);
barSelectColor = t.getColor(R.styleable.RangeSelectBar_RSBSelectColor, getResources().getColor(R.color.colorPrimary));
sliderDrawable = t.getDrawable(R.styleable.RangeSelectBar_RSBSliderRes);
leftSliderPosition = t.getInt(R.styleable.RangeSelectBar_RSBLeftSliderPosition, 0);
hideLeftSlider = t.getBoolean(R.styleable.RangeSelectBar_RSBHideLeftSlide, false);
// 最后记得回收
t.recycle();
系统通过集合的形式来整合布局文件中的属性以及属性的值,其中,如果需要同时接收reference|color的值,一般使用Drawable来接受该属性。
这个过程一般会在构造阶段完成。
View生命周期的三个函数
在自定义控件一文中详细写过这三个函数的作用,这里在总结一下。
measure测量当前控件需要的空间大小
layout对当前控件进行布局
draw将布局完成的控件呈现出来
onMeasure
我们知道,当不进行特殊处理时,布局文件中wrap与match属性所呈现的效果都是一致的,都会填充父布局,为了不让我们的控件像是一个不知道自己饭量的傻子,简单的处理是必须的。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
if (heightSpecMode == MeasureSpec.AT_MOST) {
heightSpecSize = (int) (lagerBetweenTheTwo + barBottomPadding + textSize + getPaddingTop() + getPaddingBottom());
setMeasuredDimension(widthSpecSize, heightSpecSize);
}
}
继承View的自定义控件,自身最小需要多少空间非常好算
本文中,范围选择器所需的最小高度就是
轴体/滑块比较宽的那一个+文字大小+两者间隙+上下间距。
而手机的显示器的宽比较短,宽度填充父容器才能得到比较好的显示效果,所以不进行处理。
onLayout
一般来说,自定义控件是不需要处理onLayout的,onlayout方法多用于父控件对子控件的布局控制。
但是本文中的自定义控件,虽说是继承于View,并且没有机会对其添加子控件,但是它自身却是由多个部分组成的,所以接下来需要处理布局。
此时,控件被测量完毕,开始计算各个部分的放置位置。控件总共由三部分组成,从上到下依次是
- 控件主体,是长条加滑块的一个组合,取其中高度最高者
- 控件主体与下方文字间隙的高度
- 下方文字高度
然后按照事先想好的规则,就像拼积木一样将每个部分放上去,计算四角的坐标就可以了。
这段代码对讲解意义不大就不贴出来了。
onDraw
onLayout 部分已经计算好了每个部分的位置,最后一步只要将其画出来即可。
当然本例中,会在onDraw中计算滑块的四角坐标,因为滑块式随着手指移动的,而每次手指触摸滑块时,会实时计算滑块的坐标,并且通过invalidate方法对图形进行刷新,这样的话,在onDraw方法中,通过滑块中心点统一计算滑块的坐标反而是比较节省计算时间的一个方式。代码也更加简洁。
onTouch
当然对于一部分同学来说,如何在自定义控件中处理触摸事件也是一件比较头疼的事情,但这个不是自定义控件的难点所在,希望事件分发机制会对你有帮助。
以及最重要的
最重要的源码地址,欢迎下载,觉得有用请给我个star。
个人理解,难免有错误纰漏,欢迎指正。转载请注明出处。