文 / 杨骑滔

在最近做个一个自定义PageControl——KYAnimatedPageControl中,我实现了CALayer的形变动画以及CALayer的弹性动画,效果先过目:

先做个提纲:

第一个分享的主题是“如何让CALayer发生形变”,这个技术在我之前一个项目 ———— KYCuteView 中有涉及,也写了篇简短的实现原理博文。今天再举一个例子。

之前我也做过类似果冻效果的弹性动画,比如这个项目—— KYGooeyMenu。用到的核心技术是CAKeyframeAnimation,然后设置几个不同状态的关键帧,就能初步达到这种弹性效果。但是,毕竟只有几个关键帧,而且是需要手动计算,不精确不说,动画也不够细腻,毕竟你不可能手动创建60个关键帧。所以,今天的第二个主题是 —— “如何用阻尼振动函数创建出60个关键帧”,从而实现CALayer产生类似[UIView animateWithDuration:delay:usingSpringWithDamping:initialSpringVelocity:ptions:animations:completion] 的弹性动画。

正文。

如何让CALayer发生形变?

关键技术很简单:你需要用多条贝塞尔曲线 “拼” 出这个Layer。之所以这样做的原因不言而喻,因为这样方便我们发生形变。

比如 KYAnimatedPageControl 中的这个小球,其实它是这么被画出来的:

小球是由弧AB、弧BC、弧CD、弧DA 四段组成,其中每段弧都绑定两个控制点:弧AB 绑定的是 C1 、 C2;弧BC 绑定的是 C3 、 C4 .....

如何表达各个点?

首先,A、B、C、D是四个动点,控制他们动的变量是ScrollView的contentOffset.x。我们可以在-(void)scrollViewDidScroll:UIScrollView *)scrollView中实时获取这个变量,并把它转换成一个控制在 0~1 的系数,取名为factor。

  1. _factor = MIN(1, MAX(0, (ABS(scrollView.contentOffset.x - self.lastContentOffset) / scrollView.frame.size.width)));

复制代码

假设A、B、C、D的最大变化距离为小球直径的2/5。那么结合这个0~1的系数,我们可以得出A、B、C、D的真实变化距离 extra 为:extra = (self.width * 2 / 5) * factor。当factor == 1时,达到最大形变状态,此时四个点的变化距离均为(self.width * 2 / 5)。

注意:根据滑动方向,我们还要根据是B点移动还是D点移动。

  1. CGPoint pointA = CGPointMake(rectCenter.x ,self.currentRect.origin.y + extra);
  2. CGPoint pointB = CGPointMake(self.scrollDirection == ScrollDirectionLeft ? rectCenter.x + self.currentRect.size.width/2 : rectCenter.x + self.currentRect.size.width/2 + extra*2 ,rectCenter.y);
  3. CGPoint pointC = CGPointMake(rectCenter.x ,rectCenter.y + self.currentRect.size.height/2 - extra);
  4. CGPoint pointD = CGPointMake(self.scrollDirection == ScrollDirectionLeft ? self.currentRect.origin.x - extra*2 : self.currentRect.origin.x, rectCenter.y);

复制代码

然后是控制点:

关键是要知道上图中A-C1 、B-C2、B-C3、C-C4....这些水平和垂直虚线的长度,命名为offSet。经过多次尝试,我得出的结论是:

当offSet设置为 直径除以3.6 的时候,弧线能完美地贴合成圆弧。我隐约感觉这个 3.6 是必然,貌似和360度有某种关系,或许通过演算能得出 3.6 这个值的必然性,但我没有尝试。

因此,各个控制点的坐标:

  1. CGPoint c1 = CGPointMake(pointA.x + offset, pointA.y);
  2. CGPoint c2 = CGPointMake(pointB.x, pointB.y - offset);
  3. CGPoint c3 = CGPointMake(pointB.x, pointB.y + offset);
  4. CGPoint c4 = CGPointMake(pointC.x + offset, pointC.y);
  5. CGPoint c5 = CGPointMake(pointC.x - offset, pointC.y);
  6. CGPoint c6 = CGPointMake(pointD.x, pointD.y + offset);
  7. CGPoint c7 = CGPointMake(pointD.x, pointD.y - offset);
  8. CGPoint c8 = CGPointMake(pointA.x - offset, pointA.y);

