谈谈iOS中粘性动画以及果冻效果的实现

  • 格式:docx
  • 大小:759.93 KB
  • 文档页数:9

谈谈iOS中粘性动画以及果冻效果的实现在最近做个一个自定义PageControl——KYAnimatedPageControl中,我实现了CALayer 的形变动画以及CALayer的弹性动画,效果先过目:先做个提纲:第一个分享的主题是“如何让CALayer发生形变”,这个技术在我之前一个项目————KYCuteView 中有涉及,也写了篇简短的实现原理博文。

今天再举一个例子。

之前我也做过类似果冻效果的弹性动画,比如这个项目—— KYGooeyMenu。

用到的核心技术是CAKeyframeAnimation,然后设置几个不同状态的关键帧,就能初步达到这种弹性效果。

但是,毕竟只有几个关键帧,而且是需要手动计算,不精确不说,动画也不够细腻,毕竟你不可能手动创建60个关键帧。

所以,今天的第二个主题是——“如何用阻尼振动函数创建出60个关键帧”,从而实现CALayer产生类似[UIViewanimateWithDuration:delay:usingSpringWithDamping:initialSpringVelocity:o 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 - stContentOffset) / 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)toValueusingSpringWithDamping:(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 * dampingFactorinitialSpringVelocity:velocity * velocityFactor duration:duration];4.anim.values = values;5.anim.duration = duration;6.return anim;7.}另一个关键以上,我们创建了 CAKeyframeAnimation 。