关于Core Animation的一些初步探索


所谓Core Animation,顾名思义就是用来做动画的,它包含了一些Objective-C类,这些类都在Quartz Core框架中。

用它的原因也无需多说,首先是性能很好,使用了GPU硬件加速;其次是接口易用,毕竟是Objective-C,不需要像OpenGL ES一样完全和C打交道。

不过要掌握它也很费劲,这2天就遇到了不少问题,于是记录在此。


首先说下起因吧,其实我主要是想模拟UITableViewCell中,对imageView赋值时的动画效果。即从nil更改为一个UIImage对象后,图像的大小会从0逐渐放大,而右侧的textLabeldetailTextLabel也会右移。

这个默认的实现应该是用UIView做的,先调用UIViewbeginAnimations:context:方法,然后修改imageViewtextLabeldetailTextLabelframe,再调用UIViewcommitAnimations方法,就会自动使用动画来表现frame的更改了。

不过使用UIView也就意味着绘制性能的下降,造成滚动时不太流畅,于是我想到了轻量级的CALayer


CALayer就是一个层。和绘图软件中的图层一样,CALayer也可以叠加,最终混合成一个可见的视图。

实际上UIView对象就有个layer属性,那就是它的根层,添加到这个层上面的CALayer对象就会随这个UIView一起绘制出来了。

CALayer *layer = [[CALayer alloc] init];

layer.frame = imageRect;

layer.contents = (id)image.CGImage;

cell.imageLayer = layer;

[cell.contentView.layer addSublayer:layer];

[layer release];

上述代码中,把layer.contents赋值为一个CGImageRef对象后,就会在这个layer中显示这张图像了。


层还可以实现一些特效,例如圆角和阴影:

layer.cornerRadius = 10.0;

layer.masksToBounds = YES;

这段代码就给层加上了10像素半径的圆角,不过你会发现绘制性能显著下降了,所以如果不是必须的话,可以先CGContextRef中画出圆角图像,再绘制到layerview中。不过原文中没有设置scale,在iPhone 4下效果很差,因此最后创建时要改成[UIImage imageWithCGImage:imageMasked scale:[[UIScreen mainScreen] scale] orientation:UIImageOrientationUp]


而如果要显示文字的话,可以继承CALayer,然后改写drawInContext:方法;或者实现delegatedrawLayer:inContext方法。代码可见Add text to CALayer


不过当时我用的是CATextLayer这个子类,用法也很简单:

cell.textLayer = [CATextLayer layer];

textLayer.frame = textRect;

textLayer.truncationMode = kCATruncationEnd;

textLayer.foregroundColor = black;

textLayer.backgroundColor = white;

textLayer.wrapped = YES;

textLayer.fontSize = 17;

textLayer.string = text;

[cell.layer addSublayer:textLayer];

然而用了以后,我感觉被Apple坑了


首先是啥都没看到,查了下文档,发现默认的字体颜色是白色,于是手动设置为黑色:

textLayer.foregroundColor = [UIColor blackColor].CGColor;

接着是文字过长时,会直接切断文字,而不显示省略号,并且有些文字可能只显示一半。如果把textLayer.wrapped设为NO,确实可以让它以省略号的形式截断,但是就只能显示一行了貌似无解,除非自行计算长度并截断。

然后是在iPhone 4上测试时,发现文字没有抗锯齿。搜索了一下,发现需要手动设置contentsScale

textLayer.contentsScale = [[UIScreen mainScreen] scale];

还有在滚动时,因为table cell是重用的,更改文字会出现短暂的残像。最后发现更改contents属性也会产生动画,因此需要去掉这个动画效果:

NSDictionary *actions = [[NSDictionary alloc] initWithObjectsAndKeys:[NSNull null], @"contents", nil];

textLayer.actions = actions;

[newActions release];

此外,采用相同字体和字号的时候,CATextLayer里的文本会比直接用NSStringdrawInRect:withFont:lineBreakMode:方法粗一些,原因不明。

另外,我还发现CALayer默认是透明的,改成不透明后就显示成黑色了,而且改背景色也没用,原因亦不明。

于是最终我去掉了textLayer,而是直接调用NSStringdrawInRect:withFont:lineBreakMode:方法。由于动画时间很短,倒也不影响视觉效果。


创建CALayer搞定了,接下来就该实现动画了。和UIView一样,修改CALayer的属性就会自动生成动画了。因此初始时可以把imageLayerbounds的长宽都设为0,然后赋值为一个正数,它就会自动放大了。

