iOS开发——仿淘宝添加到购物车的动画效果实现

这篇博文实在不知道该起什么名字才能概况我的意思。。。挫语文水平技术分享

类似于淘宝一样,我们在写一些购物、订餐之类的app的时候,在用户选择购买或者加入购物车时可以添加一个商品飞到购物车中的动画效果,如下图所示:

技术分享


实现这个效果还是不算难的,但涉及的问题比较多,还是挺有学习价值的。主要面对的问题有以下几点

1、cell中有button,如何获得该button,即如何知道用户点击的是哪一个button。

2、坐标系的转换,这里频繁使用坐标系转换,主要原因是这里需要涉及三个视图——cell、tableView、view

3、Bezier曲线的应用。

下面我们一一来解决这些问题。

正好这学期图形学刚刚结课,虽然没有什么关联,不过也算复习了- -。


一、获取cell中的button

这个问题也是个老问题了,方法也非常多,比较常见的是自定义cell,然后将button作为cell的property,这样我们可以在创建cell的时候为button设tag值,根据indexPath来设即可,通过tag来区分。这样在很多情况下也能解决问题。不过这次我们用的并不是这种方法。

分析:每个cell的button有自己的处理逻辑,比如,当点击收藏按钮时要将选中的FoodModel保存起来,要改变button的标题……,从MVC的原则以及职责单一化的原则来看,这些写在cell之外的地方都是不合适的,而上面的动画很明显是在控制器层级的动画,也就是动画代码不能写在cell中,而是在某某Controller中的。如果只是设tag在控制器中处理是不能实现这个需求的。

既然都要处理,那就将处理逻辑分开即可。说到底这还是代理模式的应用,是类与类之间的通信问题,用协议、块、通知都可以。具体来说就是当点击按钮时,在cell中处理自己的逻辑,然后把其他任务交给其他类。这里我用的是通知的方法。

当然,再说第二个问题之前先顺带一提,坐标系转换,很明显是需要坐标的,我们在控制器中生成动画的时候,是需要知道点击的那个cell的某一特定位置(之后会作为动画的起点)的坐标,所以在发送通知的时候要自带上userInfo便于在控制器中取出来。

附上这部分相关代码:

- (IBAction)tapLikedButton:(UIButton *)sender {
    //处理自己的逻辑
    
    //if the food has been chosen,then remove it
    if ([self.likedFoods containsObject:self.foodModel.foodName]) {
        [self.likedFoods removeObject:self.foodModel.foodName];
        [self.foodLikedButton setTitle:@"收藏" forState:UIControlStateNormal];
        
    } else {
        //like the food and change the title of btn
        [self.likedFoods addObject:self.foodModel.foodName];
        [self.foodLikedButton setTitle:@"取消收藏" forState:UIControlStateNormal];
        
        //将动画交给其他类去处理
        [[NSNotificationCenter defaultCenter] postNotificationName:LIKE_FOOD_NOTIFICATION object:nil userInfo:@{@"position" : [NSValue valueWithCGPoint:[self convertPoint:self.foodNameLabel.center toView:self.superview]]}];
    }
    
    //save the foods
    NSString *filePath = [self filePath];
    [self.likedFoods writeToFile:filePath atomically:YES];
}

二、坐标系的转换

其实在上面的代码中已经用到了,还是,做一下分析:这里我们要将一个位置坐标传出去,但是传什么位置呢?如果是Lable的位置简答的传出去,那么很明显会出现一个问题:不管你点击那个cell的按钮,动画都是从同一个起点出发的,而且绝对不会是任何正确的起点。因为每个cell中Label的位置都是一样的,而我们实际需要的是这个坐标相对于TableView的位置,也就是说它在父视图中的位置,所以这里要将该点坐标转换。

同样,上面gif图片中,可以看到,我们要涉及的视图有,最右下角有一组图片和按钮,表示购物车,在tableView中有我们之前传过来的坐标,而我们希望让动画发生在view层级上,所以这里需要两次坐标转换,把右下角的控件集合中的按钮坐标(购物车是个按钮)和tableView中的传过来的起点坐标都转换到self.view中,具体做法是

CGPoint endpoint = [self.view convertPoint:btnCenter fromView:carBG];

    CGPoint startPoint = [self.view convertPoint:lbCenter fromView:self.tableView];
