本人录制技术视频地址:https://edu.csdn.net/lecturer/1899 欢迎观看。
这一节讲述的动画效果是碰撞动画,就是模拟或者仿真现实物体的碰撞效果。先看看效果图。
动画效果分析:
1. 有两个形式一样的View(自己和对手),所以我们可以考虑直接封装一个View。
2. 注意到View的里面的图片及边框的圆形都有可能变成椭圆。所以使用View的block方式实现这个效果有点不可靠。我们可以考虑使用图层动画。
View的层次结构图如下:
1. 因为每个View有自己的name,所以封装的View是继承自UIView而不是UIImageView。
2. photoLayer是为了显示图片。
3. maskLayer是为了实现圆角效果。
4. circleLayer就是图片外面的圆圈。
有了上面的分析,我们自定义一个头像的View (AvatarView) 并且它应该有图片及名称两个属性。
@property (nonatomic, strong) UIImage *image;
@property (nonatomic, copy) NSString *name;
然后在AvaterView.m文件中,先进行相应的初始化工作。
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [super initWithCoder:aDecoder]) {
[self initSubs];
}
return self;
}
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
[self initSubs];
}
return self;
}
- (void)initSubs {
self.photoLayer = [CALayer layer];
self.circleLayer = [CAShapeLayer layer];
self.maskLayer = [CAShapeLayer layer];
self.label = [[UILabel alloc] init];
self.label.font = [UIFont fontWithName:@"ArialRoundedMTBold" size:18.0];
self.label.textAlignment = NSTextAlignmentCenter;
self.label.textColor = [UIColor blackColor];
[self.layer addSublayer:self.photoLayer];
self.photoLayer.mask = self.maskLayer;
[self.layer addSublayer:self.circleLayer];
[self addSubview:self.label];
}
- (void)setImage:(UIImage *)image {
_image = image;
self.photoLayer.contents = (__bridge id)(image.CGImage);
}
- (void)setName:(NSString *)name {
_name = [name copy];
self.label.text = name;
}
需要说明的一点是,代码 self.photoLayer.contents = (__bridge id) (image.CGImage)来完成图层图片的绘制工作。就是将图片image直接绘制到图层上面,而不是通过addsubView或者addsubLayer的形式添加到photoLayer上面。
然后我们对AvatarView中的元素进行布局工作,代码如下:
- (void)layoutSubviews {
[super layoutSubviews];
//Size the avatar image to fit
CGFloat width = self.bounds.size.width;
CGFloat height = self.bounds.size.height;
CGFloat photoLayerX = (width - self.image.size.width + borderWidth) * 0.5;
CGFloat photoLayerY = (height - self.image.size.height - borderWidth) * 0.5;
self.photoLayer.frame = CGRectMake(photoLayerX, photoLayerY, self.image.size.width, self.image.size.height);
//Draw the circle
self.circleLayer.path = [UIBezierPath bezierPathWithOvalInRect:self.bounds].CGPath;
self.circleLayer.strokeColor = [UIColor whiteColor].CGColor;
self.circleLayer.lineWidth = borderWidth;
self.circleLayer.fillColor = [UIColor clearColor].CGColor;
//Size the layer
self.maskLayer.path = self.circleLayer.path;
self.maskLayer.position = CGPointMake(0, 10);
//Size the label
self.label.frame = CGRectMake(0, height + 10, width, 24);
}
方法bezierPathWithOvalInRect 就是在指定的矩形区域获取它的内切圆或者内切椭圆。而这里是正方形,所以得到的头像显示出来的就是圆形。
然后在ViewController中的viewDidload方法中调用自定义的AvatarView并完成其初始化工作,代码如下:
- (void)viewDidLoad {
[super viewDidLoad];
self.opponentAvatar.image = [UIImage imageNamed:@"empty"];
self.myAvatar.image = [UIImage imageNamed:@"avatar-1"];
self.myAvatar.name = @"Me";
}
至此,UI界面效果如下:
对撞动画分析。
它们看起来是对撞动画,本质上就是让两个View往对方靠近,当达到一定的位置(即两者相遇)时,改变photoLayer和circleLayer的呈现形态(椭圆型)。
我们以屏幕正中心为基准线,最终两者相撞时,它们的中心点的X值分别为:屏幕中心点x - 头像宽度 / 2,屏幕中心点x + 头像宽度 / 2。所以两者最终相撞的代码如下:
CGSize avatarSize = self.myAvatar.frame.size;
// 理论上bounceXOffset的值是avatarSize.width / 2, 但考虑到图片有边框,所以适当进行调节
CGFloat bounceXOffset = avatarSize.width / 1.9;
CGSize morphSize = CGSizeMake(avatarSize.width * 0.85, avatarSize.height * 1.1);
CGPoint rightBouncePoint = CGPointMake(self.view.frame.size.width * 0.5 + bounceXOffset, self.myAvatar.center.y);
CGPoint leftBouncePoint = CGPointMake(self.view.frame.size.width * 0.5 - bounceXOffset, self.myAvatar.center.y);
对上面变量bounceXOffset和morphSize进行说明:
1. 偏离的距离应该是头像宽度 / 2, 但这里是除以1.9, 目的是为了相撞时候,椭圆的显示效果更加明显。
2. morphSize 就是两者相撞时候的形态,明显看出将原来头像的宽度压缩,高度拉伸。这样,到时候取内切图形的时候,就是大家看到的椭圆效果。
在AvatarView中定义一个方法,主要负责动画对撞的效果。
- (void)bounceOffPoint:(CGPoint)bouncePoint morphSie:(CGSize)morphSize;
我们在上面定义的方法中,写入以下代码。
CGPoint originalCenter = self.center;
//Damping值越小,弹性越大
[UIView animateWithDuration:animationDuration delay:0.0 usingSpringWithDamping:0.8 initialSpringVelocity:0.0 options:UIViewAnimationOptionCurveLinear animations:^{
self.center = bouncePoint;
} completion:^(BOOL finished) {
}];
[UIView animateWithDuration:animationDuration delay:animationDuration usingSpringWithDamping:0.7 initialSpringVelocity:0.1 options:UIViewAnimationOptionCurveLinear animations:^{
self.center = originalCenter;
} completion:^(BOOL finished) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
<span style="white-space:pre"> </span>[self bounceOffPoint:bouncePoint morphSie:morphSize];
});
}];
在执行动画之前,先用变量originalCenter保存原始中心点,接着使用弹簧动画,使头像运动到相撞点。停留1秒钟,接着又用一个类似的弹簧动画,循环执行上述动画,这样就完成了两者靠近并且循环执行的效果。效果图如下:
二、碰撞为椭圆效果
- (void)bounceOffPoint:(CGPoint)bouncePoint morphSie:(CGSize)morphSize;
在上述方法的底部继续添加以下代码
CGRect rightMorphFrame = CGRectMake(0, self.bounds.size.height - morphSize.height, morphSize.width, morphSize.height);
CGRect leftMorphFrame = CGRectMake(self.bounds.size.width - morphSize.width, self.bounds.size.height - morphSize.height, morphSize.width, morphSize.height);
CGRect morphedFrame = (originalCenter.x > bouncePoint.x) ? rightMorphFrame : leftMorphFrame;
CABasicAnimation *morphAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
morphAnimation.duration = animationDuration;
morphAnimation.toValue = (__bridge id)([UIBezierPath bezierPathWithOvalInRect:morphedFrame].CGPath);
morphAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
[self.circleLayer addAnimation:morphAnimation forKey:nil];
[self.maskLayer addAnimation:morphAnimation forKey:nil];
让两个头像的View执行CABasicAnimation动画的path效果即可。而toValue的值就是morphedFrame的椭圆效果,所以两者相撞时会产生视觉上面的椭圆效果。