粘性动画以及果冻效果

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

先做个提纲:

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

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

_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点移动。

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

然后是控制点:

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

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

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

 CGPoint c1 = CGPointMake(pointA.x + offset, pointA.y);
 CGPoint c2 = CGPointMake(pointB.x, pointB.y - offset);
 CGPoint c3 = CGPointMake(pointB.x, pointB.y + offset);
 CGPoint c4 = CGPointMake(pointC.x + offset, pointC.y);
 CGPoint c5 = CGPointMake(pointC.x - offset, pointC.y);
 CGPoint c6 = CGPointMake(pointD.x, pointD.y + offset);
 CGPoint c7 = CGPointMake(pointD.x, pointD.y - offset);
 CGPoint c8 = CGPointMake(pointA.x - offset, pointA.y);

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

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

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

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

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

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

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

+ (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个数值:

 +(NSMutableArray *) animationValues:(id)fromValue toValue:(id)toValue usingSpringWithDamping:(CGFloat)damping initialSpringVelocity:(CGFloat)velocity duration:(CGFloat)duration{
 //60个关键帧
 NSInteger numOfPoints  = duration * ;
 NSMutableArray *values = [NSMutableArray arrayWithCapacity:numOfPoints];
 ; i < numOfPoints; i++) {
 [values addObject:@(0.0)];
 }
 //差值
 CGFloat d_value = [toValue floatValue] - [fromValue floatValue];
 ; pointCGFloat x = (CGFloat)point / (CGFloat)numOfPoints;
 CGFloat value = [toValue floatValue] - d_value * (pow(M_E, -damping * x) * cos(velocity * x)); //1 y = 1-e^{-5x} * cos(30x)
 values[point] = @(value);
 }
 return values;
    }

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

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

另一个关键

以上,我们创建了 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:

 CAKeyframeAnimation *anim = [KYSpringLayerAnimation createSpring: fromValue:@() toValue:@()];
 self.factor = ;
 [self addAnimation:anim forKey:@"restoreAnimation"];

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

 +(BOOL)needsDisplayForKey:(NSString *)key{
 if ([key isEqual:@"factor"]) {
 return  YES;
 }
 return  [super needsDisplayForKey:key];
 }

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

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

 -(id)initWithLayer:(GooeyCircle *)layer{
 self = [super initWithLayer:layer];
 if (self) {
 self.indicatorSize  = layer.indicatorSize;
 self.indicatorColor = layer.indicatorColor;
 self.currentRect = layer.currentRect;
 self.lastContentOffset = layer.lastContentOffset;
 self.scrollDirection = layer.scrollDirection;
 self.factor = layer.factor;
 }
 return self;
 }

总结:

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

 

iOS开发——图形编程OC篇&粘性动画以及果冻效果的更多相关文章

  1. iOS开发——图形编程OC篇&OpenGL ES2.0编程步骤

    OpenGL ES2.0编程步骤 OpenGL ES (OpenGL for Embedded Systems) 是 OpenGL 三维图形 API 的子集,针对手机.PDA和游戏主机等嵌入式设备而设 ...

  2. iOS开发——图形编程Swift篇&CAShapeLayer实现圆形图片加载动画

    CAShapeLayer实现圆形图片加载动画 几个星期之前,Michael Villar在Motion试验中创建一个非常有趣的加载动画. 下面的GIF图片展示这个加载动画,它将一个圆形进度指示器和圆形 ...

  3. iOS开发——网络编程OC篇&Socket编程

    Socket编程 一.网络各个协议:TCP/IP.SOCKET.HTTP等 网络七层由下往上分别为物理层.数据链路层.网络层.传输层.会话层.表示层和应用层. 其中物理层.数据链路层和网络层通常被称作 ...

  4. iOS开发——网络编程OC篇&使用WebView构建HyBird应用

    使用WebView构建HyBird应用 HyBird是一种本地技术与Web相结合,能过实现跨平台的移动应用开发,最常用的一个框架:PhoneGap 一:首先,写好html代码 <!DOCTYPE ...

  5. iOS开发——网络编程OC篇&(一)XMPP简单介绍与准备

    XMPP简单介绍与准备 一.即时通讯简单介绍 1.简单说明 即时通讯技术(IM)支持用户在线实时交谈.如果要发送一条信息,用户需要打开一个小窗口,以便让用户及其朋友在其中输入信息并让交谈双方都看到交谈 ...

  6. iOS开发——网络编程OC篇&GCDAsyncSocket编程

    GCDAsyncSocket编程 同上一篇文章一样,这里也是使用Socket实现一个聊天室,但是这里使用的是一个常用的框架实现的:GCDAsyncSocket 一:导入这个框架 二:声明这个Socke ...

  7. iOS开发——网络编程OC篇&(二)XMPP实现用户登录与注销

    XMPP实现用户登录与注销 登录: 步骤: * 在AppDelegate实现登录 1. 初始化XMPPStream 2. 连接到服务器[传一个JID] 3. 连接到服务成功后,再发送密码授权 4. 授 ...

  8. ios开发——实用技术篇OC篇&iOS的主要框架

    iOS的主要框架         阅读目录 Foundation框架为所有的应用程序提供基本系统服务 UIKit框架提供创建基于触摸用户界面的类 Core Data框架管着理应用程序数据模型 Core ...

  9. iOS开发——网络实用技术OC篇&网络爬虫-使用青花瓷抓取网络数据

    网络爬虫-使用青花瓷抓取网络数据 由于最近在研究网络爬虫相关技术,刚好看到一篇的的搬了过来! 望谅解..... 写本文的契机主要是前段时间有次用青花瓷抓包有一步忘了,在网上查了半天也没找到写的完整的教 ...

