iOS下拉動畫:仿華爾街見聞

簡介

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

下面先來看一下具體動畫效果(轉成gif圖后動畫效果變丑了,原效果更好看)
1123.gif

知識點梳理

知識點.png

動畫實現(xiàn)步驟

動畫的時間步驟大體上可以分為5步
一. 創(chuàng)建4個立方體設置其陰影
二. 設置立方體position并添加到CATransformLayer上,將CATransformLayer進行3D旋轉
三. 設置根據下來百分比設置下拉過程動畫
四. 設置網絡請求時關鍵幀動畫
五. 網絡請求完成后根據獲取當前動畫呈現(xiàn)樹狀態(tài),將4個立方體通過動畫運動至標準形態(tài)。通知下拉刷新控件動畫完成

詳細實現(xiàn)說明

一、立方體創(chuàng)建

  1. 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];
  1. 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)建

  1. CATransformLayer簡介
    其他的圖層雖然能也能夠對承載的內容進行3D變換的顯示,但是它們其實是把它的子視圖都平面化到一個場景中。CATransformLayer不同于普通的CALayer,因為它不能顯示它自己的內容。只有當存在了一個能作用域子圖層的變換它才真正存在。CATransformLayer并不平面化它的子圖層,所以它能夠用于構造一個層級的3D結構

  2. 添加立方體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;
}
  1. 創(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)如下圖:
動畫初始狀態(tài).png

三. 設置下拉過程動畫

  1. 動畫路徑分析
    動畫路徑.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;
}
  1. 從下拉動畫那一欄可以看到,下拉過程四個立方體從形態(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)會有卡頓的感覺,這里就需要解決這個問題


2018-09-13 14_40_07.gif

需要了解的知識點有兩個

  1. 顯示動畫并不改變作用對象的屬性。也就是說顯示動畫并不改變模型樹的屬性值,例如你通過顯示使對象的position變化,并且在動畫結束是保留動畫的最終狀態(tài)。但是無論是動畫中還是動畫結束,對象的position始終還是動畫前的值。當你移除動畫的時候,會看到對象又回到了之前位置
  2. 在動畫中我們可以通過呈現(xiàn)樹(presentationLayer)獲取當前對象的當時的屬性值。
    由于網絡請求并不確定是什么時候返回,動畫運行到什么樣式也無法確定。因此我們可以在請求完成通過獲取呈現(xiàn)樹的position屬性值,移除當前動畫,添加新動畫讓立方體運動到標準形態(tài)。完成后通知刷新控件進行返回。
  3. 我們經常會使用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();
        }
    });
}

最終效果


下拉刷新.gif

源碼地址:https://gitee.com/dbmxl/PullRefreshAnimation

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • 1 CALayer IOS SDK詳解之CALayer(一) http://doc.okbase.net/Hell...
    Kevin_Junbaozi閱讀 5,336評論 3 23
  • 1、通過CocoaPods安裝項目名稱項目信息 AFNetworking網絡請求組件 FMDB本地數(shù)據庫組件 SD...
    陽明AI閱讀 16,203評論 3 119
  • 男人孤獨無助地在絢麗繁華的中央大街上彳亍著,臉上彌漫著一種哀傷的情緒,無論從哪種角度望去都難以發(fā)現(xiàn)可以喻之為喜悅的...
    七點本人閱讀 324評論 0 0
  • 5月,終于過去。 這個月,話費異常,流量驟增,打了客服電話才知道,手機后臺一直在自動更新該升級的各種app,象是手...
    李慶容閱讀 439評論 0 0
  • 我未曾見過你, 但一直在等你, 在街角,在路口,在車站 你不認識我,我也不認識你 等你,便滿心歡喜 未曾相遇,便可...
    破碗碗花閱讀 302評論 0 4

友情鏈接更多精彩內容