最近有看到百度贴吧app网络请求时的水波浪动画(抱歉只截到了上面那样的静态图),觉得挺好玩,然后自己就研究了一下做了一个。由于没有找到百度贴吧的原图片,所以就在自己的项目中借用了一套图片制作了一个水波浪动画,又顺手做了其他几个动画,其实原理是一样的。
一、实现原理:
把百度贴吧水波浪动画拆开来看,其实就是底部一张背景图片(蓝字白背景,暂称为背景图),然后再覆盖一张用来制作水波浪的图片(白字蓝背景,暂称为波浪图)。背景图不用做任何改动,而波浪图需要进行裁剪成波浪图。裁剪过程:
- 首先利用
UIBezierPath
绘制一条正弦曲线path
,正弦曲线的创建原理如下引用所示(下面会详细介绍)。 - 然后创建一个
CAShapeLayer
并与path
建立关系,并对path
进行路径渲染。 - 然后将波浪图的
layer.mask
设置为创建的CAShapeLayer
对象。 - 最后用
CADisplayLink
实时刷新波浪图的位置。
以下引自正弦曲线百度百科:(括号内为添加的解释)
正弦曲线可表示为
y=Asin(ωx+φ)+k
,定义为函数y=Asin(ωx+φ)+k
在直角坐标系上的图象,其中sin
为正弦符号,x
是直角坐标系 X 轴上的数值,y
是在同一直角坐标系上函数对应的y值,k
、ω
和φ
是常数(k
、ω
、φ
∈R且ω
≠0)。
A
——振幅,当物体作轨迹符合正弦曲线的直线往复运动时,其值为行程的1/2。(控制波浪上下高度)
(ωx+φ)
——相位,反映变量y所处的状态。
φ
——初相,x
=0时的相位;反映在坐标系上则为图像的左右移动。(控制波浪左右偏移速度)
k
——偏距,反映在坐标系上则为图像的上移或下移。
ω
——角速度, 控制正弦周期(单位弧度内震动的次数)。(角速度ω
变大,则波形在X轴上收缩(波形变紧密);角速度ω
变小,则波形在X轴上延展(波形变稀疏))。
二、实现过程:
- 首先找到两张图片:背景图和波浪图。
- 然后创建相同
frame
的背景图和波浪图,依次添加到父视图中,背景图在下,波浪图在上。
#pragma mark - UI
- (UIImageView *)backImageView {
if (!_backImageView) {
_backImageView = [[UIImageView alloc]initWithImage:[UIImage imageNamed:@"project_list_gray_word”]];
_backImageView.frame = self.bounds;
}
return _backImageView;
}
- (UIImageView *)waveImageView {
if (!_waveImageView) {
_waveImageView = [[UIImageView alloc]initWithImage:[UIImage imageNamed:@"project_list_word”]];
_waveImageView.frame = self.bounds;
}
return _waveImageView;
}
- 然后利用
UIBezierPath
绘制一条正弦曲线path
:
设置正弦曲线参数属性:
//波浪相关的参数
@property(nonatomic,assign)CGFloat waveWidth;
@property(nonatomic,assign)CGFloat waveHeight;
@property(nonatomic,assign)CGFloat maxAmplitude;//最大振幅,当物体作轨迹符合正弦曲线的直线往复运动时,其值为行程的1/2。
@property(nonatomic,assign)CGFloat phase;//初相,即波浪左右偏移量
@property(nonatomic,assign)CGFloat shiftPhase;//变化初相
设置属性大小:
self.waveWidth = CGRectGetWidth(self.backImageView.bounds);
self.waveHeight = CGRectGetHeight(self.backImageView.bounds)/2;//设置波浪大小
self.maxAmplitude = self.waveHeight * 0.1f;//振幅
self.phase = 16;
绘制正弦曲线代码:
UIBezierPath * path = [UIBezierPath bezierPath];
CGFloat endX = 0;
for (CGFloat x=0; x<self.waveWidth+1; x += 1) {
endX = x;
CGFloat y = self.maxAmplitude * sinf(2*M_PI/self.waveWidth * x + self.shiftPhase * M_PI/180);
if (x == 0) {
[path moveToPoint:CGPointMake(x, y)];
} else {
[path addLineToPoint:CGPointMake(x, y)];
}
}
CGFloat endY = CGRectGetHeight(self.backImageView.bounds);
[path addLineToPoint:CGPointMake(endX, endY)];
[path addLineToPoint:CGPointMake(0, endY)];
self.waveLayer.path = path.CGPath;
上述代码中的参数waveWidth
和waveHeight
分别表示能够展示波浪图有效区域的宽高。self.maxAmplitud
、2*M_PI/self.waveWidth
和self.shiftPhase * M_PI/180
分别对应正弦函数y=Asin(ωx+φ)+k
中的A
、ω
和φ
,即振幅、角速度和初相。
1、振幅表示波浪上下振动的最大幅度;
2、角速度用来控制正弦周期,即角速度ω
变大,则波形在X轴上收缩(波形变紧密);角速度ω
变小,则波形在X轴上延展(波形变稀疏);
3、初相表示正弦曲线在坐标系上的左右移动距离大小,其中phase
是相位,是设置的常量。而此时创建的正弦曲线是一条静止的曲线,因为其相位没有改变。如果想要出现波浪动起来的效果,则就需要利用CADisplayLink
来实时刷新波浪图的位置,并利用shiftPhase
来不断改变正弦曲线的相位,即在CADisplayLink
刷新的方法中添加self.shiftPhase += self.phase;
(常量phase
设置越大,正弦曲线偏移的距离越远,即动画抖动越剧烈)。这样就会形成波浪动起来的效果了。
- 创建需要对正弦曲线
path
进行渲染的CAShapeLayer
:
- (CAShapeLayer *)waveLayer {
if (!_waveLayer) {
_waveLayer = [CAShapeLayer layer];
}
return _waveLayer;
}
- 并将
waveLayer
和path
进行关联:
self.waveLayer.path = path.CGPath;
这样一条静态的波浪曲线就出来了。
- 下面需要用
CADisplayLink
去实时改变正弦曲线的相位(上面有提到):
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(refreshWave:)];
[_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
刷新方法refreshWave:
中去添加self.shiftPhase += self.phase;
。这样正弦曲线就左右移动起来了。
- 最后去设置
waveLayer
的位置,并将创建好的waveLayer
设置为需要进行裁剪的波浪图(即waveImageView
)的layer.mask
了。
将waveLayer
的位置设置在波浪图的中部:
self.waveLayer.frame = CGRectMake(0, K_HEIGHT/2, K_WIDTH, K_HEIGHT);
设置波浪图的mask
:
self.waveImageView.layer.mask = self.waveLayer;
至此,仿百度贴吧的水波浪加载动画就制作完成了。
其他三种动画:
- 第二种动画:是在第一个动画的基础上添加一个让波浪图从下到上移动的动画就是了。
首先将waveLayer
的位置设置在背景图的最下面:
self.waveLayer.frame = CGRectMake(0, K_HEIGHT, K_WIDTH, K_HEIGHT);
然后设置waveLayer
要移动到的位置,即背景图的最上面:
CGPoint position = self.waveLayer.position;
position.y = position.y - K_HEIGHT;
最后添加动画,并设置动画的开始位置fromValue
和结束位置toValue
、一次动画完成的时间duration
以及动画的重复次数repeatCount
为永久HUGE_VALF
。
[self.waveLayer addAnimation:[self creatBasicAnimationFromValue:[NSValue valueWithCGPoint:self.waveLayer.position] toValue:[NSValue valueWithCGPoint:position] duration:1.0f] forKey:@"position”];
动画设置为:
#pragma mark - 动画
- (CABasicAnimation *)creatBasicAnimationFromValue:(id)fromValue toValue:(id)toValue duration:(CFTimeInterval)duration {
CABasicAnimation * animation = [CABasicAnimation animationWithKeyPath:@"position”];
animation.fromValue = fromValue;
animation.toValue = toValue;
animation.duration = duration;
animation.repeatCount = HUGE_VALF;
animation.removedOnCompletion = NO;
return animation;
}
- 第三种动画:不需要绘制波浪图,也不需要
CADisplayLink
进行实时的刷新,只需要绘制一个移动的矩形路径,添加动画,让其从下到上移动即可。
绘制矩形路径:
- (void)creatAcrossBezierPath {
CGRect rect = CGRectMake(0, 0, K_WIDTH, K_HEIGHT/2);
UIBezierPath * path = [UIBezierPath bezierPathWithRect:rect];
self.waveLayer.path = path.CGPath;
}
将waveLayer
的位置设置在背景图的最下面:
self.waveLayer.frame = CGRectMake(0, K_HEIGHT, K_WIDTH, K_HEIGHT);
设置waveLayer
要移动到的位置,即背景图的最上面:
CGPoint position = self.waveLayer.position;
position.y = position.y - K_HEIGHT - self.waveLayer.position.y/2;
最后添加动画,和第二种一样。
- 第四种动画:和第三种动画一样。只是需要绘制一个平行四边形位于背景图的最左边,并用动画将其从左向右移动即可。
- (void)creatObliqueBezierPath {
UIBezierPath * path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(K_WIDTH/2, 0)];
[path addLineToPoint:CGPointMake(K_WIDTH, 0)];
[path addLineToPoint:CGPointMake(K_WIDTH/2, K_HEIGHT)];
[path addLineToPoint:CGPointMake(0, K_HEIGHT)];
[path closePath];
self.waveLayer.path = path.CGPath;
}
总结:其实显示原理就是用动画在UIBezierPath
绘制的封闭路径中实时显示路径在waveImageView
中裁剪的图片区域罢了。