一、前言
在解析pop源码之前,我们先通过面向过程的方式来实现系统动画,来理解动画的本质。因为如果从面向对象的方式直接去解析,很容易就绕到对象与对象之间的关联中。
二、系统动画
假设我们需要将一个view向下移动600px,那么可以使用UIView Animation来实现
- (void)startSystemAnimation {
[UIView animateWithDuration:1.0 delay:0.f options:UIViewAnimationOptionCurveEaseIn animations:^{
self.animatedView.layer.transform = CATransform3DMakeTranslation(0, 300, 0);
} completion:nil];
}
复制代码
对于这个简单的系统动画,我们需要分别了解以下内容:
- duration:动画的执行周期,一般我们可以通过设置来控制动画的执行时间
- options: 动画的执行效果,我们可以通过选择不同的UIViewAnimationOptions来实现不同的动画效果
- transform:动画的执行对象,CATransform3D是一个用户处理3D形变的类,可以改变控件的平移、缩放、旋转等。
三、相关知识
简单来说,视图的动画就是在一定时间内,动画的属性(位置、颜色或大小)发生线性或非线性的变化。假设我们要实现上面的系统动画,那么我们要明确时间、变化效果和变化对象三者。
- 时间:动画开始时间和动画结束时间
- 变化效果:动画是匀速变化还是先加速后减速
- 变化对象:需要修改何种属性
1. CADisplayLink
CADisplayLink是一个可以让我们以和屏幕刷新率相同的频率将内容渲染到屏幕上的定时器。与NSTimer最大的区别就是时间误差小且调用时机与屏幕刷新率相同,能够保证动画的流畅性。
2. UIViewAnimationOptions
该属性定义了一系列的动画效果,具体可以参考UIViewAnimationOptions,这次我们主要探讨动画速度控制的效果。
UIViewAnimationOptionCurveEaseInOut:动画先加速、后减速[默认]。
UIViewAnimationOptionCurveEaseIn :动画开始后逐渐加速。
UIViewAnimationOptionCurveEaseOut:动画快结束时进行减速。
UIViewAnimationOptionCurveLinear :动画匀速执行,默认值。
复制代码
3. CAMediaTimingFunction
该类主要定义了动画的缓冲效果,上面UIViewAnimationOptions的四种动画效果实际上是通过CAMediaTimingFunction来实现的,具体如下:
kCAMediaTimingFunctionLinear//匀速的线性计时函数
kCAMediaTimingFunctionEaseIn//缓慢加速,然后突然停止
kCAMediaTimingFunctionEaseOut//全速开始,慢慢减速
kCAMediaTimingFunctionEaseInEaseOut//慢慢加速再慢慢减速
kCAMediaTimingFunctionDefault//也是慢慢加速再慢慢减速,但是它加速减速速度略慢
复制代码
4. CATransform3D
在使用系统动画时,我们通过CATransform3DMakeTranslation方法来修改视图的位置,而CATransform3D本身是一个矩阵,它矩阵中对应的属性如下:
struct CATransform3D{
CGFloat m11(x缩放), m12(y切变), m13(旋转), m14();
CGFloat m21(x切变), m22(y缩放), m23, m24;
CGFloat m31(旋转), m32, m33, m34(透视效果,要有旋转角度才能看出效果);
CGFloat m41(x平移), m42(y平移), m43(z平移), m44;
};
复制代码
如上所示,矩阵中对应的m42即为视图y轴方向上的平移,通过修改该属性我们就可以达到修改视图y轴方向的位置了。
四、自定义动画实现
1. 伪代码:简单来说就是计算每一小段时间对应视图的偏移,然后更新视图的偏移。
t0: 开始时间
t1: 当前时间
t2: 结束时间
t1 = currentTime();
t2 = t0 + duration;
while (t1 < t2) {
offset = caculateOffset(); //计算偏移量
obj.offset = offset;
t1 = currentTime(); //更新当前时间
}
复制代码
2.线性变化
- (void)startCustomAnimation {
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(render:)];
[displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}
static bool isStarted = NO;
static CFTimeInterval beginTime = 0;
static CFTimeInterval endTime = 0;
- (void)render:(CADisplayLink *)displayLink { //正常情况下,每秒调用60次,即每16ms调用一次
CGFloat translationY = 300;
CFTimeInterval duration = 1.0;
if (!isStarted) { //记录动画开始时间和结束时间
beginTime = CACurrentMediaTime();
endTime = CACurrentMediaTime() + duration;
isStarted = YES;
}
CFTimeInterval curTime = CACurrentMediaTime();
if (curTime < endTime) {
CFTimeInterval interval = curTime - beginTime;
CGFloat offset = [self linearWithInterval:interval duration:duration translation:translationY]; //获取当前时间对应的位置
CATransform3D transform = CATransform3DIdentity;
transform.m42 = offset; //更新视图对应的位置
self.animatedView.layer.transform = transform;
} else {
displayLink.paused = YES; //动画结束,停止计时器
[displayLink invalidate];
}
}
//线性计算视图对应的偏移:匀速变化
- (CGFloat)linearWithInterval:(CFTimeInterval)interval duration:(CFTimeInterval)duration translation:(CGFloat)transation {
return interval / duration * transation;
}
复制代码
3.非线性变化
-
CAMediaTimingFunction缓冲动画的本质是使用三次贝塞尔曲线,通过曲线来描述动画的变化速率,具体贝塞尔曲线相关内容可参考: 贝塞尔曲线
-
求解三次贝塞尔曲线方程
B(t) = (3*P1-3*P2+1)*t^3 + (3*P2-6*P1)*t^2 + 3*P1*t
我们需要根据x(时间点)计算出f(x)偏移量,那么需要先解出系数t来,系数t的求解一般采用牛顿法或二分法.下面先采用牛顿法进行求解,若没有得到合适的解,再通过二分法来求解。
- (CGFloat)curveEaseInWithInterval:(CFTimeInterval)interval duration:(CFTimeInterval)duration translation:(CGFloat)transation {
CAMediaTimingFunction *timingFunc = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
float timingControls[4];
[timingFunc getControlPointAtIndex:1 values:&timingControls[0]];
[timingFunc getControlPointAtIndex:2 values:&timingControls[2]];
CGPoint controlPoint1 = CGPointMake(timingControls[0], timingControls[1]);
CGPoint controlPoint2 = CGPointMake(timingControls[2], timingControls[3]); //获取控制点位置
CGFloat p1x = controlPoint1.x, p1y = controlPoint1.y;
CGFloat p2x = controlPoint2.x, p2y = controlPoint2.y;
CGFloat ax,bx,cx,ay,by,cy; //分别表示t,t^2和t^3前的系数
cx = 3.0 * p1x;
bx = 3.0 * (p2x - p1x) - cx;
ax = 1.0 - cx - bx;
cy = 3.0 * p1y;
by = 3.0 * (p2y - p1y) - cy;
ay = 1.0 - cy - by;
CGFloat (^sampleCurveX)(CGFloat t) = ^CGFloat (CGFloat t) { //坐标x中的三次贝塞尔曲线公式
return ((ax * t + bx) * t + cx) * t; //秦九韶算法
};
CGFloat (^sampleCurveY)(CGFloat t) = ^CGFloat (CGFloat t) { //坐标y中的三次贝塞尔曲线公式
return ((ay * t + by) * t + cy) * t;
};
CGFloat (^sampleCurveDerivativeX)(CGFloat t) = ^CGFloat (CGFloat t) { //求导函数
return (3 * ax * t + 2.0 * bx) * t + cx;
};
CGFloat (^solveCurveX)(CGFloat x, CGFloat epsilon) = ^(CGFloat x, CGFloat epsilon) { //求解公式中的因子t
CGFloat t0,t1,t2,x2,d2;
int i;
//牛顿法求解
for (t2 = x, i = 0; i < 8; i ++) {
x2 = sampleCurveX(t2) - x;
if (fabs(x2) < epsilon) {
return t2;
}
d2 = sampleCurveDerivativeX(t2);
if (fabs(d2) < 1e-6) {
break;
}
t2 = t2 - x2 / d2;
}
t0 = 0.0;
t1 = 1.0;
t2 = x;
if (t2 < t0) {
return t0;
}
if (t2 > t1) {
return t1;
}
//二分法求解
while (t0 < t1) {
x2 = sampleCurveX(t2);
if (fabs(x2 - x) < epsilon) {
return t2;
}
if (x > x2) {
t0 = t2;
} else {
t1 = t2;
}
t2 = (t1 - t0) * 0.5 + t0;
}
return t2;
};
CGFloat epsilon = 1.0 / (1000 * duration); //误差允许范围
CGFloat t = solveCurveX(interval / duration, epsilon); //计算出系数t
return sampleCurveY(t) * transation; //根据公式计算出时间点对应的偏移量,然后再乘以总偏移量
}
复制代码
五、总结
本文主要使用一种面向过程的方式来实现简单的动画,但其实它里面已经包括了pop中的一些关键代码,只是pop是使用面向对象的方式来实现动画,以保证动画更灵活、更高效,关于pop的具体实现方式,后面会逐一介绍。