原文地址:Drip Into A River · Part I
原文地址效果要更好些,由于简书不支持LaTeX,所以部分行内文字未使用数学符号,请谅解
Sweet iOS Animation Plan
给自己制定了一个很长远的动效学习计划,Sweet-iOS-Animation。其实就是促进喜欢写动效的同学,多多实现dribbble上的优秀作品。我也会定期更新原型图作品表单,收集优秀的原型图。如果大家对此学习计划也有兴趣,可以从这里查看内容。
以上是对于学习计划的简介,下面开始这次动效分析。
“水滴汇集”下载按钮概念图
在dribbble上发现一个创意很好的下载按钮的创意,首先要感谢原型图作者SamuraiChen分享原型图Animation on a concept draft downloads。我将这个特效起名为Drip Into a River【水滴汇集】。以下是原型图效果:

动画拆解
这个动画我们主要关注的是水滴汇集部分,和下载完成后按钮的震动波扩散两个部分。先来说说水滴汇集动画。
一、水滴汇集
- 内部的下载状态实心圆要逐渐增大;
- 外部水滴要以一个弧度飞入内部圆的圆心,并且接触时最好有粘黏效果;
- 外部水滴的起始位置大概分布在内部圆最大轮廓线环形半径的2倍左右;
- 外部水滴在飞行时,会有忽大忽小的效果。
二、震动波扩散
- 整体按钮先略微缩小,准备突然增大并抖动;
- 按钮变大,并带有弹簧效果,最终回复原大小;
- 抖动最猛烈时,向四周发散“振动波”效果。
我从中再次抽取主要的一些效果,进行实现。在制作的过程中,我遇到了很多问题,在本文最后部分进行讨论。
利用贝塞尔曲线绘制水滴路径
我绘制了一张静态描述图如下:
在图中,我们在最外围有一个有圆环,被称为水滴生成区。我们所有的水滴,为了保证在汇集飞行时候弯曲轨迹,所以OC的距离(即水滴生成区圆环半径),我们需要保证。在OC上,取中点A,并且选取长度为h的距离,确定B点,得到BA = h。这里的h距离需要我们去尝试,B点也就是轨迹曲线的Control Poiont。
在我们写代码的时候,我们会已知圆心O(x1, y1),以及水滴圆心C(x2, y2),以及一些距离OC = radius · k(这里的k,我定义为主半径的倍数系数,只用于描述外围半径和内部圆的一个半径关系)。我们设待求点坐标为B(x, y),对以上问题进行建笛卡尔坐标系求解。
当然这之中,我们需要使用欧几里得距离法,将d求出。
在推导公式后,也许你会发觉一个问题:水滴飞行轨迹在确定起始点后,可能会有两种。是的,这种想法是正确的,因为这两种贝塞尔轨迹是关于OC对称的。在图中,我也给出了另外一种轨迹的Congrol Point位置。其实,我们发现,我们仅需要修改h的符号,就是另外一种轨迹的计算方法。
我在实例代码中,建立了一个DGDownloaderMath.m
文件,这里就是存储在绘制贝塞尔曲线时,所用到的公式算法。下面代码片段是关于以上水滴飞行贝塞尔轨迹的实现方式:
1 2 3 4 5 6 7 8 9 10 11 12 |
+ (CGPoint)calcControlPoint: (CGPoint)O1 and: (CGPoint)O2 random: (bool)isRandom { CGPoint O_centre = CGPointMake((O1.x + O2.x) / 2.f, (O1.y + O2.y) / 2.f); CGFloat d = [self calcDistance: O_centre to: O1]; CGFloat k = d / 40.f; if (isRandom) { int isRandom_int = arc4random() % 2; if (isRandom_int) k = -k; } CGFloat new_x = (O1.y - O2.y) / 2.f / k + (O1.x + O2.x) / 2.f; CGFloat new_y = - ((O1.x - O2.x) / 2.f / k - (O1.y + O2.y) / 2.f); return CGPointMake(new_x, new_y); } |
为了生成不同轨迹效果的样式,我使用了方法arc4random()
来生成一个二进制随机数。剩下计算方法,严格遵循以上推导公式。
之后,我们要在视图层为水滴制作layer样式、设计动画并提供触发动画的入口方法。
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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
#pragma mark - 动画开启入口 - (void) startAnimation { if (self.gameTime != nil) { [self stopAnimation]; } self.gameTime = [CADisplayLink displayLinkWithTarget: self selector: @selector(refreshAnimation)]; [self.gameTime addToRunLoop: [NSRunLoop currentRunLoop] forMode: NSRunLoopCommonModes]; } #pragma mark - 动画关闭方法 - (void) stopAnimation { // 坑点! // 安全释放DisplayLink切记要赋值nil [self.gameTime invalidate]; self.gameTime = nil; } #pragma mark - 刷新动画,判断是否需要增加动点 - (void) refreshAnimation { if (!self.isProgressing) { [self stopAnimation]; } // 这里是CADisplayLink触发函数 // count属性为一个频率计数器 // 并且最大值为49,我可以通过控制周期最大数值, // 从而影响生成新小球的生成频率 _count ++; _count %= 50; if (_count == 40) { [self readyPointAnimation: [DGDownloaderMath calcBeginPoint: self.circlePoint radius: self.circleRadius coefficent: 2.0f]]; } } #pragma mark - 进入动画,传入起始坐标点 - (void) readyPointAnimation: (CGPoint) center { CGFloat pointRadius = 8.f; CALayer *shape = [[CALayer alloc] init]; shape.backgroundColor = DGDownloaderButtonDefaultColor.CGColor; shape.cornerRadius = pointRadius; shape.frame = CGRectMake(center.x, center.y, pointRadius * 2, pointRadius * 2); [self.layer addSublayer: shape]; [self runPointAnimation: shape]; } #pragma mark - 启动动画,向中心吸收 - (void) runPointAnimation: (CALayer *)point { CAKeyframeAnimation *keyAnimation = [CAKeyframeAnimation animationWithKeyPath: @"position"]; keyAnimation.path = [self makePointPath: point].CGPath; keyAnimation.fillMode = kCAFillModeForwards; keyAnimation.timingFunction = [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseIn]; keyAnimation.duration = 2; keyAnimation.removedOnCompletion = NO; [point addAnimation: keyAnimation forKey: @"moveAnimation"]; // 这里使用GCD,是为了将水滴的layer删去 // 因为在fillMode属性为kCAFillModeForwards // removedOnCompletion为NO的时候 // 水滴在动画结束后会保持在圆心的状态 // 我们需要使用其他方法来将其删除 // 否则会在中心进度刚开始时,影响效果 double delay = 2; dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)); dispatch_after(popTime, dispatch_get_main_queue(), ^{ [point removeFromSuperlayer]; }); } #pragma mark - 生成曲线路径 - (UIBezierPath *) makePointPath: (CALayer *)point { UIBezierPath *path = [UIBezierPath bezierPath]; [path moveToPoint: point.position]; [path addQuadCurveToPoint: self.circlePoint controlPoint: [DGDownloaderMath calcControlPoint: self.circlePoint and: point.position random: YES]]; return path; } |