附:关于坐标转换,网上也有不少资料,本人之前的博客中也有提及:iOS开发——仿新版iBOOks书本打开与关闭动画 

有了起止点之后,剩下的就是最关键的问题——bezier曲线的使用了。

三、Bezier曲线

关于Bezier曲线,iOS已经为我们封装好了生成操作,我们只需要提供控制点即可。为了更好地理解Bezier曲线,为了以后能更好的应用Bezier曲线来创造好看的效果,我们应该学习其原理与生成机制,这里只做简单一提,以后再专门学习记录。。

因为我们想产生一种类抛物线的动画,所以这里我们需要二阶Bezier曲线即可,所以要提供三个控制点,起始点和终止点都已经有了,关键就是中间的控制点。在计图实验中生成Bezier时,我们用的一种思路是以直代曲,用大量短线段来表示一条曲线,每一个n阶Bezier曲线(n+1个点)在生成时,总能在n个线段中按照一个比例各找出一个点,而这n个点又能生成一个n-1阶Bezier,我们的Bezier曲线上的点就是当只有一条线段以后按照那个比例找出的那个点。

无图无真相,盗图可耻,我干脆摆上一个链接好了技术分享

Beizer曲线上点的确定

原理是这样,我们用起来只要稍微了解一点,就知道我们缺少的那个控制点就是在起止点之间,但是纵坐标要比这两点“高”不少的一个点。所以可以通过下面的公式得出一个控制点

    float x = sx + (ex - sx) / 3;
    float y = sy + (ey - sy) * 0.5 - 400;

由于该控制点的存在,我们的曲线会从起始点向上抛起然后再落到终点处。这里x、y的算法并不是固定的,可以自由更改,只要符合上面上的条件并且自己觉得好看就好。

利用这三个控制点就能生成一个二阶Bezier曲线,将其作为动画的path属性即可。

四、其他方面

UIView的动画是作用在layer层级的,所以我们可以生成一个CALayer,在这个layer上添加上自己的图片,然后将动画应用到这个layer中即可。


附该部分代码:

- (void)showLikedFoodsAnimation:(NSNotification *)notification {
    //get the location of label in table view
    NSValue *value = notification.userInfo[@"position"];
    CGPoint lbCenter = value.CGPointValue;
    
    //the image which will play the animation soon
    UIImageView *imageView = [[UIImageView alloc]initWithImage:[UIImage imageNamed:@"cm_center_discount"]];
    imageView.contentMode = UIViewContentModeScaleToFill;
    imageView.frame = CGRectMake(0, 0, 20, 20);
    imageView.hidden = YES;
    imageView.center = lbCenter;
    
    //the container of image view
    CALayer *layer = [[CALayer alloc]init];
    layer.contents = imageView.layer.contents;
    layer.frame = imageView.frame;
    layer.opacity = 1;
    [self.view.layer addSublayer:layer];
    
    CGPoint btnCenter = carButton.center;
    //动画 终点 都以sel.view为参考系
    CGPoint endpoint = [self.view convertPoint:btnCenter fromView:carBG];
    UIBezierPath *path = [UIBezierPath bezierPath];
    //动画起点
    CGPoint startPoint = [self.view convertPoint:lbCenter fromView:self.tableView];
    [path moveToPoint:startPoint];
    //贝塞尔曲线控制点
    float sx = startPoint.x;
    float sy = startPoint.y;
    float ex = endpoint.x;
    float ey = endpoint.y;
    float x = sx + (ex - sx) / 3;
    float y = sy + (ey - sy) * 0.5 - 400;
    CGPoint centerPoint=CGPointMake(x, y);
    [path addQuadCurveToPoint:endpoint controlPoint:centerPoint];
    
    //key frame animation to show the bezier path animation
    CAKeyframeAnimation *animation=[CAKeyframeAnimation animationWithKeyPath:@"position"];
    animation.path = path.CGPath;
    animation.removedOnCompletion = NO;
    animation.fillMode = kCAFillModeForwards;
    animation.duration = 0.8;
    animation.delegate = self;
    animation.autoreverses = NO;
    animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
    [layer addAnimation:animation forKey:@"buy"];
}




郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。