动画特效十七:粘性动画

来源:互联网 发布:淘宝登录名可以修改吗 编辑:程序博客网 时间:2024/05/21 12:46

注:本文内容是学习自 KittenYang  的《A GUIDE TO IOS ANIMATION》一书,如果大家感兴趣的话,可以购买本书阅读。但就个人而言,觉得作者的确思维清晰,但语言表达上面忽略了很多细节方面的说明,代码书写方面有许多需要优化及更正的地方。

粘性动画效果图如下:


其实这里的动画效果实现起来还是比较复杂的。我只进行分析,并讲解粘性小球的部分。

思路分析

1. 我们可以自定义一个View来操作这个整体的效果。

2. 这个View上面有两层

2.1  后面的灰色的背景与红色的进度条,关于在动画执行过程中,绘制具体的线条,我已经在 CALayer的needsDisplayForKey方法使用说明 进行了详细的说明。

2.2  上面滚动的正方形或者滚动的小球(这一节主要讲解怎么实现上面滚动的小球)。

局部分析小球的形变效果,效果图如下(就是上图拖拽过程中小球的拉伸效果):



代码分析

1. 主控制器代码如下:

- (void)viewDidLoad {    [super viewDidLoad];        [self.slider addTarget:self action:@selector(sliderChanged:) forControlEvents:UIControlEventValueChanged];        CGRect frame = CGRectMake((self.view.frame.size.width - kRoundWidth) * 0.5, (self.view.frame.size.height - kRoundWidth) * 0.5, kRoundWidth, kRoundWidth);    self.roundView = [[LFRoundView alloc] initWithFrame:frame];    self.roundView.backgroundColor = [UIColor yellowColor];    [self.view addSubview:self.roundView];        // first load    self.roundView.roundLayer.progress = self.slider.value;}- (void)sliderChanged:(UISlider *)slider {    self.roundView.roundLayer.progress = slider.value;}

代码中的roundView就是黄色背景的View,是小球运动的载体。

2. LFRoundView的代码如下:

@class LFRoundLayer;@interface LFRoundView : UIView@property (nonatomic, strong) LFRoundLayer *roundLayer;@end@implementation LFRoundView// LFRoundView只是LFRoundLayer的载体- (instancetype)initWithFrame:(CGRect)frame {    if (self = [super initWithFrame:frame]) {        self.roundLayer = [LFRoundLayer layer];        self.roundLayer.frame = CGRectMake(0, 0, frame.size.width, frame.size.height);        self.roundLayer.contentsScale = [UIScreen mainScreen].scale;        [self.layer addSublayer:self.roundLayer];    }    return self;}@end

在roundView的Layer上面添加了roundLayer的图层,这个图层就是用来绘制上面变动的小球。

3. LFRoundLayer代码分析:

由于在主控制器中拖拽UISlider会一直促发小球的变形效果,所以LFRoundLayer应该定义一个progress属性用来接受主控制器中传递过来的信息,并根据这个信息实时绘制变动的小球。

LFRoundLayer的 .h文件定义如下:

@interface LFRoundLayer : CALayer@property (nonatomic, assign) CGFloat progress;@end

然后在 .m文件中重写progress属性,用来实时监听属性值的变化,绘制变动的小球。

- (void)setProgress:(CGFloat)progress {    _progress = progress;        // 1. Prepare Positioning Square    // 1-1) squareX 的计算    CGFloat squareX = (self.frame.size.width - kOutsideWidth) * progress;    CGFloat squareY = self.position.y - kOutsideWidth * 0.5;    CGFloat squareW = kOutsideWidth;    CGFloat squareH = kOutsideWidth;    self.SquareRect = CGRectMake(squareX, squareY, squareW, squareH);        [self setNeedsDisplay];}

需要说明的有以下几点:

1. kOutsideWidth是自定义的一个宏,它是正圆的外接正方形的边长。

#define kOutsideWidth 90

2. SquareRect 是外接正方形的frame, 虽然小球在运动过程中一直在发生变化,但是那个正方形一直保持不变,只是squareX一直在发生变化, SquareRect这个frame主要是用来定位计算,方便的计算出小球的相关位置信息,下面会做详细的介绍。

3. 调用了[self setNeedsDisplay]方法,所以在重写progress的过程中,会一直促发屏幕的重绘工作,即调用下面这个方法

- (void)drawInContext:(CGContextRef)ctx;

最后,我们在 drawInContext: 这个方法中完成小球的绘制工作。

- (void)drawInContext:(CGContextRef)ctx {    // 2. Prepare A,B,C,D    CGFloat movedDistance = kOutsideWidth / 6 * fabs(self.progress - 0.5);    self.direction = self.progress >= 0.5 ? MovedDirectionRight : MovedDirectionLeft;        CGFloat squareX = self.SquareRect.origin.x;    CGFloat squareY = self.SquareRect.origin.y;    CGFloat squareW = self.SquareRect.size.width;        CGPoint pointA = CGPointMake(squareX + kOutsideWidth * 0.5, squareY + movedDistance);    CGPoint pointB = CGPointMake(self.direction == MovedDirectionRight ? (squareX + squareW) : (squareX + squareW + 2 * movedDistance), self.position.y);        CGPoint pointC = CGPointMake(squareX + kOutsideWidth * 0.5, self.position.y + kOutsideWidth * 0.5 - movedDistance);    CGPoint pointD = CGPointMake(self.direction == MovedDirectionRight ? (squareX - 2 * movedDistance) : squareX, self.position.y);        // 3. Prepare C1~C8    CGPoint pointC1 = CGPointMake(pointA.x + kOffset, pointA.y);    CGPoint pointC2 = CGPointMake(pointB.x, pointB.y - kOffset);    CGPoint pointC3 = CGPointMake(pointB.x, pointB.y + kOffset);    CGPoint pointC4 = CGPointMake(pointC.x + kOffset, pointC.y);    CGPoint pointC5 = CGPointMake(pointC.x - kOffset, pointC.y);    CGPoint pointC6 = CGPointMake(pointD.x, pointD.y + kOffset);    CGPoint pointC7 = CGPointMake(pointD.x, pointD.y - kOffset);    CGPoint pointC8 = CGPointMake(pointA.x - kOffset, pointA.y);        NSArray *points = @[                        [NSValue valueWithCGPoint:pointA], [NSValue valueWithCGPoint:pointB],                        [NSValue valueWithCGPoint:pointC], [NSValue valueWithCGPoint:pointD],                        [NSValue valueWithCGPoint:pointC1], [NSValue valueWithCGPoint:pointC2],                        [NSValue valueWithCGPoint:pointC3], [NSValue valueWithCGPoint:pointC4],                        [NSValue valueWithCGPoint:pointC5], [NSValue valueWithCGPoint:pointC6],                        [NSValue valueWithCGPoint:pointC7], [NSValue valueWithCGPoint:pointC8]                        ];    // highlighted assistant points    [self assistantPointWithArray:points inContext:ctx];        // 1. Draw Positioning Square    UIBezierPath *squareBezierPath = [UIBezierPath bezierPathWithRect:self.SquareRect];    CGContextAddPath(ctx, squareBezierPath.CGPath);    CGContextSetStrokeColorWithColor(ctx, [UIColor blackColor].CGColor);    CGContextSetLineWidth(ctx, 1);    // how to use CGContextSetLineDash, refer to http://blog.csdn.net/zhangao0086/article/details/7234859    CGFloat squareDash[2] = {5, 5};    CGContextSetLineDash(ctx, 0, squareDash, 2);    CGContextStrokePath(ctx);        // 2. Draw Assistant Lines    UIBezierPath *assistantBezierPath = [UIBezierPath bezierPath];    [assistantBezierPath moveToPoint:pointA];    [assistantBezierPath addLineToPoint:pointC1];    [assistantBezierPath addLineToPoint:pointC2];    [assistantBezierPath addLineToPoint:pointB];    [assistantBezierPath addLineToPoint:pointC3];    [assistantBezierPath addLineToPoint:pointC4];    [assistantBezierPath addLineToPoint:pointC];    [assistantBezierPath addLineToPoint:pointC5];    [assistantBezierPath addLineToPoint:pointC6];    [assistantBezierPath addLineToPoint:pointD];    [assistantBezierPath addLineToPoint:pointC7];    [assistantBezierPath addLineToPoint:pointC8];    [assistantBezierPath closePath];    CGContextAddPath(ctx, assistantBezierPath.CGPath);    CGFloat lineDash[2] = {2, 2};    CGContextSetLineDash(ctx, 0, lineDash, 2);    CGContextSetStrokeColorWithColor(ctx, [UIColor blackColor].CGColor);    CGContextStrokePath(ctx);        // 3. Draw Entity    UIBezierPath *ovalBezierPath = [UIBezierPath bezierPath];    [ovalBezierPath moveToPoint:pointA];    [ovalBezierPath addCurveToPoint:pointB controlPoint1:pointC1 controlPoint2:pointC2];    [ovalBezierPath addCurveToPoint:pointC controlPoint1:pointC3 controlPoint2:pointC4];    [ovalBezierPath addCurveToPoint:pointD controlPoint1:pointC5 controlPoint2:pointC6];    [ovalBezierPath addCurveToPoint:pointA controlPoint1:pointC7 controlPoint2:pointC8];    [ovalBezierPath closePath];    CGContextAddPath(ctx, ovalBezierPath.CGPath);    CGContextSetStrokeColorWithColor(ctx, [UIColor blackColor].CGColor);    CGContextSetFillColorWithColor(ctx, [UIColor redColor].CGColor);    CGContextSetLineDash(ctx, 0, NULL, 0);    CGContextDrawPath(ctx, kCGPathFillStroke);}- (void)assistantPointWithArray:(NSArray *)points inContext:(CGContextRef)ctx {    CGFloat rectWidth = 4;    CGContextSetFillColorWithColor(ctx, [UIColor greenColor].CGColor);    for (NSValue *pointValue in points) {        CGPoint point = pointValue.CGPointValue;        CGContextFillRect(ctx, CGRectMake(point.x - rectWidth * 0.5, point.y - rectWidth * 0.5, rectWidth, rectWidth));    }}

由于代码量有点多,我会详细的说明相关代码:

1. movedDistance这个值的分析如下图



2. 为了标记小球是在向左移动还是在向右移动,定义一个枚举来标示:

typedef enum {    MovedDirectionLeft,    MovedDirectionRight}MovedDirection;

3. 后面的一系列代码就是来绘制相关点(A,B,C,D, C1~C8)

先看下面这张关于各个点位置分布介绍图:


仔细观察Demo中gif小球的运动,在运动过程中,线段AC1的长度始终是保持不变的,变化的只是B点和D点的位置。所以,在小球运动过程中,我们可以选择特殊的位置来计算AC1的大小,而这个特殊位置就是self.progress为0.5的时候,即小球处于圆形的时刻。

我们设置A点作为起始点,B点作为终止点,C1和C2作为定位点,然后利用贝塞尔曲线进行计算。关于详细的计算过程,见上图的分析。

4. 在计算得到所有点的坐标信息后,就可以绘制出小球及相关辅助线等信息了。

扩充,关于贝塞尔曲线的知识

Bézier curve(贝塞尔曲线)是应用于二维图形应用程序的数学曲线。 曲线定义:起始点、终止点(也称锚点)、控制点。通过调整控制点,贝塞尔曲线的形状会发生变化。 1962年,法国数学家Pierre Bézier第一个研究了这种矢量绘制曲线的方法,并给出了详细的计算公式,因此按照这样的公式绘制出来的曲线就用他的姓氏来命名,称为贝塞尔曲线。

以下公式中:B(t)t时间下 点的坐标;

 P0为起点,Pn为终点,Pi为控制点

一阶贝塞尔曲线(线段)

意义:由 P0 至 P1 的连续点, 描述的一条线段


二阶贝塞尔曲线(抛物线)

原理:由 P0 至 P1 的连续点 Q0,描述一条线段。 
      由 P1 至 P2 的连续点 Q1,描述一条线段。 
      由 Q0 至 Q1 的连续点 B(t),描述一条二次贝塞尔曲线。

经验:P1-P0为曲线在P0处的切线。

 

三阶贝塞尔曲线:


0 0
原创粉丝点击