CALayer动画与UIView动画的使用场合
我本来对于CALayer动画的一些使用场合比较疑惑,可以直接用UIView块动画为什么要费心思写CALayer动画呢?所以首先,我想说一下CALayer动画与UIView动画的使用场合。
1、UIView属于UIKit框架,属于苹果原生框架,而CALayer属于QuartzCore框架,而后者是可以跨平台的,所以当需要跨平台的时候就用CALayer动画喽。
2、UIView可以与用户交互,而CALayer只用于展示,所以,如果需要与用户交互的动画就用UIView动画,不需要交互的,就用CALayer。
CALayer进行有间隔的重复性动画中的问题
使用CALayer显式动画的好处是对于时间的可控,尤其是使用CAKeyFrameAnimation的时候。那么如何进行有间隔的重复性动画呢?有人说直接设置repeatcount就可以了,注意我说的是有时间间隔的重复性动画,很多时候我们需要在动画结束后暂停一段时间在开始动画而不是立即进行重复,以使得重复性动画并不是那么突兀。那么如何做呢?
当我们把一个animation添加到图层时,可以给该animation设置代理,用以监听动画结束,在监听到动画结束时,可以remove掉该图层上的所有动画,然后重新alloc一个animation,添加到该图层上即可。但是,这里有个问题是当同时有很多动画时,如何在该方法中准确拿到到底是哪个layer结束了动画呢?经过查阅资料,主要有这两种方法:
1、直接贴代码:
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag{
if ([self.firstFingerLayer animationForKey:@"firstFingerAnimation"] == anim) {
if (flag){
//do your work
}
}
else if ([self.secondFingerLayer animationForKey:@"secondFingerAnimation"] == anim) {
if (flag) {
//do your work
}
}
else if ([self.thirdFingerLayer animationForKey:@"thirdFingerAnimation"] == anim){
if (flag) {
//do your work
}
}
}
当我们将animation添加到layer上时,可以为animation传入一个key值,这里就可以用该key获取到对应layer上的animation,如果与代理方法传入的anim相等,则只需判断flag为YES,就可以知道哪个layer结束了动画,然后再重新进行动画即可。 注意:这里的animation的removeOnCompletion属性必须为NO,不然通过key获取到的animation为nil。
2、KVC
像所有的NSObject子类一样,CAAnimation实现了KVC(键-值-编码)协议,于是你可以用-setValue:forKey:和-valueForKey:方法来存取属性。但是CAAnimation有一个不同的性能:它更像一个NSDictionary,可以让你随意设置键值对,即使和你使用的动画类所声明的属性并不匹配。这意味着你可以对动画用任意类型打标签。在这里,我们给UIView类型的指针添加的动画,所以可以简单地判断动画到底属于哪个视图,然后在委托方法中用这个信息正确地更新钟的指针。
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIImageView *hourHand;
@property (nonatomic, weak) IBOutlet UIImageView *minuteHand;
@property (nonatomic, weak) IBOutlet UIImageView *secondHand;
@property (nonatomic, weak) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//adjust anchor points
self.secondHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f);
self.minuteHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f);
self.hourHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f);
//start timer
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(tick) userInfo:nil repeats:YES];
//set initial hand positions
[self updateHandsAnimated:NO];
}
- (void)tick
{
[self updateHandsAnimated:YES];
}
- (void)updateHandsAnimated:(BOOL)animated
{
//convert time to hours, minutes and seconds
NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
NSUInteger units = NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit;
NSDateComponents *components = [calendar components:units fromDate:[NSDate date]];
CGFloat hourAngle = (components.hour / 12.0) * M_PI * 2.0;
//calculate hour hand angle //calculate minute hand angle
CGFloat minuteAngle = (components.minute / 60.0) * M_PI * 2.0;
//calculate second hand angle
CGFloat secondAngle = (components.second / 60.0) * M_PI * 2.0;
//rotate hands
[self setAngle:hourAngle forHand:self.hourHand animated:animated];
[self setAngle:minuteAngle forHand:self.minuteHand animated:animated];
[self setAngle:secondAngle forHand:self.secondHand animated:animated];
}
- (void)setAngle:(CGFloat)angle forHand:(UIView *)handView animated:(BOOL)animated
{
//generate transform
CATransform3D transform = CATransform3DMakeRotation(angle, 0, 0, 1);
if (animated) {
//create transform animation
CABasicAnimation *animation = [CABasicAnimation animation];
[self updateHandsAnimated:NO];
animation.keyPath = @"transform";
animation.toValue = [NSValue valueWithCATransform3D:transform];
animation.duration = 0.5;
animation.delegate = self;
[animation setValue:handView forKey:@"handView"];
[handView.layer addAnimation:animation forKey:nil];
} else {
//set transform directly
handView.layer.transform = transform;
}
}
- (void)animationDidStop:(CABasicAnimation *)anim finished:(BOOL)flag
{
//set final position for hand view
UIView *handView = [anim valueForKey:@"handView"];
handView.layer.transform = [anim.toValue CATransform3DValue];
}
我们成功的识别出每个图层停止动画的时间,然后更新它的变换到一个新值,很好。
不幸的是,即使做了这些,还是有个问题,这段在模拟器上运行的很好,但当真正跑在iOS设备上时,我们发现在-animationDidStop:finished:委托方法调用之前,指针会迅速返回到原始值。问题在于回调方法在动画完成之前已经被调用了,但不能保证这发生在属性动画返回初始状态之前。所以这里要注意设置animation的fillMode属性值为 kCAFillModeForwards,使动画结束时可以保持最新的状态。
不幸的是,即使做了这些,还是有个问题,这段在模拟器上运行的很好,但当真正跑在iOS设备上时,我们发现在-animationDidStop:finished:委托方法调用之前,指针会迅速返回到原始值。问题在于回调方法在动画完成之前已经被调用了,但不能保证这发生在属性动画返回初始状态之前。所以这里要注意设置animation的fillMode属性值为 kCAFillModeForwards,使动画结束时可以保持最新的状态。
隐式动画与显式动画的疑惑
之前我认为当设置animation的fillMode为kCAFillModeForwards,则动画结束后其真实属性值会发生变化,实则不然,所有使用CAAnimation添加到图层的动画,都没有改变其实际的属性值,只是改变了其PresentationLayer的属性值,所以原来的属性没有发生任何改变,所以如果需要依赖变化之后的属性值进行接下来的一些操作,就需要在动画结束时更新其属性值了,这里需要在animationDidStop动画这样做:
//更新layer实际属性值
[CATransaction begin];
[CATransaction setDisableActions:YES]; //注意这句代码,需要手动关闭隐式动画,否则,更改属性值会带有隐式动画效果,这里是不期望的
self.secondHairView.layer.transform = CATransform3DMakeTranslation(0, 0, 0);
self.secondMarqueeLayer.transform = CATransform3DMakeTranslation(0, 0, 0);;
[CATransaction commit];
[self.secondHairView.layer removeAllAnimations];//如果接下来还要进行动画,需先清除之前的动画,否则会有问题。
[self.secondMarqueeLayer removeAllAnimations];
在一个与UIView关联的Layer上向CATransaction提交动画时会没有效果,因为UIView禁用了隐式动画,关于UIKit如何禁用隐式动画请参见这里,但是若是在UIView的animation的block中更改UIView关联的Layer属性时,依然有动画效果。