不过默认的动画时间很短,如果要修改的话,就需要用到CATransaction了:

[CATransaction begin];

[CATransaction setAnimationDuration:1];

imageLayer.frame = imageRect;

[CATransaction commit];

CATransaction还有其他的作用,最重要的功能就是让其中的所有动画在commit时并行执行,以使它们同步。这样如果同时放大图像并缩小文字的话,就不会出现图像比文字先变化,而覆盖文字的情况。

此外还可以用下述语句来禁用一些动画,不过并非万能:

[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];


当然也可以自定义动画,例如使用CABasicAnimation

CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"];

animation.duration = 1;

CGPoint point = {100, 0};

animation.byValue=[NSValue valueWithCGPoint:point];

layer.position = position;

[layer addAnimation:animation forKey:@"positionAnimation"];

这段代码就让layer右移了100 point。不过执行完后,你会发现它又立刻回到原处了。


实际上Core Animationlayer维护了3tree

  1. 第一种是我们可以直接访问的layer tree,对CALayer的属性赋值后,就会立刻更改。
  2. 第二种是presentation tree,它存储了动画进行过程中的属性。因此在整个动画过程中,它会不断变化。我们可以通过它来得知layer当前的显示状态。
  3. 第三种是render tree,它根据presentation tree来计算,我们不能访问它。

知道这个后就清楚原因了。上例中我们执行了一个动画,它的presentation tree一直在变化,直到动画结束。接着它发现layer treepresentation tree不一致,于是又自动更新了layer的位置,所以就还原了。

因此在执行完动画后,立刻更改CALayer的属性,应该就不会自动还原了,不过我懒得去试了。


现在就来实现一下drawRect:方法:

- (void)drawRect:(CGRect)rect {

    [CATransaction begin];

    if (shouldAnimatied) {

        [CATransaction setAnimationDuration:.3];

    } else {

        [CATransaction setAnimationDuration:0];

        [CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];

    }


    if (image) {

        if (imageLayer.hidden) {

            imageLayer.hidden = NO;

            imageLayer.frame = imageRect;

        }

        imageLayer.contents = (id)image.CGImage;

        self.image = nil;

        [textString drawInRect:textRect withFont:font lineBreakMode:UILineBreakModeTailTruncation];

    } else {

        imageLayer.hidden = YES;

        imageLayer.frame = imageRect0;

        [textString drawInRect:textRect0 withFont:font lineBreakMode:UILineBreakModeTailTruncation];

    }

    [CATransaction commit];

    shouldAnimatied = NO;

}

其中shouldAnimatied是在drawRect:前设置的,只有当前cell可见,并且图像下载完时,才需要用动画的方式显示出来。

当没有图像时,就直接显示文字,并将imageLayer隐藏,长宽设为0;而有图像时,就将其显示出来,长宽增大,而文本则画在右侧。

目前这个版本在图像全部载入过的情况下,滚动时能稳定在55fps以上,但显示新图像时会降低到低于30fps,并明显感到很卡。而直接调用[image drawAtPoint:imagePoint]的话,无论何时基本都能维持在55fps以上。


所以考虑了另一个方案,即平时隐藏imageLayer,通过调用drawAtPoint:来显示图像。只有在动画更新时才显示imageLayer

- (void)drawRect:(CGRect)rect {

    if (shouldAnimatied) {

        [CATransaction begin];

        [CATransaction setAnimationDuration:.3];

        imageLayer.hidden = NO;

        imageLayer.frame = imageRect;

        imageLayer.contents = (id)image.CGImage;

        [CATransaction commit];

        self.image = nil;

        [textString drawInRect:textRect withFont:font lineBreakMode:UILineBreakModeTailTruncation];

        shouldAnimatied = NO;

    } else {

        if (!imageLayer.hidden) {

            [CATransaction begin];

            [CATransaction setAnimationDuration:0];

            [CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];

            imageLayer.hidden = YES;

            imageLayer.frame = imageRect0;

            [CATransaction commit];

        }

        if (image) {

            [image drawAtPoint:imagePoint];

            self.image = nil;

            [textString drawInRect:textRect withFont:font lineBreakMode:UILineBreakModeTailTruncation];

        } else {

            [textString drawInRect:textRect0 withFont:font lineBreakMode:UILineBreakModeTailTruncation];

        }

    }

}

现在这个版本也基本上稳定到55fps以上了,总算大功告成了~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值