簡介
最近看到華爾街見聞下拉刷新動畫,覺得挺好看的。于是決定模仿寫一個demo,順便發(fā)現(xiàn)華爾街見聞下拉刷新動畫的1個疑似小問題,順便解決了一下。
網絡請求完成后,四個立方體會出現(xiàn)突然回到原始狀態(tài),會出現(xiàn)動畫卡頓的情況(解決方案可以通過獲取當前動畫呈現(xiàn)樹的位置,將立方體移動至標準形態(tài)后,再將立方體設置為原始狀態(tài))

知識點梳理

動畫實現(xiàn)步驟
動畫的時間步驟大體上可以分為5步
一. 創(chuàng)建4個立方體設置其陰影
二. 設置立方體position并添加到CATransformLayer上,將CATransformLayer進行3D旋轉
三. 設置根據下來百分比設置下拉過程動畫
四. 設置網絡請求時關鍵幀動畫
五. 網絡請求完成后根據獲取當前動畫呈現(xiàn)樹狀態(tài),將4個立方體通過動畫運動至標準形態(tài)。通知下拉刷新控件動畫完成
詳細實現(xiàn)說明
一、立方體創(chuàng)建
-
iOS的3D坐標系
立方體創(chuàng)建其實將6個平面進行3D變換組合而成的圖形,因此我們先了解一下iOSX,Y,Z軸,以及圍繞它們旋轉的方向
iOS3D坐標系.png
由圖所見,繞Z軸的旋轉等同于之前二維空間的仿射旋轉,但是繞X軸和Y軸的旋轉就突破了屏幕的二維空間,并且在用戶視角看來發(fā)生了傾斜。
我們可以先創(chuàng)建一個上平面和下平面平行于iphone屏幕的立方體,上平面的Z坐標為halfCubeUnit,下平面的Z坐標為-halfCubeUnit。由于我們需要將平面的正面放在立方體的外側,因此在平移立方體Z坐標后,還需要將下平面繞X軸旋轉180°或者-180°。其他平面類似,因此我們就得到了一個上平面正對著我們的立方體,從用戶視角看上去此時的立方體就相當于一個正方形。
//創(chuàng)建立方體的單個面
- (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;
}
//創(chuàng)建承載立方體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通道合成創(chuàng)建其陰影。實時計算陰影非常消耗資源,尤其是圖層有多個子圖層,每個圖層還有一個有透明效果的寄宿圖的情況
我們在創(chuàng)建立方體陰影的時候,使用了上平面的陰影。由于在繪制立方體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創(chuàng)建
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;
}
- 創(chuàng)建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;
此時可以看到四個立方體動畫前的初始狀態(tài)如下圖:
三. 設置下拉過程動畫
-
動畫路徑分析動畫路徑.png
上圖左上角顯示了立方體動畫的關鍵幀位置。動畫其實是在6個位置值中循環(huán),其中關鍵幀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;
}
- 從下拉動畫那一欄可以看到,下拉過程四個立方體從形態(tài)0變化到了形態(tài)2, 我們可以將需要根據百分比獲取四個立方體在形態(tài)變化過程中的位置,設置立方體的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個關鍵幀的無限循環(huán)。
//開始刷新動畫
- (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"];
}
/**
創(chuàng)建關鍵幀動畫
@param index 第幾個立方體[0-3]
@param step 從關鍵幀的第幾步開始進行動畫
@param valueCount 從立方體關鍵幀數(shù)組中選取幾步進行動畫
@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數(shù)組長度,防止數(shù)組越界
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]];
}
}
//創(chuàng)建關鍵幀動畫
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
animation.values = values;
animation.duration = timeDuration/animeRefreshSteps * timingFunctions.count;
animation.calculationMode = kCAAnimationLinear;
animation.timingFunctions = timingFunctions;
//動畫結束后,保持最后一幀狀態(tài)
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeBoth;
return animation;
}
五. 結束動畫
上面提到華爾街見聞app中,在網絡請求返回后由于四個立方體的組合成的形態(tài)并不定。突然從當期動畫狀態(tài)切換到動畫初始狀態(tài)會有卡頓的感覺,這里就需要解決這個問題

需要了解的知識點有兩個
- 顯示動畫并不改變作用對象的屬性。也就是說顯示動畫并不改變模型樹的屬性值,例如你通過顯示使對象的position變化,并且在動畫結束是保留動畫的最終狀態(tài)。但是無論是動畫中還是動畫結束,對象的position始終還是動畫前的值。當你移除動畫的時候,會看到對象又回到了之前位置
- 在動畫中我們可以通過呈現(xiàn)樹(presentationLayer)獲取當前對象的當時的屬性值。
由于網絡請求并不確定是什么時候返回,動畫運行到什么樣式也無法確定。因此我們可以在請求完成通過獲取呈現(xiàn)樹的position屬性值,移除當前動畫,添加新動畫讓立方體運動到標準形態(tài)。完成后通知刷新控件進行返回。 - 我們經常會使用beginTime = CACurrentMediaTime()+N來讓動畫延遲N秒執(zhí)行,特別是動畫組中運用的更加頻繁。我們也可以使用beginTime = CACurrentMediaTime()-N,讓動畫從第N秒開始執(zhí)行。這樣就可以銜接之前已經進行過的動畫。
- (void)stopAnimation
{
//獲取呈現(xiàn)樹當前位置
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();
}
});
}
最終效果


