在闲逛一个图片社区时看到这张图片,个人对炫酷的东西比较敏感(视觉肤浅),本来想下载一下这个 App 看一下实际效果,可是没找到。心有不甘,于是分析了一下,感觉实现起来不会太难,自己也花点时间实现了效果,发布了一个库。
Github地址:https://github.com/OCNYang/ContourView
今天就借助这个开源控件,来为大家梳理一下自定义 View 的整个流程:
构造函数(获取自定义属性,设置画笔等)
--> onMeasure()(测量大小)
--> onSizeChanged()(确定大小,一般我们在这里获取大小)
--> (onLayout()自定义View,因为没有子控件,这一步是不需要的)
--> onDraw()(按照需求和根据属性绘制实际内容)
--> 其他
那么现在我们就根据上面这个流程一步步来实现 ContourView。
根据上面的分析,实现的思路大概都有了。那么我们就开始寻找具体实现方法。
首先,我们选用三阶贝塞尔曲线,我们都知道三阶曲线的计算公式是:
path.moveTo(start.x, start.y);
path.cubicTo(control1.x, control1.y, control2.x,control2.y, end.x, end.y);
也就是说绘制一段曲线,我们需要知道两个锚点的坐标以及两个控制点的坐标,为了保证曲线的弯曲度能够达到理想的状态,控制点的坐标也不能是随意取的,这就要求我们必须通过一种计算方法合理的得出控制点的坐标。Google 了一下,发现先驱们已经找到了很多种方法供我们选择。
最终经过对比我们选用了这样一种方法:
这种方法大概的形式如上图,利用锚点集合,连续的4个锚点坐标Pi-1、Pi、Pi+1、Pi+2,通过具体公式来计算出中间两个锚点之间曲线的两个控制点坐标。
详细的计算方法介绍请看 ContourView 的 WiKi:
Bézier-求贝塞尔曲线控制点
通过上面的分析,其实我们大概能总结出需要自定义的属性有哪些了。这里不着急,我们先总结一下自定义属性相关的内容和步骤?
1. 创建自定义属性文件
在 res/values/ 下新建 attrs.xml 文件(默认新建项目没有这个文件)。文件内容类似如下:
其中 attr 和 declare-styleable 节点分别代表的意思如下:
attr: 定义了一个属性,属性名为 custom_color 这个是可以随意起的,但是要注意不要和其他控件所冲突, format 所定义的是属性的格式,其中格式又分为好多种,下面会细说,这里定义的是颜色 color。
**declare-styleable:**定义了一个属性组,在里面我们可以单独写 attr 属性,也可以引用直接在 resources 下定义的 attr,其中的区别就是引用的不用写 format。
需要注意的是,attr 并不依赖与 declare-styleable,declare-styleable 只是方便了 attr 的使用,使属性的使用更加明确。两者在代码中的获取方式并不相同,下面会细说。
在实际开发中,我们一般是采用 declare-styleable 方式,直接定义一组自己所编写的自定义控件需要用到的属性。
2. 自定义属性的可以设置哪些属性
我们根据需要可以设置的自定义属性的格式一共有一下几种:
format=“格式” | 说明 | app:myattr=“使用值” |
---|---|---|
reference | 参考某一资源ID | “@drawable/图片ID” |
color | 颜色值 | “#FFFFFFFF” or “@color/颜色ID” |
boolean | 布尔值 | “true” or “false” |
dimension | 尺寸值 | “0dp” |
float | 浮点型 | “1.2” |
integer | 整型值 | “10” |
fraction | 百分数值 | “50%” |
string | 字符串 | “OCN.Yang” |
enum | 枚举值(详见下) | “自定义类型名称” |
flag | 位或运算 | “center | bottom” |
附:
enum 枚举型定义:
enum 使用:
app:handsomeBoy="OCNYang"
flag 定义:
flag 使用:
app:gravity="center|bottom"
混搭使用
这样,你传入资源ID或颜色值都是可以的了。
3. 获取自定义属性
那怎么获取这些自定义的属性呢,只需要在自定义 View 的构造方法(两个参数或两个以上的参数)里通过一下方式就能获取到了:
public ContourView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ContourView);
//注意:获取时自定义的属性名有变动,例如:定义名:contour_style -> 获取名:ContourView_contour_style(即:自定义属性组名_属性名)
mStyle = typedArray.getInt(R.styleable.ContourView_contour_style, STYLE_SAND);
}
当然获取时,不同格式的属性需要通过 TypedArray 对应的不同的方法获取,那 TypedArray 都有哪些获取方法呢?如下图:
通过方法名称,相信你能很轻易的知道,需要哪个对应方法获取了。
如果你想更详细的了解每个方法的详细介绍,可以点击下面链接查看:
https://developer.android.com/reference/android/content/res/TypedArray.html
另外,比较特殊的 enum 的获取方法:
由于 enum 的 value 值只能设置 int 型,所以,获取enum的方式是getInt()
。
好了,关于自定义属性的介绍大概就是这么多内容了,那么回到原题,我们的 ContourView 需要哪几种 自定义属性呢?其实通过分析模块中我们就基本知道我们需要的属性有哪些了:
具体如下:
既然自定义 View,那我们一定会为它提供一种或几种内置好的样式呀。这样别人在偷懒不想自己定制样式时,可以也有不错的显示效果呀!
通过上面知道,ContourView 的轮廓样式主要是通过给出的锚点集控制的,所有的锚点围成的闭合曲线就是轮廓的大概样式了。
所以,这里我们想内置几种样式,就等于内置几个锚点集就行了,这里的我们内置的锚点坐标为了使得不同大小显示效果相同,我们先在 onSizeChanged() 获得了 View 的宽高,然后根据宽高按照百分比来设置坐标。
设置的内置轮廓有以下几种(丑爆了),只是轮廓,颜色是自己设置的:
样式(contuor_style) | 效果 |
---|---|
Sand(默认) | |
Clouds | |
Beach | |
Ripples | |
Shell |
关于自定义 View 重写各方法的介绍,网上已经有太多太多,这里就不再啰嗦了。
这里推荐一个关于自定义 View 尤其关于绘制方面讲解特别详细的系列博客:
https://github.com/GcsSloop/AndroidNote
另外厚脸皮的放上一篇自己的关于讲解“自定义组合控件”的博客地址:
http://www.jianshu.com/p/4bbc967214c9
我们知道,在自定义 View 时,必须要有构造函数的,对于4个构造函数,有时可能大家不确定到底该重写哪个,也不知道每个构造函数有什么区别,这里对常用的做法做下说明。
//在代码中直接 new 一个 Custom View 实例时,会调用第一个构造函数.这个没有任何争议.
public View(Context context);
//在 xml 布局文件中使用自定义 View 时,会调用第二个构造函数.这个也没有争议.
public View(Context context, AttributeSet attrs);
//关于这个构造函数的调用,网上真是众说纷纭,我也不说哪种说法正确,下面提供详解
public View(Context context, AttributeSet attrs, int defStyle);
//4个参数的构造函数这里不做考虑
关于内部这4个构造函数是怎么调用的,这里直接放源码图片,自己一目了然:
大家在自定义 View 时,如果没有特别的需求,只要重写前两个构造函数就可以了,我习惯性的写成下面的形式:
public class MyView extends View {
public MyView(Context context) {
this(context, null);
}
public MyView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//初始化画笔,做一些属性的默认赋值等;
//获取自定义的属性等;
}
}
那,说了这么多还是没有提第3个参数到底是干什么的有什么用呀,这里我就不再为大家详细讲解了,这里找到了一片文章,讲解了第3个参数在什么时候怎么使用,大家可以看一下:
http://www.cnblogs.com/angeldevil/p/3479431.html
回归到 ContourView,其实 ContourView 内部很简单,只对 onDraw() 进行了重写,毕竟 ContourView 的主要部分就是绘制。绘制的逻辑,就是遍历锚点集,然后利用上面 WiKi 里提到的公式求出各段曲线的控制点,然后用三阶贝塞尔曲线画出路径。当遍历完锚点集时,闭合曲线的轮廓基本上就得到了,然后就用Shader对路径进行绘制就行。
好了,本次的梳理内容就到这了,感兴趣的可以查看 ContourView 的源码进行分析,同时 ContourView 的这种背景效果还是不错的,需要的时候大家真的可以用到呢!
ContourView GitHub:https://github.com/OCNYang/ContourView
如果大家想看一些高级的自定义 View 的例子可以查看上次开源的 App 的天气模块,其中的天气页面以及天气折线图等等控件都是通过自定义 ViewGroup 或自定义 View 实现的。地址是:
Qbox Github:https://github.com/OCNYang/QBox