这篇博文实在不知道该起甚么名字才能概况我的意思。。。挫语文水平
类似于淘宝1样,我们在写1些购物、订餐之类的app的时候,在用户选择购买或加入购物车时可以添加1个商品飞到购物车中的动画效果,以下图所示:
实现这个效果还是不算难的,但触及的问题比较多,还是挺有学习价值的。主要面对的问题有以下几点
1、cell中有button,如何取得该button,即如何知道用户点击的是哪个button。
2、坐标系的转换,这里频繁使用坐标系转换,主要缘由是这里需要触及3个视图――cell、tableView、view
3、Bezier曲线的利用。
下面我们逐一来解决这些问题。
正好这学期图形学刚刚结课,虽然没有甚么关联,不过也算温习了- -。
1、获得cell中的button
这个问题也是个老问题了,方法也非常多,比较常见的是自定义cell,然后将button作为cell的property,这样我们可以在创建cell的时候为button设tag值,根据indexPath来设便可,通过tag来辨别。这样在很多情况下也能解决问题。不过这次我们用的其实不是这类方法。
分析:每一个cell的button有自己的处理逻辑,比如,当点击收藏按钮时要将选中的FoodModel保存起来,要改变button的标题……,从MVC的原则和职责单1化的原则来看,这些写在cell以外的地方都是不适合的,而上面的动画很明显是在控制器层级的动画,也就是动画代码不能写在cell中,而是在某某Controller中的。如果只是设tag在控制器中处理是不能实现这个需求的。
既然都要处理,那就将处理逻辑分开便可。说到底这还是代理模式的利用,是类与类之间的通讯问题,用协议、块、通知都可以。具体来讲就是当点击按钮时,在cell中处理自己的逻辑,然后把其他任务交给其他类。这里我用的是通知的方法。
固然,再说第2个问题之前先顺带1提,坐标系转换,很明显是需要坐标的,我们在控制器中生成动画的时候,是需要知道点击的那个cell的某1特定位置(以后会作为动画的出发点)的坐标,所以在发送通知的时候要自带上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];
}
2、坐标系的转换
其实在上面的代码中已用到了,还是,做1下分析:这里我们要将1个位置坐标传出去,但是传甚么位置呢?如果是Lable的位置简答的传出去,那末很明显会出现1个问题:不管你点击那个cell的按钮,动画都是从同1个出发点动身的,而且绝对不会是任何正确的出发点。由于每一个cell中Label的位置都是1样的,而我们实际需要的是这个坐标相对TableView的位置,也就是说它在父视图中的位置,所以这里要将该点坐标转换。
一样,上面gif图片中,可以看到,我们要触及的视图有,最右下角有1组图片和按钮,表示购物车,在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曲线的使用了。
3、Bezier曲线
关于Bezier曲线,iOS已为我们封装好了生成操作,我们只需要提供控制点便可。为了更好地理解Bezier曲线,为了以后能更好的利用Bezier曲线来创造好看的效果,我们应当学习其原理与生成机制,这里只做简单1提,以后再专门学习记录。。
由于我们想产生1种类抛物线的动画,所以这里我们需要2阶Bezier曲线便可,所以要提供3个控制点,起始点和终止点都已有了,关键就是中间的控制点。在计图实验中生成Bezier时,我们用的1种思路是以直代曲,用大量短线段来表示1条曲线,每个n阶Bezier曲线(n+1个点)在生成时,总能在n个线段中依照1个比例各找出1个点,而这n个点又能生成1个n⑴阶Bezier,我们的Bezier曲线上的点就是当只有1条线段以后依照那个比例找出的那个点。
无图无真相,盗图可耻,我干脆摆上1个链接好了
Beizer曲线上点的肯定
原理是这样,我们用起来只要略微了解1点,就知道我们缺少的那个控制点就是在起止点之间,但是纵坐标要比这两点“高”很多的1个点。所以可以通过下面的公式得出1个控制点
float x = sx + (ex - sx) / 3;
float y = sy + (ey - sy) * 0.5 - 400;
由于该控制点的存在,我们的曲线会从起始点向上抛起然后再落到终点处。这里x、y的算法其实不是固定的,可以自由更改,只要符合上面上的条件并且自己觉得好看就好。
利用这3个控制点就可以生成1个2阶Bezier曲线,将其作为动画的path属性便可。
4、其他方面
UIView的动画是作用在layer层级的,所以我们可以生成1个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"];
}