上个月开始项目的更新迭代,除了新功能,再加入一些交互动画,添加一些灵动性。我们UI妹子说百度外卖个人中心的波浪动画不错,刚看到感觉效果特别棒,并有些无从下手。上网一查,有很多仿百度外卖波浪动画代码,看了几篇一时兴起,也写了一个继承 UIView 的 MYWaveView 类。但是接入现有项目中,感觉就有点麻烦了,耦合度太高,需要修改的东西好多,时间紧迫,强加入项目中。昨天终于提审了,有时间修改一下这个地方的代码,运用 runTime 将波浪动画设置成 UIView 的扩展类,零耦合接入项目中,HAPPY!!!
以下是效果图:
主要思路
这个动画主要是运用 CAShapeLayer 和 CGPath 通过 sin 函数绘制波浪图形,然后使用 CADisplayLink 以屏幕刷新率相同的频率对波浪的偏移进行刷新,从而达到动画效果。为了降低耦合度,运用 runLoop 将继承 UIView 的 MYWaveView 改成 UIVIew 的扩展类 UIView+MYWave
CAShapeLayer
CAShapeLayer 是 CALayer 的子类,通过矢量图形绘制,一般和 CGPath 一起使用。使用 CAShapeLayer 有以下优点:
渲染快速:CAShapeLayer 使用了硬件加速,绘制同一图形会比用 Core Graphics 快很多。
高效使用内存:一个 CAShapeLayer 不需要像普通 CALayer 一样创建一个寄宿图形,所以无论有多大,都不会占用太多内存。
不会被图形边界剪裁掉:一个 CAShapeLayer 可以在边界之外绘制,图层路径不会像在使用 Core Graphics 的普通 CALayer 一样被裁剪掉。
不会出现像素化:当你给 CAShapeLayer 做 3D 变换时,它不像一个有寄宿图层一样变得像素化。
CADisplayLink
CADisplayLink 是一个能让我们以屏幕刷新率相同的频率将内容画到屏幕上的定时器。一旦 CADisplayLink 以特定的模式注册到 runLoop 之后,每当屏幕需要刷新,runLoop 就会调用 CADisplayLink 绑定的 target 上的 selector,这时 target 可以读到 CADisplayLink 的每次调用的时间戳,用来准备下一帧显示需要的数据。
runLoop
runLoop 是一种事件循环结构,我们主要运用 objc_setAssociatedObject
和 objc_getAssociatedObject
关联对象来实现在分类中存储和获取属性值。
- (void)setTimer:(CADisplayLink *)timer
{
objc_setAssociatedObject(self, "timer", timer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (CADisplayLink *)timer
{
return objc_getAssociatedObject(self, "timer");
}
注意:属性类型不同,objc_AssociationPolicy 也不同
- OBJC_ASSOCIATION_ASSIGN 对应 assign
- OBJC_ASSOCIATION_RETAIN_NONATOMIC 对应 strong
- OBJC_ASSOCIATION_COPY_NONATOMIC 对应 copy
主要代码
私有属性设置
// 刷屏器
@property (nonatomic, strong) CADisplayLink *timer;
// 真实浪
@property (nonatomic, strong) CAShapeLayer *realWaveLayer;
// 遮罩浪
@property (nonatomic, strong) CAShapeLayer *maskWaveLayer;
- (void)setTimer:(CADisplayLink *)timer
{
objc_setAssociatedObject(self, "timer", timer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (CADisplayLink *)timer
{
return objc_getAssociatedObject(self, "timer");
}
- (void)setOffset:(CGFloat)offset
{
objc_setAssociatedObject(self, "offset", @(offset), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (CGFloat)offset
{
return [objc_getAssociatedObject(self, "offset") floatValue];
}
- (CAShapeLayer *)realWaveLayer
{
CAShapeLayer *realWaveLayer = objc_getAssociatedObject(self, "realWaveLayer");
if (!realWaveLayer) {
realWaveLayer = [CAShapeLayer layer];
realWaveLayer.frame = [self setWaveFrame];
[self.layer addSublayer:realWaveLayer];
objc_setAssociatedObject(self, "realWaveLayer", realWaveLayer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return realWaveLayer;
}
- (CAShapeLayer *)maskWaveLayer
{
CAShapeLayer *maskWaveLayer = objc_getAssociatedObject(self, "maskWaveLayer");
if (!maskWaveLayer) {
maskWaveLayer = [CAShapeLayer layer];
maskWaveLayer.frame = [self setWaveFrame];
[self.layer addSublayer:maskWaveLayer];
objc_setAssociatedObject(self, "maskWaveLayer", maskWaveLayer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return maskWaveLayer;
}
开始浪
- (void)startWaveAnimation
{
self.timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(wave)];
[self.timer addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)wave
{
self.offset += self.waveSpeed;
CGFloat width = [self setWaveFrame].size.width;
CGFloat height = self.waveHeight;
// 真实浪
CGMutablePathRef realpath = CGPathCreateMutable();
CGPathMoveToPoint(realpath, NULL, 0, height);
CGFloat realY = 0.f;
// 遮罩浪
CGMutablePathRef maskpath = CGPathCreateMutable();
CGPathMoveToPoint(maskpath, NULL, 0, height);
CGFloat maskY = 0.f;
for (CGFloat x = 0.f; x <= width; x++) {
realY = height * sinf(0.01 * self.waveCurvature * x + self.offset * 0.045);
CGPathAddLineToPoint(realpath, NULL, x, realY);
maskY = -realY;
CGPathAddLineToPoint(maskpath, NULL, x, maskY);
}
// 变化的中间Y值
CGFloat centerX = width * 0.5;
CGFloat centerY = height * sinf(0.01 * self.waveCurvature * centerX + self.offset * 0.045);
for (UIView *view in self.views) {
CGRect frame = view.frame;
frame.origin.y = self.bounds.size.height - frame.size.height + centerY - self.waveHeight;
view.frame = frame;
}
CGPathAddLineToPoint(realpath, NULL, width, height);
CGPathAddLineToPoint(realpath, NULL, 0, height);
CGPathCloseSubpath(realpath);
self.realWaveLayer.path = realpath;
self.realWaveLayer.fillColor = self.realWaveColor.CGColor;
CGPathRelease(realpath);
CGPathAddLineToPoint(maskpath, NULL, width, height);
CGPathAddLineToPoint(maskpath, NULL, 0, height);
CGPathCloseSubpath(maskpath);
self.maskWaveLayer.path = maskpath;
self.maskWaveLayer.fillColor = self.maskWaveColor.CGColor;
CGPathRelease(maskpath);
}
总结
其实动画原理特别简单,主要是设计思维很赞,类似的还有 Uber 启动动画,设计思路都好棒。以后再有无从下手的需求,时间充裕的情况下,自己要多想多练,不能马上百度。但是项目结束后,我能够马上优化了这部分内容,而且把继承改变成扩展类,还是给自己点个赞吧,哈哈。
Demo 请移步至我的 github 下载:https://github.com/Mayan29/MYWave.git