iOS开发笔记 - 仿京东的加入购物车动画

EliNussbaum 7年前
   <p style="text-align:center"><img src="https://simg.open-open.com/show/f8e506b5f42838e52bbb98d0e973060f.png"></p>    <p>请叫我死肥宅</p>    <p>之前APP里的加入购物车动画是最简单的UIView动画(一句代码那种),这几天正好有时间所以就跟产品那边确认优化了一下。虽然产品嘴上说让我自由发挥,但我相信没处理好肯定会让我改,改到产品那边满意为止,所以我研究了一下京东的加入购物车动画。</p>    <p>先看看京东的购物车动画是怎样的:</p>    <p><img src="https://simg.open-open.com/show/1eb57333cf07feb88a78f5e877808803.gif"></p>    <p>京东的加入购物车动画.gif</p>    <p>再看看我模仿出来的效果:</p>    <p><img src="https://simg.open-open.com/show/d15786ad0059003fa65c639c31cea0d9.gif"></p>    <p>我为了突出效果把动画写得夸张了点,实际项目中不会这么张狂。</p>    <h3>先分析一下整个动画的过程</h3>    <p>当用户点击加入购物车按钮时,一张商品图片从“加入购物车按钮”中心飞到了“购物车”按钮中心。其中:</p>    <ul>     <li>飞行的路径是抛物线的</li>     <li>飞行过程中图片越来越小</li>     <li>飞行结束后商品数量label颤抖了两下</li>    </ul>    <h3>如何定义这个动画?</h3>    <ol>     <li>这个动画是购物车相关的,所以它的类名应该是 ShoppingCartTool 或者 ShoppingCartManagement 之类的。</li>     <li>这个动画效果至少需要3个参数:商品图片、起点和终点。</li>     <li>我们需要在动画结束时进行相应处理,所以还需要一个动画结束时回调的block。</li>     <li>类方法比对象方法使用更加方便。</li>    </ol>    <p>基于这四点,方法定义如下:</p>    <pre>  <code class="language-objectivec">#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>    <h3>动画实现详细讲解</h3>    <p>先把完整代码贴出来:</p>    <pre>  <code class="language-objectivec">+ (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>    <p>看到这种抛物线的动画我就条件反射的想到 <strong> CAShapeLayer+UIBezierPath </strong> 。</p>    <p>展示:由layer决定</p>    <p>layer可以装图片</p>    <pre>  <code class="language-objectivec">animationLayer.contents = (id)goodsImage.CGImage;</code></pre>    <p>轨迹:由贝塞尔曲线决定</p>    <p>贝塞尔曲线决定了移动轨迹</p>    <pre>  <code class="language-objectivec">pathAnimation.path = movePath.CGPath;</code></pre>    <p>动画:由animation决定</p>    <p>动画有很多,按需添加</p>    <pre>  <code class="language-objectivec">// 添加轨迹动画  [animationLayer addAnimation:pathAnimation forKey:nil];  // 添加缩小动画  [animationLayer addAnimation:scaleAnimation forKey:nil];</code></pre>    <h3>难点</h3>    <p>颤抖效果如何实现?</p>    <p>快速缩放两次不就是颤抖效果了吗?:flushed:</p>    <pre>  <code class="language-objectivec">/** 加入购物车按钮点击 */  - (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>    <p><img src="https://simg.open-open.com/show/e4f837420c958dcfdb297b0a300614f4.jpg"></p>    <p>就这样成功颤抖了。</p>    <h3>细节:</h3>    <p>为什么我不直接将动画layer加到window上?</p>    <p>如果直接加在window上,不管是keyWindow还是AppDelegate的window,当动画进行中的时候切换视图控制器,视图控制器切换了,但是动画并不会跟着切换。来张动图你就明白了:</p>    <p><img src="https://simg.open-open.com/show/06eb2f563eacbdebca4e4432335f2736.gif"></p>    <p>动画进行中切换页面.gif</p>    <p>这显然不是我们想要的结果,所以我把动画layer添加到的最顶层视图控制器上。</p>    <h3>精髓</h3>    <p>通过延迟加载来和动画结束时间相对应:</p>    <pre>  <code class="language-objectivec">//------- 动画结束后执行 -------//  dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(durationTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{      [animationLayer removeFromSuperlayer];      completion(YES);  });</code></pre>    <h3>总结:</h3>    <p>封装小功能时不仅仅要完成功能,细节是不能忽视的。</p>    <h3>补充说明:</h3>    <p>实际开发中很可能需要将frame坐标转换为屏幕坐标,这个百度一下就可以找到答案。</p>    <h3> </h3>