在最近作个一个自定义PageControl——KYAnimatedPageControl中,我实现了CALayer的形变更画以及CALayer的弹性动画,效果先过目:git
https://github.com/KittenYang/KYAnimatedPageControl
先作个提纲:github
第一个分享的主题是“如何让CALayer发生形变”,这个技术在我以前一个项目 ———— KYCuteView 中有涉及,也写了篇简短的实现原理博文。今天再举一个例子。 以前我也作过相似果冻效果的弹性动画,好比这个项目—— KYGooeyMenu。用到的核心技术是CAKeyframeAnimation,而后设置几个不一样状态的关键帧,就能初步达到这种弹性效果。可是,毕竟只有几个关键帧,并且是须要手动计算,不精确不说,动画也不够细腻,毕竟你不可能手动建立60个关键帧。因此,今天的第二个主题是 —— “如何用阻尼振动函数建立出60个关键帧”,从而实现CALayer产生相似[UIView animateWithDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion] 的弹性动画。
正文。数组
关键技术很简单:你须要用多条贝塞尔曲线 “拼” 出这个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。spa
_factor = MIN(1, MAX(0, (ABS(scrollView.contentOffset.x - self.lastContentOffset) / scrollView.frame.size.width)));3d
假设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)。rest
注意:根据滑动方向,咱们还要根据是B点移动仍是D点移动。
CGPoint pointA = CGPointMake(rectCenter.x ,self.currentRect.origin.y + extra); CGPoint pointB = CGPointMake(self.scrollDirection == ScrollDirectionLeft ? rectCenter.x + self.currentRect.size.width/2 : rectCenter.x + self.currentRect.size.width/2 + extra*2 ,rectCenter.y); CGPoint pointC = CGPointMake(rectCenter.x ,rectCenter.y + self.currentRect.size.height/2 - extra); CGPoint pointD = CGPointMake(self.scrollDirection == ScrollDirectionLeft ? self.currentRect.origin.x - extra*2 : self.currentRect.origin.x, rectCenter.y);
而后是控制点:
关键是要知道上图中A-C1 、B-C二、B-C三、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 * 60; NSMutableArray *values = [NSMutableArray arrayWithCapacity:numOfPoints]; for (NSInteger i = 0; i < numOfPoints; i++) { [values addObject:@(0.0)]; } //差值 CGFloat d_value = [toValue floatValue] - [fromValue floatValue]; for (NSInteger point = 0; 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:3 fromValue:@(1) toValue:@(0)]; self.factor = 0; [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底层就有个定时器,而定时器的做用就是能够产生变量,时间就是变量,就能够产生变化的输入,就能看到变化的状态,连起来就是动画了。