谈谈iOS中粘性动画以及果冻效果的实现
在最近做个一个自定义PageControl——KYAnimatedPageControl中,我实现了CALayer的形变动画以及CALayer的弹性动画,效果先过目:
https://github.com/KittenYang/KYAnimatedPageControl
先做个提纲:
第一个分享的主题是“如何让CALayer发生形变”,这个技术在我之前一个项目 ———— KYCuteView 中有涉及,也写了篇简短的实现原理博文。今天再举一个例子。
之前我也做过类似果冻效果的弹性动画,比如这个项目—— 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];
for (NSInteger i = ; i < numOfPoints; i++) {
[values addObject:@(0.0)];
} //差值
CGFloat d_value = [toValue floatValue] - [fromValue floatValue]; for (NSInteger point = ; point<numOfPoints; point++) { CGFloat 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:@"factor" duration:0.8 usingSpringWithDamping:0.5 initialSpringVelocity: 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中粘性动画以及果冻效果的实现的更多相关文章
- 转:谈谈iOS中粘性动画以及果冻效果的实现
在最近做个一个自定义PageControl——KYAnimatedPageControl中,我实现了CALayer的形变动画以及CALayer的弹性动画,效果先过目: 先做个提纲: 第一个分享的主题是 ...
- iOS开发——图形编程OC篇&粘性动画以及果冻效果
粘性动画以及果冻效果 在最近做个一个自定义PageControl——KYAnimatedPageControl中,我实现了CALayer的形变动画以及CALayer的弹性动画,效果先过目: 先做个提纲 ...
- 浅谈贝塞尔曲线以及iOS中粘性动画的实现
关于贝塞尔曲线,网上相关的文章很多,这里我主要想用更简单的方法让大家理解贝塞尔曲线,当然,这仅仅是我个人的理解,如有错误的地方还请大家能够帮忙指出来,这样大家才能一起进步. 贝塞尔曲线,常用到的可分为 ...
- iOS中的动画
iOS中的动画 Core Animation Core Animation是一组非常强大的动画处理API,使用它能做出非常绚丽的动画效果,而且往往是事半功倍,使用它需要添加QuartzCore .fr ...
- iOS中 Animation 动画大全 韩俊强的博客
每日更新关注:http://weibo.com/hanjunqiang 新浪微博! iOS开发者交流QQ群: 446310206 1.iOS中我们能看到的控件都是UIView的子类,比如UIButt ...
- iOS中的动画(转载)
iOS中的动画 最近两天没事在慢慢学习一些动画,好多东西长时间不用都给忘了,找到一篇介绍很详细的文章就粘贴了过来以备复习,原文地址:https://my.oschina.net/aofe/blog/ ...
- 如何让IOS中的文本实现3D效果
本转载至 http://bbs.aliyun.com/read/181991.html?spm=5176.7114037.1996646101.25.p0So7c&pos=9 zh ...
- CADisplayLink+弹簧动画实现果冻效果
项目中在Tabbar中间的按钮要从底部弹出视图并有果冻效果,在CocoaChina中找了一篇博客用 UIBezierPath 实现果冻效果,github,自己就按着上面的demo修改了一下( 之前也是 ...
- IOS中的动画菜单
SvpplyTable(可折叠可张开的菜单动画) 允许你简单地创建可折叠可张开的菜单动画效果,灵感来自于Svpply app.不同表格项目使用JSON定义,你可以定义每个菜单项和任何子菜单,为每个项目 ...
随机推荐
- Java线程运行轨迹-代码追踪与定位
今天在写程序时,想到一个问题,当我的程序出异常的时候,控制台输出信息中,明确指出了出现异常的位置,并详细列举了函数的调用层次关系,它是怎么做到的. 竟然想到了这个问题,就去查看了源代码,不过没点几下, ...
- Centos下安装配置LAMP(Linux+Apache+MySQL+PHP)
Centos下安装配置LAMP(Linux+Apache+MySQL+PHP) 关于LAMP的各种知识,还请大家自行百度谷歌,在这里就不详细的介绍了,今天主要是介绍一下在Centos下安装,搭建一 ...
- width:100%缩小窗口时背景图片出现空白bug
页面容器(#wrap)与页面头部(#header )为100%宽度.而内容的容器(#page)为固定宽度960px.浏览窗口缩小而小于内容层宽度时会产生宽度理解上的差异.如下图所示窗口宽度大于内容层宽 ...
- Mac电脑手动清理
Xcode的无用的文件如图找到DerivedData文件夹,删除里面文件就可以了(先找到隐藏的文件)
- 安装 mysql server
三个月前向公司申请了一台服务器单独给我用,作为部署我写的自动发布工具的服务器.同事不久前在我这台服务器上装了个数据库,今天上去查了一下,用不了,只有information_schema和test两个数 ...
- Python中的Warnings模块忽略告警信息
写了个小工具,其中涉及到从远程数据库中查询并返回,数据库是utf8编码,但是我的工具用的是GB2312编码,因此在返回数据的时候,有部分数据出现了:Truncated incorrect DECIMA ...
- 关于C#的委托
作者 陈嘉栋(慕容小匹夫) 阅读目录 0x00 前言 0x01 从观察者模式说起 0x02 向Unity3D中的SendMessage和BroadcastMessage说拜拜 0x03 认识回调函数 ...
- [BZOJ 1207] [HNOI 2004] 打鼹鼠 【DP】
题目链接:BZOJ - 1207 题目分析 每一次打鼹鼠一定是从上一次打某只鼹鼠转移过来的,从打第 j 只鼹鼠能不能转移到打第 i 只鼹鼠,算一下曼哈顿距离和时间差就知道了. 那么就有一个 DP ,用 ...
- Gray码 (格雷码) 【二进制】
以下内容是看了Matrix67的关于二进制的blog(Link)的一点总结与摘录. Gray码,中文“格雷码”,是一种特殊的编码,相邻两个格雷码的二进制表示中有且仅有一位不同,且 n 阶 Gray 码 ...
- JAVA线程示范之一种
线程有两种方法生成,这是其中的一种.. MyRunnable.java public class MyRunnable implements Runnable { public void run() ...