iOS開發筆記 - 仿京東的加入購物車動畫
請叫我死肥宅
之前APP里的加入購物車動畫是最簡單的UIView動畫(一句代碼那種),這幾天正好有時間所以就跟產品那邊確認優化了一下。雖然產品嘴上說讓我自由發揮,但我相信沒處理好肯定會讓我改,改到產品那邊滿意為止,所以我研究了一下京東的加入購物車動畫。
先看看京東的購物車動畫是怎樣的:
京東的加入購物車動畫.gif
再看看我模仿出來的效果:
我為了突出效果把動畫寫得夸張了點,實際項目中不會這么張狂。
先分析一下整個動畫的過程
當用戶點擊加入購物車按鈕時,一張商品圖片從“加入購物車按鈕”中心飛到了“購物車”按鈕中心。其中:
- 飛行的路徑是拋物線的
- 飛行過程中圖片越來越小
- 飛行結束后商品數量label顫抖了兩下
如何定義這個動畫?
- 這個動畫是購物車相關的,所以它的類名應該是 ShoppingCartTool 或者 ShoppingCartManagement 之類的。
- 這個動畫效果至少需要3個參數:商品圖片、起點和終點。
- 我們需要在動畫結束時進行相應處理,所以還需要一個動畫結束時回調的block。
- 類方法比對象方法使用更加方便。
基于這四點,方法定義如下:
#import <Foundation/Foundation.h>
import <UIKit/UIKit.h>
@interface ShoppingCartTool : NSObject
/**
加入購物車的動畫效果
@param goodsImage 商品圖片
@param startPoint 動畫起點
@param endPoint 動畫終點
@param completion 動畫執行完成后的回調
*/
- (void)addToShoppingCartWithGoodsImage:(UIImage *)goodsImage
startPoint:(CGPoint)startPoint
endPoint:(CGPoint)endPoint
completion:(void (^)(BOOL finished))completion;
@end</code></pre>
動畫實現詳細講解
先把完整代碼貼出來:
+ (void)addToShoppingCartWithGoodsImage:(UIImage *)goodsImage startPoint:(CGPoint)startPoint endPoint:(CGPoint)endPoint completion:(void (^)(BOOL))completion{
//------- 創建shapeLayer -------//
CAShapeLayer *animationLayer = [[CAShapeLayer alloc] init];
animationLayer.frame = CGRectMake(startPoint.x - 20, startPoint.y - 20, 40, 40);
animationLayer.contents = (id)goodsImage.CGImage;
// 獲取window的最頂層視圖控制器
UIViewController *rootVC = [[UIApplication sharedApplication].delegate window].rootViewController;
UIViewController *parentVC = rootVC;
while ((parentVC = rootVC.presentedViewController) != nil ) {
rootVC = parentVC;
}
while ([rootVC isKindOfClass:[UINavigationController class]]) {
rootVC = [(UINavigationController *)rootVC topViewController];
}
// 添加layer到頂層視圖控制器上
[rootVC.view.layer addSublayer:animationLayer];
//------- 創建移動軌跡 -------//
UIBezierPath *movePath = [UIBezierPath bezierPath];
[movePath moveToPoint:startPoint];
[movePath addQuadCurveToPoint:endPoint controlPoint:CGPointMake(200,100)];
// 軌跡動畫
CAKeyframeAnimation *pathAnimation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
CGFloat durationTime = 1; // 動畫時間1秒
pathAnimation.duration = durationTime;
pathAnimation.removedOnCompletion = NO;
pathAnimation.fillMode = kCAFillModeForwards;
pathAnimation.path = movePath.CGPath;
//------- 創建縮小動畫 -------//
CABasicAnimation *scaleAnimation = [CABasicAnimation animationWithKeyPath:@"transform.scale"];
scaleAnimation.fromValue = [NSNumber numberWithFloat:1.0];
scaleAnimation.toValue = [NSNumber numberWithFloat:0.5];
scaleAnimation.duration = 1.0;
scaleAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
scaleAnimation.removedOnCompletion = NO;
scaleAnimation.fillMode = kCAFillModeForwards;
// 添加軌跡動畫
[animationLayer addAnimation:pathAnimation forKey:nil];
// 添加縮小動畫
[animationLayer addAnimation:scaleAnimation forKey:nil];
//------- 動畫結束后執行 -------//
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(durationTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[animationLayer removeFromSuperlayer];
completion(YES);
});
}</code></pre>
看到這種拋物線的動畫我就條件反射的想到 CAShapeLayer+UIBezierPath 。
展示:由layer決定
layer可以裝圖片
animationLayer.contents = (id)goodsImage.CGImage;
軌跡:由貝塞爾曲線決定
貝塞爾曲線決定了移動軌跡
pathAnimation.path = movePath.CGPath;
動畫:由animation決定
動畫有很多,按需添加
// 添加軌跡動畫
[animationLayer addAnimation:pathAnimation forKey:nil];
// 添加縮小動畫
[animationLayer addAnimation:scaleAnimation forKey:nil];
難點
顫抖效果如何實現?
快速縮放兩次不就是顫抖效果了嗎?:flushed:
/* 加入購物車按鈕點擊 /
(void)addButtonClicked:(UIButton *)sender {
[ShoppingCartTool addToShoppingCartWithGoodsImage:[UIImage imageNamed:@"heheda"] startPoint:self.addButton.center endPoint:self.shoppingCartButton.center completion:^(BOOL finished) {
NSLog(@"動畫結束了");
//------- 顫抖吧 -------//
CABasicAnimation *scaleAnimation = [CABasicAnimation animationWithKeyPath:@"transform.scale"];
scaleAnimation.fromValue = [NSNumber numberWithFloat:1.0];
scaleAnimation.toValue = [NSNumber numberWithFloat:0.7];
scaleAnimation.duration = 0.1;
scaleAnimation.repeatCount = 2; // 顫抖兩次
scaleAnimation.autoreverses = YES;
scaleAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[self.goodsNumLabel.layer addAnimation:scaleAnimation forKey:nil];
}];
}</code></pre>

就這樣成功顫抖了。
細節:
為什么我不直接將動畫layer加到window上?
如果直接加在window上,不管是keyWindow還是AppDelegate的window,當動畫進行中的時候切換視圖控制器,視圖控制器切換了,但是動畫并不會跟著切換。來張動圖你就明白了:

動畫進行中切換頁面.gif
這顯然不是我們想要的結果,所以我把動畫layer添加到的最頂層視圖控制器上。
精髓
通過延遲加載來和動畫結束時間相對應:
//------- 動畫結束后執行 -------//
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(durationTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[animationLayer removeFromSuperlayer];
completion(YES);
});
總結:
封裝小功能時不僅僅要完成功能,細節是不能忽視的。
補充說明:
實際開發中很可能需要將frame坐標轉換為屏幕坐標,這個百度一下就可以找到答案。