随机推荐

  1. hdu 4081 Qin Shi Huang's National Road System(最小生成树+dp)2011 Asia Beijing Regional Contest

    同样是看别人题解才明白的 题目大意—— 话说秦始皇统一六国之后,打算修路.他要用n-1条路,将n个城市连接起来,并且使这n-1条路的距离之和最短.最小生成树是不是?不对,还有呢.接着,一个自称徐福的游 ...

  2. [Papers]NSE, $u_3$, Lebesgue space [Cao-Titi, IUMJ, 2008]

    $$\bex u_3\in L^p(0,T;L^q(\bbR^3)),\quad \frac{2}{p}+\frac{3}{q}=\frac{2}{3}+\frac{2}{3q},\quad \fra ...

  3. Linux 系统编程

    简介和主要概念 Linux 系统编程最突出的特点是要求系统程序员对它们工作的的系统的硬件和操作系统有深入和全面的了解,当然它们还有库和系统调用上的区别. 系统编程分为:驱动编程.用户空间编程和网络编程 ...

  4. 简易nagios安装

    这段时间一直在进行nagios安装的实验,进行了很多的实验,现在也就是将这些进行一些基础的记录. 本篇主要讲述的是进行nagios的简易安装,在安装完成之后,能够在web页面上看到本地的监控图像, n ...

  5. 【暑假】[实用数据结构]UVa11991 Easy Problem from Rujia Liu?

    UVa11991 Easy Problem from Rujia Liu?  思路:  构造数组data,使满足data[v][k]为第k个v的下标.因为不是每一个整数都会出现因此用到map,又因为每 ...

  6. (转载)OC学习篇之---类的三大特性:封装,继承,多态

    之前的一片文章介绍了OC中类的初始化方法和点语法的使用,今天来继续学习OC中的类的三大特性,我们在学习Java的时候都知道,类有三大特性:继承,封装,多态,这个也是介绍类的时候,必须提到的话题,那么今 ...

  7. poj1000 A+B Problem

    Description Calculate a+b Input Two integer a,b (0<=a,b<=10) Output Output a+b Sample Input 1 ...

  8. jdk+jira配置

    1.JDK.JIRA.MySQL安装完毕,停止JIRA服务 创建数据库:mysqlcreate database jiradb character set ‘UTF8′; 创建用户并赋与权限:crea ...

  9. mysql performance_schema 初探

    mysql  performance_schema 初探: mysql 5.5 版本 新增了一个性能优化的引擎: PERFORMANCE_SCHEMA 这个功能默认是关闭的: 需要设置参数: perf ...

  10. ProtoBuffer 简单例子

    最近学了一下protobuf,写了一个简单的例子,如下: person.proto文件 message Person{ required string name = 1; required int32 ...