所谓波浪效果如图:
看起来很柔和,很惹眼,如题目所说,作出这个效果需要用到
CAShapeLayer
和
CADisplayLink
1、CAShapeLayer
CAShapeLayer
顾名思义,继承于CALayer
。每个CAShapeLayer
对象都代表着将要被渲染到屏幕上的一个任意的形状(shape
)。具体的形状由其path
(类型为CGPathRef
)属性指定。 普通的CALayer
是矩形,所以需要frame
属性。 CAShapeLayer
初始化时也需要指定frame
值,但 它本身没有形状,它的形状来源于其属性path
.。(tip: 如果我们在使用时将CAShapeLayer
和UIBezierPath
相结合,就可以灵活的绘制出很多动画效果了) 这个形状不一定要闭合,图层路径也不一定要不可破,事实上你可以在一个图层上绘制好几个不同的形状。你可以控制一些属性比如lineWith
(线宽,用点表示单位),lineCap
(线条结尾的样子),和lineJoin
线条之间的结合点的样子);但是在图层层面你只有一次机会设置这些属性。如果你想用不同颜色或风格来绘制多个形状,就不得不为每个形状准备一个图层了。 CAShapeLayer
有不同于CALayer的属性,它从CALayer
继承而来的属性在绘制时是不起作用的。相比直下,使用CAShapeLayer
有以下一些优点:
- 渲染快速
CAShapeLayer
使用了硬件加速,绘制同一图形会比用Core Graphics
快很多。 - 高效使用内存。一个
CAShapeLayer
不需要像普通CALayer
一样创建一个寄宿图形,所以无论有多大,都不会占用太多的内存。 - 不会被图层边界剪裁掉。一个
CAShapeLayer
可以在边界之外绘制。你的图层路径不会像在使用Core Graphics
的普通CALayer
一样被剪裁掉。 - 不会出现像素化。当你给
CAShapeLayer
做3D变换时,它不像一个有寄宿图的普通图层一样变得像素化。
2、CADisplayLink
CADisplayLink
就像是一个定时器,每隔几毫秒刷新一次屏幕。能让我们以和屏幕刷新频率相同的频率去刷新我们绘制到屏幕上的内容。CADisplayLink
使用方式如下:
timer = CADisplayLink(target: self, selector: #selector(waveEvent))
timer?.add(to: .current, forMode: .commonModes)
当CADisplayLink
注册到runloop
以后,屏幕刷新的时候就会调用绑定到它上面的target
所拥有的selector
方法。停止CADisplayLink
的运行非常的简单,只需要调用它的invalidate
方法。
说道定时器,我们肯定会想到NSTimer
, CADisplayLink
的接口设计的和NSTimer
很类似,所以它实际上就是一个内置实现的替代,但是和timeInterval
以秒为单位不同,CADisplayLink
有一个整型的frameInterval
属性,指定了间隔多少帧之后才执行。默认值是1,意味着每次屏幕更新之前都会执行一次。但是如果动画的代码执行起来超过了六十分之一秒,你可以指定frameInterval
为2,就是说动画每隔一帧执行一次(一秒钟30帧)。正常情况下CADisplayLink
在屏幕每次刷新时都会调用,精确度非常高,并且CADisplayLink
的使用场合相对专一,适合做UI的不停重绘,比如动画的连续绘制。CADisplayLink
会保证帧率足够连续,使得动画看起来更加平滑。NSTimer
的使用范围要广泛很多,可以做单次或者循环处理某个任务,精度相比CADisplayLink
要低。
3、绘制波浪轮廓
绘制波浪轮廓,我们会想到三角形的正弦、余弦函数,如图是在单位为1的右手直角坐标系中的曲线变化如下:
可以看到在(-2π , 2π )的范围类,y值在[-1, 1]之间变化。
以正弦曲线为例,它可以表示为y=Asin(ωx+φ)+k,公式中各符号表示的含义:
A–振幅,即波峰的高度。
(ωx+φ)–相位,反应了变量y所处的位置。
φ–初相,x=0时的相位,反映在坐标系上则为图像的左右移动。
k–偏距,反映在坐标系上则为图像的上移或下移。
ω–角速度,控制正弦周期(单位角度内震动的次数)。
通过上面的函数,我们就能计算出波浪曲线上任意位置的坐标点。通过CGMutablePath
的函数addLineToPoint
来把这些点连接起来,就构建了波浪形状的path
。只要我们的设定相邻两点的间距够小,就能构建出平滑的正弦曲线,构建正弦波浪的代码如下:
for x in 0...Int(width) {
y = height * CGFloat(sinf(waveFrequency_f * Float(x) + offset_f))
path.addLine(to: CGPoint(x: CGFloat(x), y: y))
maskPath.addLine(to: CGPoint(x: CGFloat(x), y: -y))
}
在这里我们设定了两个正弦曲线上的点的横坐标间距是1,现在来解释一下通过横坐标x来得出y的计算过程:
y = height * CGFloat(sinf(waveFrequency_f * Float(x) + offset_f))
height
表示曲线的波峰值,(height
==> let height = CGFloat(priWaveHeight)
)。waveFrequency_f
表示角速度,也就是控件单位角度内振动的次数。offset_f
代表偏距,由于我们需要让波浪曲线的波峰在layer的范围内显示,所以需要将整个曲线向下移动波峰大小的单位,因为CALayer使用左手坐标系,所以向下移动需要加上波峰的大小。(offset_f
==> let offset_f = Float(offset * 0.045)
)
4、让波浪曲线动起来
正弦或者余弦曲线上的点,不论角度如何,在y轴上的变化范围在它的波峰与波谷之间。拿单位正交直角坐标系来说,只要我们规律性的增加角度值,那么曲线上的点就会在[1, -1]之间变化。找到规律,我们就能让波浪曲线动起来:
offset += priWaveSpeed
let width = frame.width
let height = CGFloat(priWaveHeight)
let path = CGMutablePath()
path.move(to: CGPoint(x: 0, y: height))
var y: CGFloat = 0
let maskPath = CGMutablePath()
maskPath.move(to: CGPoint(x: 0, y: height))
let offset_f = Float(offset * 0.045)
let waveFrequency_f = Float(0.01 * priFrequency)
for x in 0...Int(width) {
y = height * CGFloat(sinf(waveFrequency_f * Float(x) + offset_f))
path.addLine(to: CGPoint(x: CGFloat(x), y: y))
maskPath.addLine(to: CGPoint(x: CGFloat(x), y: -y))
}
path.addLine(to: CGPoint(x: width, y: height))
path.addLine(to: CGPoint(x: 0, y: height))
path.closeSubpath()
self.realWaveLayer.path = path
maskPath.addLine(to: CGPoint(x: width, y: height))
maskPath.addLine(to: CGPoint(x: 0, y: height))
maskPath.closeSubpath()
self.maskWaveLayer.path = maskPath
在这里, 使用CADisplayLink
来不断刷新由CGMutablePath
创建的形状,两次刷新之间曲线的变化通过增加初相来实现, 初相:offset += priWaveSpeed
5、最后
代码在这儿:https://github.com/irembeu/WaveView.git