复制代码

有了终点和控制点,就可以用UIBezierPath 中提供的方法 - (void)addCurveToPoint:CGPoint)endPoint controlPoint1:CGPoint)controlPoint1 controlPoint2:CGPoint)controlPoint2; 画线段了。

重载CALayer的- (void)drawInContext:CGContextRef)ctx;方法,在里面画图案:

  1. - (void)drawInContext:(CGContextRef)ctx{
  2. ....//在这里计算每个点的坐标
  3. UIBezierPath* ovalPath = [UIBezierPath bezierPath];
  4. [ovalPath moveToPoint: pointA];
  5. [ovalPath addCurveToPoint:pointB controlPoint1:c1 controlPoint2:c2];
  6. [ovalPath addCurveToPoint:pointC controlPoint1:c3 controlPoint2:c4];
  7. [ovalPath addCurveToPoint:pointD controlPoint1:c5 controlPoint2:c6];
  8. [ovalPath addCurveToPoint:pointA controlPoint1:c7 controlPoint2:c8];
  9. [ovalPath closePath];
  10. CGContextAddPath(ctx, ovalPath.CGPath);
  11. CGContextSetFillColorWithColor(ctx, self.indicatorColor.CGColor);
  12. CGContextFillPath(ctx);
  13. }

复制代码

现在,当你滑动ScrollView的时候,小球就会形变了。

如何用阻尼振动函数创建出60个关键帧?

上面的例子中,有个很重要的因素,就是ScrollView中的contentOffset.x这个变量,没有这个输入,那接下来什么都不会发生。但想要获得这个变量,是需要用户触摸、滑动去交互产生的。在某个动画中用户是没有直接的交互输入的,比如当手指离开之后,二手手机拍卖要让这个小球以果冻效果弹回初始状态,这个过程手指已经离开屏幕,也就没有了输入,那么用上面的方法肯定行不通,所以,我们可以用CAAnimation.

我们知道,iOS7中苹果在 UIView(UIViewAnimationWithBlocks) 加入了一个新的制作弹性动画的工厂方法:

  1. + (void)animateWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay usingSpringWithDamping:(CGFloat)dampingRatio initialSpringVelocity:(CGFloat)velocity options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion NS_AVAILABLE_IOS(7_0);

复制代码

但是没有直接的关于弹性的 CAAnimation 子类,类似CABasicAnimation或CAKeyframeAnimation 来直接给CALayer添加动画。好消息是iOS9中添加了公开的 CASpringAnimation。但是出于兼容低版本以及对知识探求的角度,我们可以了解一下如何手动给CALayer创建一个弹性动画。

在开始之前需要复习一下高中物理知识 ———— 阻尼振动,你可以点击高亮字体的链接稍微复习一下。

根据维基百科,我们可以得到如下振动函数通式:

当然这只是一个通式,我们需要让 图像过(0,0),并且最后衰减到1 。我们可以让原图像先绕X轴翻转180度,也就是加一个负号。然后沿y轴向上平移一个单位。所以稍加变形可以得到如下函数:

想看函数的图像?没问题,推荐一个在线查看函数图象的网站 —— Desmos ,把这段公式 1-\left(e^{-5x}\cdot \cos (30x)\right) 复制粘帖进去就可以看到图像。

改进后的函数图像是这样的:

完美满足了我们 图形过(0,0),震荡衰减到1 的要求。其中式子中的 5 相当于阻尼系数,数值越小幅度越大;式子中的 30 相当于震荡频率 ,数值越大震荡次数越多。

接下来就需要转换成代码。

总体思路是创建60帧关键帧(因为屏幕的最高刷新频率就是60FPS),然后把这60帧数据赋值给 CAKeyframeAnimation 的 values 属性。

用以下代码生成60帧后保存到一个数组并返回它,其中//1就是利用刚才的公式创建60个数值:

  1. +(NSMutableArray *) animationValues:(id)fromValue toValue:(id)toValue usingSpringWithDamping:(CGFloat)damping initialSpringVelocity:(CGFloat)velocity duration:(CGFloat)duration{
  2. //60个关键帧
  3. NSInteger numOfPoints = duration * 60;
  4. NSMutableArray *values = [NSMutableArray arrayWithCapacity:numOfPoints];
  5. for (NSInteger i = 0; i < numOfPoints; i++) {
  6. [values addObject:@(0.0)];
  7. }
  8. //差值
  9. CGFloat d_value = [toValue floatValue] - [fromValue floatValue];
  10. for (NSInteger point = 0; point CGFloat x = (CGFloat)point / (CGFloat)numOfPoints;
  11. CGFloat value = [toValue floatValue] - d_value * (pow(M_E, -damping * x) * cos(velocity * x)); //1 y = 1-e^{-5x} * cos(30x)
  12. values[point] = @(value);
  13. }
  14. return values;
  15. }

