Swift下拉波浪动画

我们先看一下效果,现在很多app有这种交互,普通的下拉刷新有点low,这样感觉酷炫一点

1.gif

水波动画的重点就是sincos函数

我们来复习一下不知道是初中还是高中的知识

正弦型函数解析式:y=Asin(ωx+φ)+h
φ(初相位):决定波形与X轴位置关系或横向移动距离(左加右减)
ω:决定周期(最小正周期T=2π/|ω|)
A:决定峰值(即纵向拉伸压缩的倍数)
h:表示波形在Y轴的位置关系或纵向移动距离(上加下减)

2.png

po出项目中的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
private struct BingoWaveRefreshViewData{
/// 最大波峰
static var maxVariable : CGFloat = 1.6
/// 最小波峰
static var minVariable : CGFloat = 1.0
/// 最小波峰增量
static var minStepLength : CGFloat = 0.01
/// 最大波峰增量
static var maxStepLength : CGFloat = 0.05
/// 键值
static var keyPathsContentOffset = "contentOffset"
}
/// 刷新事件
var actionClosure : (()->())?
/// 顶部波浪颜色
var topWaveColor : UIColor? {
willSet{
firstWaveLayer.fillColor = newValue!.cgColor
}
}
/// 底部波浪颜色
var bottomWaveColor : UIColor? {
willSet{
secondWaveLayer.fillColor = newValue!.cgColor
}
}
/// 对应scrollView
fileprivate weak var scrollView : UIScrollView?{
willSet{
_cycle = CGFloat(2 * M_PI) / newValue!.frame.size.width;
}
}
/// 定时器
fileprivate var displaylink : CADisplayLink!
/// 顶部波浪
fileprivate var firstWaveLayer : CAShapeLayer!
/// 底部波浪
fileprivate var secondWaveLayer : CAShapeLayer!
/// 状态
fileprivate var state : WaveRefreshViewState! = .stop
/// 根据scrollView偏移量得出的比例
fileprivate var _times : NSInteger = 0
/// 波峰值
fileprivate var _amplitude : CGFloat = 0
/// 波浪的周期值
fileprivate var _cycle : CGFloat = 0
/// 单位时间平移速率
fileprivate var _speed : CGFloat = 0
/// 波浪平移偏移量
fileprivate var _offsetX : CGFloat = 0
/// scrollView偏移量
fileprivate var _offsetY : CGFloat = 0
/// 计算波峰的比率
fileprivate var _variable : CGFloat = 0
/// 波峰至波谷的距离
fileprivate var _height : CGFloat = 0
/// 波峰是否增大
fileprivate var _increase : Bool = false

绘制曲线

如果要根据函数绘制曲线,我们需要设置各个值
初相位设置为0
周期设置为2π
波峰设置为1
位移设置为0
然后根据y = sin(x)计算出点的坐标然后使用CGMutablePath对象的addLine方法连线得出最后路径

由静态到动态

我们已经绘制了一条静态的曲线,我们通过什么方法来让它变成动态的呢?我们看上面各个值的定义,我们通过初相位来让曲线左右平移,我们需要通过定时器来让初相位根据时间进行线性的变化,我们设置单位时间的初相位的变化值为π/2,因为函数的周期为,4个单位时间为一个周期,我们可以通过改变这个变量来控制波浪平移的速度
顶部波浪的刷新方法,firstWaveLayer已创建好

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func configFirstWaveLayerPath() -> Void {
guard let scrollView = scrollView else {
return
}
var y = _offsetY
let path : CGMutablePath = CGMutablePath()
path.move(to: CGPoint(x: 0, y: y))
let waveWidth : CGFloat = scrollView.frame.size.width
for x in 0...Int(waveWidth) {
y = _amplitude * sin(_cycle * CGFloat(x) + _offsetX) + _offsetY
path.addLine(to: CGPoint(x: CGFloat(x), y: y))
}
path.addLine(to: CGPoint(x: waveWidth, y: self.frame.size.height))
path.addLine(to: CGPoint(x: 0, y: self.frame.size.height))
path.closeSubpath()
firstWaveLayer.path = path
}

顶部波浪的刷新方法,secondWaveLayer已创建好

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func configSecondWaveLayerPath() -> Void {
guard let scrollView = scrollView else {
return
}
var y = _offsetY
let path = CGMutablePath()
path.move(to: CGPoint(x: 0, y: y))
let forward = CGFloat(M_PI) / (_cycle * 4.0)
let waveWidth : CGFloat = scrollView.frame.size.width
for x in 0...Int(waveWidth) {
y = _amplitude * cos(_cycle * CGFloat(x) + _offsetX + forward) + _offsetY
path.addLine(to: CGPoint(x: CGFloat(x), y: y))
}
path.addLine(to: CGPoint(x: waveWidth, y: self.frame.size.height))
path.addLine(to: CGPoint(x: 0, y: self.frame.size.height))
path.closeSubpath()
secondWaveLayer.path = path
}

调整两个波浪的偏移量

我们其中一个是sincos函数,由上图可见两个曲线错位了1/8个周期,我们如果要让上下对称需要将cos函数加1/4的个周期

1
let forward = CGFloat(M_PI) / (_cycle * 4.0)

刷新波峰

通过_increase属性交替来改变波峰的值,增长到最大时候开始减小,减小到最小的开始增大

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func configWaveAmplitude() -> Void {
if (_increase) {
_variable += BingoWaveRefreshViewData.minStepLength
} else {
let minus : CGFloat = self.state == .animatingToStopped ? BingoWaveRefreshViewData.maxStepLength : BingoWaveRefreshViewData.minStepLength
_variable -= minus;
if (_variable <= 0.00) {
self.stopWave()
}
}
if (_variable <= BingoWaveRefreshViewData.minVariable) {
_increase = !(self.state == .animatingToStopped)
}

if (_variable >= BingoWaveRefreshViewData.maxVariable) {
_increase = false
}
// self.amplitude = self.variable*self.times;
if (_times >= 7) {
_times = 7;
}
_amplitude = _variable * CGFloat( _times)
_height = BingoWaveRefreshViewData.maxVariable * CGFloat( _times)
}

添加监听

监听scrollView.contentOffset的变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if scrollView == nil {
return
}
if let keyPath = keyPath {
switch keyPath {
case BingoWaveRefreshViewData.keyPathsContentOffset:
didChangeContentOffset()
default:
return
}
}
}
func observe(scrollView : UIScrollView) -> Void {
self.scrollView = scrollView
self.scrollView?.addObserver(self, forKeyPath: BingoWaveRefreshViewData.keyPathsContentOffset, options: .new, context: nil)
}
主要波浪处理部分大致就这些,构建时使用runtime来为scrollView动态增加属性,具体请在具体项目中查看,文档覆盖率比较高

最后,po出项目地址,可以直接使用WaveRefreshView

希望可以帮助到大家,如果有更优的方法请留言或私信交流