简介
最近看到华尔街见闻下拉刷新动画,觉得挺好看的。于是决定模仿写一个demo,顺便发现华尔街见闻下拉刷新动画的1个疑似小问题,顺便解决了一下。
网络请求完成后,四个立方体会出现突然回到原始状态,会出现动画卡顿的情况(解决方案可以通过获取当前动画呈现树的位置,将立方体移动至标准形态后,再将立方体设置为原始状态)
知识点梳理
动画实现步骤
动画的时间步骤大体上可以分为5步
一. 创建4个立方体设置其阴影
二. 设置立方体position并添加到CATransformLayer上,将CATransformLayer进行3D旋转
三. 设置根据下来百分比设置下拉过程动画
四. 设置网络请求时关键帧动画
五. 网络请求完成后根据获取当前动画呈现树状态,将4个立方体通过动画运动至标准形态。通知下拉刷新控件动画完成
详细实现说明
一、立方体创建
-
iOS的3D坐标系
立方体创建其实将6个平面进行3D变换组合而成的图形,因此我们先了解一下iOSX,Y,Z轴,以及围绕它们旋转的方向
由图所见,绕Z轴的旋转等同于之前二维空间的仿射旋转,但是绕X轴和Y轴的旋转就突破了屏幕的二维空间,并且在用户视角看来发生了倾斜。
我们可以先创建一个上平面和下平面平行于iphone屏幕的立方体,上平面的Z坐标为halfCubeUnit,下平面的Z坐标为-halfCubeUnit。由于我们需要将平面的正面放在立方体的外侧,因此在平移立方体Z坐标后,还需要将下平面绕X轴旋转180°或者-180°。其他平面类似,因此我们就得到了一个上平面正对着我们的立方体,从用户视角看上去此时的立方体就相当于一个正方形。
//创建立方体的单个面
- (CALayer *)getFaceWithTransform:(CATransform3D)transform color:(UIColor *)color
{
//create cube face layer
CALayer *face = [CALayer layer];
face.bounds = CGRectMake(0, 0, cubeUnit, cubeUnit);
face.position = CGPointMake(0, 0);
face.backgroundColor = color.CGColor;
//不绘制背面
face.doubleSided = NO;
//apply the transform and return
face.transform = transform;
return face;
}
//创建承载立方体6个面的CATransformLayer
- (CALayer *)getCubeTransformLayerWithPosition:(CGPoint)position
{
//将平面正面旋转为可视面
//上下两面
CATransform3D transTop = CATransform3DMakeTranslation(0, 0, halfCubeUnit);
UIColor *colorTop = [UIColor colorWithRed:72/255. green:122/255. blue:200/255. alpha:1];
CALayer *layerTop = [self getFaceWithTransform:transTop color:colorTop];
CATransform3D transBottom = CATransform3DMakeTranslation(0, 0, -halfCubeUnit);
transBottom = CATransform3DRotate(transBottom, M_PI, 1, 0, 0);
CALayer *layerBottom= [self getFaceWithTransform:transBottom color:colorTop];
//前后两面
CATransform3D transBack = CATransform3DMakeTranslation(0, -halfCubeUnit, 0);
transBack = CATransform3DRotate(transBack, M_PI_2, 1, 0, 0);
UIColor *colorBack = [UIColor colorWithRed:89/255. green:117/255. blue:251/255. alpha:1];
CALayer *layerBack = [self getFaceWithTransform:transBack color:colorBack];
CATransform3D transFront = CATransform3DMakeTranslation(0, halfCubeUnit, 0);
transFront = CATransform3DRotate(transFront, -M_PI_2, 1, 0, 0);
CALayer *layerFront = [self getFaceWithTransform:transFront color:colorBack];
//左右两面
CATransform3D transLeft = CATransform3DMakeTranslation(-halfCubeUnit, 0, 0);
transLeft = CATransform3DRotate(transLeft, -M_PI_2, 0, 1, 0);
UIColor *colorLeft = [UIColor colorWithRed:60/255. green:81/255. blue:220/255. alpha:1];
CALayer *layerLeft = [self getFaceWithTransform:transLeft color:colorLeft];
CATransform3D transRight = CATransform3DMakeTranslation(halfCubeUnit, 0, 0);
transRight = CATransform3DRotate(transRight, M_PI_2, 0, 1, 0);
CALayer *layerRight = [self getFaceWithTransform:transRight color:colorLeft];
- CALayer阴影
阴影往往可以达到图层深度暗示的效果。它通常由5个属性来控制
@property float shadowOpacity [0.0-1.0]控制着阴影的模糊度,为0时阴影非常确定的边界线。值越来越大的时候,边界线就会越来越模糊和自然
@property CGFloat shadowRadius 值越大阴影形状越模糊,图层的深度看上去就会更明显
@property(nullable) CGColorRef shadowColor 控制着阴影的颜色
@property CGSize shadowOffset 控制着阴影的方向和距离,默认值是 {0, -3}
@property(nullable) CGPathRef shadowPath 控制阴影的轮廓,使用此属性显式指定路径通常会提高渲染性能。此值默认为nil,此时图层轮廓是通过子图层的Alpha通道合成创建其阴影。实时计算阴影非常消耗资源,尤其是图层有多个子图层,每个图层还有一个有透明效果的寄宿图的情况
我们在创建立方体阴影的时候,使用了上平面的阴影。由于在绘制立方体6个面的时候,我们设置不绘制图层的的背面。因此立方体的下底面其实是没有绘制的,因此不能在下底面添加阴影。由于立方体之后要进行绕Z轴-45°旋转,阴影位置应设置在立方体左下方。
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(0, 0, 10, 10));
layerTop.shadowPath = path;
CGPathRelease(path);
layerTop.shadowOffset = CGSizeMake(-30, 30);
layerTop.shadowColor = [UIColor grayColor].CGColor;
layerTop.shadowOpacity = 0.3;
layerTop.shadowRadius = 0.2;
layerTop.shouldRasterize = YES;
layerTop.rasterizationScale = [UIScreen mainScreen].scale;
二. CATransformLayer创建
CATransformLayer简介
其他的图层虽然能也能够对承载的内容进行3D变换的显示,但是它们其实是把它的子视图都平面化到一个场景中。CATransformLayer不同于普通的CALayer,因为它不能显示它自己的内容。只有当存在了一个能作用域子图层的变换它才真正存在。CATransformLayer并不平面化它的子图层,所以它能够用于构造一个层级的3D结构添加立方体6面到CATransformLayer,通过CATransformLayer的3D变化就能看到立体效果的立方体了
//添加到cubeLayer上
CATransformLayer *cubeLayer = [CATransformLayer layer];
cubeLayer.position = position;
[cubeLayer addSublayer:layerTop];
[cubeLayer addSublayer:layerBottom];
[cubeLayer addSublayer:layerLeft];
[cubeLayer addSublayer:layerRight];
[cubeLayer addSublayer:layerFront];
[cubeLayer addSublayer:layerBack];
return cubeLayer;
}
- 创建4个承载立方体的CATransformLayer,设置它们position并添加到父CATransformLayer上。根据四个立方体的动画初始位置设置子CATransformLayer的position,并将父CATransformLayer延X轴旋转45°,在延Z轴旋转-45°。就可以看到立体效果的立方体
self.cube0 = [self getCubeTransformLayerWithPosition:self.cube0KeyPoints[0].CGPointValue];
self.cube1 = [self getCubeTransformLayerWithPosition:self.cube1KeyPoints[0].CGPointValue];
self.cube2 = [self getCubeTransformLayerWithPosition:self.cube2KeyPoints[0].CGPointValue];
self.cube3 = [self getCubeTransformLayerWithPosition:self.cube3KeyPoints[0].CGPointValue];
[self.transformLayer addSublayer:_cube0];
[self.transformLayer addSublayer:_cube1];
[self.transformLayer addSublayer:_cube2];
[self.transformLayer addSublayer:_cube3];
transform = CATransform3DIdentity;
transform = CATransform3DRotate(transform, M_PI_4, 1, 0, 0); //绕x轴旋转45°
transform = CATransform3DRotate(transform, -M_PI_4, 0, 0, 1); //绕z轴旋转-45°
self.transformLayer.transform = transform;
此时可以看到四个立方体动画前的初始状态如下图:三. 设置下拉过程动画
-
动画路径分析
上图左上角显示了立方体动画的关键帧位置。动画其实是在6个位置值中循环,其中关键帧2和3位置相同,关键帧6和7位置相同。因此我们可以得到8个关键帧,图中红方块的动画关键帧如代码cube0KeyPoints所示,绿方块与红方块关键帧相差2代码cube1KeyPoints所示
- (NSArray <NSValue *>*)cube0KeyPoints
{
if (!_cube0KeyPoints) {
_cube0KeyPoints = @[
[NSValue valueWithCGPoint:CGPointMake(-halfCubeUnit, -cubeUnit)],
[NSValue valueWithCGPoint:CGPointMake(halfCubeUnit, -cubeUnit)],
[NSValue valueWithCGPoint:CGPointMake(halfCubeUnit, 0)],
[NSValue valueWithCGPoint:CGPointMake(halfCubeUnit, 0)],
[NSValue valueWithCGPoint:CGPointMake(halfCubeUnit, cubeUnit)],
[NSValue valueWithCGPoint:CGPointMake(-halfCubeUnit, cubeUnit)],
[NSValue valueWithCGPoint:CGPointMake(-halfCubeUnit, 0)],
[NSValue valueWithCGPoint:CGPointMake(-halfCubeUnit, 0)],
];
}
return _cube0KeyPoints;
}
- (NSArray <NSValue *>*)cube1KeyPoints
{
if (!_cube1KeyPoints) {
NSMutableArray *keyPoints = [[NSMutableArray alloc] init];
for (NSInteger index = 0; index < self.cube0KeyPoints.count; index++) {
//立方体1的动画初始位置在step2
[keyPoints addObject:self.cube0KeyPoints[(index + 2) % self.cube0KeyPoints.count]];
}
_cube1KeyPoints = keyPoints;
}
return _cube1KeyPoints;
}
- 从下拉动画那一栏可以看到,下拉过程四个立方体从形态0变化到了形态2, 我们可以将需要根据百分比获取四个立方体在形态变化过程中的位置,设置立方体的position就可以了。
3 .由于CALayer设置可动画属性是,默认是开启隐式动画的,我们需要将隐式动画禁止
- (void)refreshWithPercent:(CGFloat)percent
{
[self resetCubes];
//从关键帧0开始运动
CGPoint point0 = [self getPositionWithPercent:percent index:0];
//从关键帧2开始运动
CGPoint point1 = [self getPositionWithPercent:percent index:1];
//从关键帧4开始运动
CGPoint point2 = [self getPositionWithPercent:percent index:2];
//从关键帧6开始运动
CGPoint point3 = [self getPositionWithPercent:percent index:3];
//禁止隐式动画
[CATransaction begin];
[CATransaction setDisableActions:YES];
_cube0.position = point0;
_cube1.position = point1;
_cube2.position = point2;
_cube3.position = point3;
[CATransaction commit];
}
//根据下拉距离的百分比控制立方体位置
- (CGPoint)getPositionWithPercent:(CGFloat)percent index:(NSInteger)index
{
if(percent >= 1){
percent = 1;
}
//下拉过程动画分为pullStep步,每一步所占百分比为1./animePullSteps
CGFloat unitPercent = 1./animePullSteps;
NSInteger step = (NSInteger)(percent/unitPercent);
CGFloat subPercent = (percent - step*unitPercent)/unitPercent;
NSArray <NSValue *>*cubeKeyPoints = self.allCubeKeyPoints[index];
NSInteger count = cubeKeyPoints.count;
CGPoint pointStart = cubeKeyPoints[step%count].CGPointValue;
CGPoint pointEnd = cubeKeyPoints[(step+1)%count].CGPointValue;
CGPoint point = CGPointMake(pointStart.x+(pointEnd.x-pointStart.x)*subPercent, pointStart.y+(pointEnd.y-pointStart.y)*subPercent);
return point;
}
四. 刷新时关键帧动画
刷新过程中其实就是8个关键帧的无限循环。
//开始刷新动画
- (void)startAnimation
{
[self resetCubes];
NSInteger valueCount = animeRefreshSteps + 1;
CAAnimation *animation0 = [self animationWithIndex:0 step:0 valueCount:valueCount];
CAAnimation *animation1 = [self animationWithIndex:1 step:0 valueCount:valueCount];
CAAnimation *animation2 = [self animationWithIndex:2 step:0 valueCount:valueCount];
CAAnimation *animation3 = [self animationWithIndex:3 step:0 valueCount:valueCount];
[animation0 setRepeatCount:CGFLOAT_MAX];
[animation1 setRepeatCount:CGFLOAT_MAX];
[animation2 setRepeatCount:CGFLOAT_MAX];
[animation3 setRepeatCount:CGFLOAT_MAX];
CFTimeInterval beginTime = CACurrentMediaTime()+0.1;
animation0.beginTime = beginTime;
animation1.beginTime = beginTime;
animation2.beginTime = beginTime;
animation3.beginTime = beginTime;
[_cube0 addAnimation:animation0 forKey:@"animiation"];
[_cube1 addAnimation:animation1 forKey:@"animiation"];
[_cube2 addAnimation:animation2 forKey:@"animiation"];
[_cube3 addAnimation:animation3 forKey:@"animiation"];
}
/**
创建关键帧动画
@param index 第几个立方体[0-3]
@param step 从关键帧的第几步开始进行动画
@param valueCount 从立方体关键帧数组中选取几步进行动画
@return 关键帧动画
*/
- (CAKeyframeAnimation *)animationWithIndex:(NSInteger)index step:(NSInteger)step valueCount:(NSInteger)valueCount
{
//生成对应的关键帧
NSMutableArray *values = [[NSMutableArray alloc] init];
//生成对应的timingFunctions
NSMutableArray *timingFunctions = [[NSMutableArray alloc] init];
//对应的立方体动画关键帧
NSArray *cubeKeyPoints = self.allCubeKeyPoints[index];
//获取cubeKeyPoints数组长度,防止数组越界
NSInteger count = cubeKeyPoints.count;
for (NSInteger index = 0; index < valueCount; index++) {
[values addObject:cubeKeyPoints[(step+index) % count]];
if (index != valueCount-1) {//timingFunctions.count 比 values.count 小1位
[timingFunctions addObject:[CAMediaTimingFunction functionWithControlPoints:0.24 :0.52 :0.43 :0.8]];
}
}
//创建关键帧动画
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
animation.values = values;
animation.duration = timeDuration/animeRefreshSteps * timingFunctions.count;
animation.calculationMode = kCAAnimationLinear;
animation.timingFunctions = timingFunctions;
//动画结束后,保持最后一帧状态
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeBoth;
return animation;
}
五. 结束动画
上面提到华尔街见闻app中,在网络请求返回后由于四个立方体的组合成的形态并不定。突然从当期动画状态切换到动画初始状态会有卡顿的感觉,这里就需要解决这个问题
需要了解的知识点有两个
- 显示动画并不改变作用对象的属性。也就是说显示动画并不改变模型树的属性值,例如你通过显示使对象的position变化,并且在动画结束是保留动画的最终状态。但是无论是动画中还是动画结束,对象的position始终还是动画前的值。当你移除动画的时候,会看到对象又回到了之前位置
- 在动画中我们可以通过呈现树(presentationLayer)获取当前对象的当时的属性值。
由于网络请求并不确定是什么时候返回,动画运行到什么样式也无法确定。因此我们可以在请求完成通过获取呈现树的position属性值,移除当前动画,添加新动画让立方体运动到标准形态。完成后通知刷新控件进行返回。 - 我们经常会使用beginTime = CACurrentMediaTime()+N来让动画延迟N秒执行,特别是动画组中运用的更加频繁。我们也可以使用beginTime = CACurrentMediaTime()-N,让动画从第N秒开始执行。这样就可以衔接之前已经进行过的动画。
- (void)stopAnimation
{
//获取呈现树当前位置
CGPoint point0 = self.cube0.presentationLayer.position;
CGPoint point1 = self.cube1.presentationLayer.position;
CGPoint point2 = self.cube2.presentationLayer.position;
CGPoint point3 = self.cube3.presentationLayer.position;
//去除承载立方体CATransformLayer的显示动画,停止动画
[_cube0 removeAllAnimations];
[_cube1 removeAllAnimations];
[_cube2 removeAllAnimations];
[_cube3 removeAllAnimations];
NSArray <NSValue *>*presentPoints = @[[NSValue valueWithCGPoint:point0],[NSValue valueWithCGPoint:point1],[NSValue valueWithCGPoint:point2],[NSValue valueWithCGPoint:point3],];
NSDictionary <NSString *, NSNumber *>*presentInfo = [self getPresentInfoWithPoints:presentPoints];
CGFloat subPercent = presentInfo[@"subPercent"].floatValue;
NSInteger step = presentInfo[@"step"].integerValue;
NSInteger valueCount = [self animeEndSteps:step] + 1;
CAAnimation *animation0 = [self animationWithIndex:0 step:step valueCount:valueCount];
CAAnimation *animation1 = [self animationWithIndex:1 step:step valueCount:valueCount];
CAAnimation *animation2 = [self animationWithIndex:2 step:step valueCount:valueCount];
CAAnimation *animation3 = [self animationWithIndex:3 step:step valueCount:valueCount];
animation0.delegate = self;
CFTimeInterval beginTime = CACurrentMediaTime();
animation0.beginTime = beginTime-subTimeDuration*subPercent;
animation1.beginTime = beginTime-subTimeDuration*subPercent;
animation2.beginTime = beginTime-subTimeDuration*subPercent;
animation3.beginTime = beginTime-subTimeDuration*subPercent;
[_cube0 addAnimation:animation0 forKey:@"animiation"];
[_cube1 addAnimation:animation1 forKey:@"animiation"];
[_cube2 addAnimation:animation2 forKey:@"animiation"];
[_cube3 addAnimation:animation3 forKey:@"animiation"];
}
#pragma mark CAAnimationDelegate
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
[self resetCubes];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (self.animiationComplete) {
self.animiationComplete();
}
});
}
最终效果