复制代码

接下来创建一个对外的类方法,并返回一个 CAKeyframeAnimation :

  1. +(CAKeyframeAnimation *)createSpring:(NSString *)keypath duration:(CFTimeInterval)duration usingSpringWithDamping:(CGFloat)damping initialSpringVelocity:(CGFloat)velocity fromValue:(id)fromValue toValue:(id)toValue{
  2. CAKeyframeAnimation *anim = [CAKeyframeAnimation animationWithKeyPath:keypath];
  3. NSMutableArray *values = [KYSpringLayerAnimation animationValues:fromValue toValue:toValue usingSpringWithDamping:damping * dampingFactor initialSpringVelocity:velocity * velocityFactor duration:duration];
  4. anim.values = values;
  5. anim.duration = duration;
  6. return anim;
  7. }

复制代码

另一个关键

以上,我们创建了 CAKeyframeAnimation 。但是这些values到底是对谁起作用的呢?如果你熟悉CoreAnimation的话,没错,是对传入的keypath起作用。而这些keypath其实就是CALayer中的属性@property。比如,之所以当传入的keypath为transform.rotation.x时CAKeyframeAnimation会让layer发生旋转,就是因为CAKeyframeAnimation发现CALayer中有这么个属性叫transform,于是动画就发生了。现在我们需要改变的是主题一中的那个factor变量,所以,很自然地想到,我们可以给CALayer补充一个属性名为factor就行了,这样CAKeyframeAnimation加到layer上时发现layer有这个factor属性,就会把60帧不同的values赋值给factor。当然我们要把fromValue和toValue控制在0~1:

  1. CAKeyframeAnimation *anim = [KYSpringLayerAnimation createSpring:@"factor" duration:0.8 usingSpringWithDamping:0.5 initialSpringVelocity:3 fromValue:@(1) toValue:@(0)];
  2. self.factor = 0;
  3. [self addAnimation:anim forKey:@"restoreAnimation"];

复制代码

最后一步,虽然CAKeyframeAnimation实时地去改变了我们想要的factor,但我们还得通知屏幕刷新,这样才能看到动画。

  1. +(BOOL)needsDisplayForKey:(NSString *)key{
  2. if ([key isEqual:@"factor"]) {
  3. return YES;
  4. }
  5. return [super needsDisplayForKey:key];
  6. }

复制代码

上面的代码通知屏幕当factor发生变化时,实时刷新屏幕。

最后的最后,你需要重载CALayer中的-(id)initWithLayer:GooeyCircle *)layer方法,为了保证动画能连贯起来,你需要拷贝前一个状态的layer及其所有属性。

  1. -(id)initWithLayer:(GooeyCircle *)layer{
  2. self = [super initWithLayer:layer];
  3. if (self) {
  4. self.indicatorSize = layer.indicatorSize;
  5. self.indicatorColor = layer.indicatorColor;
  6. self.currentRect = layer.currentRect;
  7. self.lastContentOffset = layer.lastContentOffset;
  8. self.scrollDirection = layer.scrollDirection;
  9. self.factor = layer.factor;
  10. }
  11. return self;
  12. }

复制代码

总结:

做自定义的动画最关键的就是要有变量,要有输入。像滑动ScrollView的时候,滑动的距离就是动画的输入,可以作为动画的变量;当没有交互的时候,可以用CAAnimation。其实CAAnimation底层就有个定时器,而定时器的作用就是可以产生变量,时间就是变量,就可以产生变化的输入,就能看到变化的状态,连起来就是动画了。

谈谈iOS中粘性动画以及果冻效果的实现相关推荐

  1. iOS中 Animation 动画大全 韩俊强的博客

    每日更新关注:http://weibo.com/hanjunqiang  新浪微博! iOS开发者交流QQ群: 446310206 1.iOS中我们能看到的控件都是UIView的子类,比如UIButt ...

  2. 如何让IOS中的文本实现3D效果

    本转载至 http://bbs.aliyun.com/read/181991.html?spm=5176.7114037.1996646101.25.p0So7c&pos=9     zhed ...

  3. 谈谈 iOS 中图片的解压缩

    对于大多数 iOS 应用来说,图片往往是最占用手机内存的资源之一,同时也是不可或缺的组成部分.将一张图片从磁盘中加载出来,并最终显示到屏幕上,中间其实经过了一系列复杂的处理过程,其中就包括了对图片的解 ...

  4. 谈谈iOS中图片的解压缩

    原文 对于大多数 iOS 应用来说,图片往往是最占用手机内存的资源之一,同时也是不可或缺的组成部分.将一张图片从磁盘中加载出来,并最终显示到屏幕上,中间其实经过了一系列复杂的处理过程,其中就包括了对图 ...

  5. iOS中字迹动画效果

    最近自己着手一款关于中国风的app,其中需要的一个想法就是诗词可以像ppt中的一种模式:字可以一个个的显示出来.最先的想法是将诗词分成一个个字放在一个个label上面,然后添加动画将其显示出来!然后这 ...

  6. QuartzCore框架-- iOS中的动画

    iOS学习笔记之QuartzCore框架 iOS编程给用户视觉反馈其实都是通过QuartzCore框架来进行的,说白了,所有用户最终看到的显示界面都是图层合成的结果,而图层即是QuartzCore中的 ...

  7. 在ios中实现图片环绕文字效果

    IOS中有时候需要实现文字环绕图片的效果,用一般的控件很难实现,因为根本就没有支持该属性的控件,折衷的做法就是用UIWebView,在其加载html语言,实现文字的环绕效果. 实现要求:在webVie ...

  8. 山寨“饿了么”应用中添加菜品数量按钮效果

    山寨"饿了么"应用中添加菜品数量按钮效果 本人视频教程系类   iOS中CALayer的使用 最终效果: 山寨源头: 源码:(此源码解决了重用问题,可以放心的放在cell中使用) ...

  9. iOS开发 QQ粘性动画效果

    QQ(iOS)客户端的粘性动画效果 时间 2016-02-17 16:50:00  博客园精华区 原文  http://www.cnblogs.com/ziyi--caolu/p/5195615.ht ...

最新文章

  1. 通过本地IIS SMTP服务器发送邮件时提示“邮箱不可用”的解决办法
  2. 互联网医疗的前世今生与未来
  3. python图像分类代码_python实现支持向量机遥感图像分类
  4. 电脑常见问题_电脑常见问题解决方法(二)
  5. go语言结构体作为函数参数,采用的是值传递
  6. Effective Modern C++ 第一章 C++11/14/17中的类型推断
  7. bootstrap table border粗细_Web前端开发(18)——Bootstrap响应式布局
  8. 开始php脚本,php bypass disable function
  9. android fastboot模式下载以及出现的问题
  10. win7新建ios开发环境
  11. Web前端html表格制作
  12. linux 系统找回密码,Linux系统密码忘记后的五种恢复方法
  13. 【视频异常检测综述-论文阅读】Deep Video Anomaly Detection: Opportunities and Challenges
  14. HTTP超文本传输协议详解
  15. [HAOI2008]糖果传递
  16. iphone7刷入linux,iPhone7怎么进入DFU模式 iPhone7刷机步骤【详解】
  17. 技术人如何才能不焦虑
  18. 评论式软文怎么写?学会这三招,让你轻松营造出客观的感觉
  19. 创新的边界——记MAD Catz的倒闭
  20. 很火的华为太空表网站源码

热门文章

  1. misc高阶 攻防世界_攻防世界 Misc 进阶题(一)
  2. 主要技术指标简介_期货常用技术指标(五)布林线
  3. window安装mysql默认密码忘记_window10 安装Mysql 8.0.17以及忘记密码重置密码
  4. go 多线程并发 queue demo
  5. 【水滴石穿】react-native-book
  6. markdown学习记录
  7. nginx 目录讲解
  8. mysql caching_sha2_password异常分析
  9. AI应用开发实战 - 手写识别应用入门
  10. Java:清空文件内容