读iOS核心动画高级技巧

iOS 核心动画知识小结

布局、锚点、坐标系

  • UIView布局包括Frame、bounds、center,CALayer 则是 frame、bounds、position
  • 锚点:试图的中点anchorPoint 参数
  • 坐标系: 一个图层的position 是依赖于父试图的bounds,移动父试图的bounds ,自试图也会跟着移动。
    在不同坐标系之间的图层相互转换的方法:
/** Mapping between layer coordinate and time spaces. **/
- (CGPoint)convertPoint:(CGPoint)p fromLayer:(nullable CALayer *)l;
- (CGPoint)convertPoint:(CGPoint)p toLayer:(nullable CALayer *)l;
- (CGRect)convertRect:(CGRect)r fromLayer:(nullable CALayer *)l;
- (CGRect)convertRect:(CGRect)r toLayer:(nullable CALayer *)l;

在判断图层之间的点击时,

  1. 会提供一个containsPoint方法来判断该point 是否在当前的图层中
/* Returns true if the bounds of the layer contains point 'p'. */
- (BOOL)containsPoint:(CGPoint)p;
  1. 点击图层有一个接受标识 hitTest方法,判断被点击的图层,伪代码呈现如下。
- (nullable CALayer *)hitTest:(CGPoint)p;
即:CALayer *layer = [self.view.layer hitTest:point];
if(layer ==  self.targetLayer)xxxxxx

Mac 开发中的坐标翻转

Layer 中有一个属性 geometryFlipped 来对图层构建来进行翻转。
实际应用中,可以写一个分类来进行Mac 的界面布局,使得界面编写可以更加适合iOS 开发人员。

图层中圆角、蒙版的使用

使用图层蒙版为图层设置圆角

CAShapeLayer *shapeLayer = [CAShapeLayer layer];

UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:self.view.frame byRoundingCorners:UIRectCornerTopLeft|UIRectCornerTopRight cornerRadii:CGSizeMake(20, 20)];

shapeLayer.path = path.CGPath;
self.view.layer.mask = shapeLayer;

使用CoreGraphics 来给图层绘制圆角

 UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, [UIScreen mainScreen].scale);

[[UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:radius] addClip];
    
[self.image drawInRect:self.bounds];

self.image = UIGraphicsGetImageFromCurrentImageContext();

UIGraphicsEndImageContext();

上述的几种方式里面 方法一会造成离屏渲染,最好的方式是使用layer addSublayer:layer ,这样会避免上述问题。

使用图层蒙版(Mask)来进行绘制在用Instruments的animation 查看时会出现黄色的离屏渲染信号, 但是使用Instruments的Timer 来查看程序执行耗时的时候,CAShapeLayer 的mask耗时相比较小。 耗时的原因是因为ShapeLayer 使用的是GPU 绘制,而coregraphics 使用的cpu .

图层的之间的过度动画

过度动画是CAtransition参数。type 指定执行动画的类型、fillmode 指定动画在执行之后是否恢复原样。

 CATransition *transition = [CATransition animation];
    transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault];
    transition.type = @"cube";
    transition.subtype = kCATransitionPush;
    transition.fillMode = kCAFillModeForwards;
    transition.duration = 0.5f;
    [self.view.layer addAnimation:transition forKey:@"animation"];

CAShapeLayer

CAShapeLayer 是一个通过矢量图形而不是bitmap 来绘制图层子类。
优点:

  • 渲染快速。CAShapeLayer 使用的是硬件加速,绘制同一图形会比CoreGraphics块。
  • 高效使用内存。一个shapeLayer 不需要想普通的CALayer 一样创建一个寄宿图层,无论多大,都不会占用太多的内存。
  • 不会被图层边界裁掉。
  • 不会出现像素化。

CAGradientLayer

CAGradientLayer 是用来生成两种或更多颜色平滑渐变的。

CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.frame = self.view.frame;
[self.view.layer addSublayer:gradientLayer];

gradientLayer.colors = @[(__bridge id)[UIColor redColor].CGColor,(__bridge id)[UIColor blueColor].CGColor];
//设置gradientlayer 的起始点和截止点
gradientLayer.startPoint = CGPointMake(0, 0);
gradientLayer.endPoint = CGPointMake(1, 1);

CATiledLayer

CATiledLayer 在你如果需要绘制一个很大的图片的时候,从内存中读取是不明智的。在 UIImageimageNamed:方法或者imageWithContentsofFile的方法将会阻塞你的用户界面,至少会导致动画卡顿现象。

CATiledLayer 为载入大图造成的性能问题提供了一个解决方案:将大图分解成小片然后将他们单独按需载入。

使用CATiledLayer 来绘图的时候只需要遵守delegate然后实现相应的方法:

-(void)drawLayer:(CATiledLayer *)layer inContext:(CGContextRef)ctx{
    CGRect bounds = CGContextGetClipBoundingBox(ctx);
    NSInteger x = floorf(bounds.origin.x/layer.tileSize.width);
    NSInteger y = floor(bounds.origin.x/layer.tileSize.height);
    NSString *imageName = [NSString stringWithFormat:@""];
    //使用切割之后的图片来进行绘制
    NSString *imagePath = [[NSBundle mainBundle]pathForResource:imageName ofType:@"png"];
    UIImage *image = [UIImage imageWithContentsOfFile:imagePath];
    UIGraphicsPushContext(ctx);
    [image drawInRect:bounds];
    UIGraphicsPopContext();
}

CABasicAnimation

实例化
CABasicAnimation *basicAnimation = [CABasicAnimation animationWithKeyPath:@"position.x"];
设定动画

动画的属性和说明

属性 说明
duration 动画的时长
repeatCount 重复的次数,不停重复设置为HUGE_VALF
repreatDuration 在该动画内执行,不计算次数
beginTime 设置动画开始的时间,从开始延迟几秒的话,设置为CACurrentMediaTime()+秒数
timeingFunction 设置动画的速度变化
fromValue 所改变属性的起始值
toValue 所改变属性的结束时的值

防止动画结束后回到初始状态

basicAnimation.removedOnCompletion = NO;
basicAnimation.fillMode = KCAFillModeForwards;

代码演示:

CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.x"];
animation.duration = 5.0f;
animation.fromValue = [NSNumber numberWithInteger:0];
animation.toValue = [NSNumber numberWithInteger:5];
animation.fillMode = kCAFillModeForwards;
animation.removedOnCompletion = NO;

CAKeyframeAnimation

CAKeyframeAnimation 不同于CABasicAnimation ,他提供了一系列的值来做动画效果。起源于传动动画,意思是指主导的动画在显著改变发生时重绘当前帧(也就是关键帧),每帧之间剩下的绘制将交给系统来完成。CAKeynimation 也是同样的道理:你提供了显著的帧,然后CoreAnimation在每帧之间进行插入。

    //使用贝塞尔曲线绘制 三元曲线
    UIBezierPath *path = [[UIBezierPath alloc]init];
    [path moveToPoint:CGPointMake(0, 200)];
    [path addCurveToPoint:CGPointMake(200, 400) controlPoint1:CGPointMake(50, 100) controlPoint2:CGPointMake(100, 200)];
    CAShapeLayer *shapeLayer = [CAShapeLayer layer];
    shapeLayer.fillColor = [UIColor clearColor].CGColor;
    shapeLayer.strokeColor = [UIColor redColor].CGColor;
    shapeLayer.lineWidth = 3.0f;
    shapeLayer.path = path.CGPath;
    [self.view.layer addSublayer:shapeLayer];
    // 为上面的bgview 添加关键帧动画
    CAKeyframeAnimation *keyAnimation = [CAKeyframeAnimation animation];
    keyAnimation.keyPath = @"position";
    keyAnimation.duration = 4.0;
    keyAnimation.path = path.CGPath;
    keyAnimation.removedOnCompletion = NO;
    keyAnimation.fillMode = kCAFillModeForwards;
    [bgView.layer addAnimation:keyAnimation forKey:@"layer"];

得到的结果如下:
809a3d6450e6a52ec816022c9ebb1fa1.jpeg@w=50h=50

在使用关键帧动画时也可以给keyAnimationvalues属性添加一组数字。

CAAnimationGroup 的使用

CAAnimationGroupCAKeyAnimation 是仅仅用于单独的属性,而CAAnimationGroup 可以把上述的animations属性组合到一起。

动画过程中取消动画

CAKeyAnimationCABasicAnimation 的父类CAPropertyAnimation 提供了一个api 来移除当前指定animation:[bgView.layer removeAnimationForKey:@""]

或者移除所有animation动画:
[bgView.layer removeAllAnimations];

高效绘图操作

软件绘图

在iOS中,软件绘图通常是有CoreGraphics 框架完成。相比于Core Animation和OpenGL,Core Graphic要慢。CALayerDelegate中的-drawLayer:inContext:方法或者UIView中的-drawRect:方法,图层就会创建一个绘制上下文,这个上下文需要的大小的内存可以从算式中得出, 如果是3x的屏幕上就会占用更多的内存:

width * heigt * 4bitm * screenScale

软件绘图的消耗代价比较高,提高绘制性能就要减少不必要的软件绘制,即使用CoreGraphic绘制。

矢量图形

在使用CoreGraphic 进行绘制的时候,通常是因为图片或者图层效果不能轻易得到矢量图形,一个矢量绘图可能包含下面几种:

  • 任意的多边形
  • 斜线或者曲线

举例说明在使用coregraphic 进行绘制

/** 绘图所用曲线(成员属性)*/
@property(nonatomic,strong)UIBezierPath *path

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    CGPoint point = [[touches anyObject] locationInView:self.view];
    //用户手指点击作为起点
    [self.drawPath moveToPoint:point];
}

-(void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{

    CGPoint point = [[touches anyObject]locationInView:self.view];
    //开始绘制线路
    [self.drawPath moveToPoint:point];
    //绘制完成之后进行重绘,该方法会调用当前视图的drawrect 方法
    [self.view setNeedsDisplay];
    
}

/**在重绘图层时进行颜色填充*/
-(void)drawRect:(CGRect)rect{
  
    [[UIColor clearColor] setFill];
    [[UIColor redColor] setStroke];
    [self.drawPath stroke];
    
}

用户在每次进行手势移动的时候就会进行图层的重绘,对app 的性能而言就是一种灾难。

这样的实现问题在于我们画的越多,程序就会越慢。

CoreAnimation提供了专门的类,并给他们提供硬件支持。CAShapeLayer 可以用来绘制多边形,直线和曲线,CAGradientLayer 可用来绘制渐变,CATextLayer可以绘制文本,这些总体上都比Coregraphics 更加快 ,同时他们也避免创造了一个寄宿图。

使用CAShapeLayer 来实现上述功能,只需要将touchmoved方法里面的setNeedDisplay换成self.layer.path = self.path.CGPath

图像IO

图片的加载需要经过一个过程5555cb07bc3d60dfc200a8cf2ea8a60c.jpeg

具体的图片加载和解压,可以看FFMPeg 的使用,其中提到的metadata 和imageBuffer以及decode 的过程。

解压过程是一个大量占用CPU资源的工作,因此UIImage 会retain存储解压后信息的Image Buffer以便给重复的渲染工作提供信息,Image Buffer与图片的实际尺寸有关,与图片文件的大小无关。

相关参考资料:WWDC2018图像最佳实践 Image and Graphics Best Practices

绘图实际消耗的时间通常并不是影响性能的因素。图片消耗很大一部分内存,而且不太可能把需要显示的图片都保留在内存里面,所以需要在应用运行的时候周期性地加载和卸载图片。 图片文件加载的速度被CPU 和 IO 同时影响,虽然iOS 设备中的闪存已经比传统的硬盘快很多,但是仍然比 RAM 慢了将近200倍。

拓展:在对本地编译App速度的优化可以从Xcode缓存读取来进行,相关操作可参考一下链接 加快Xcode编译读写

在加载本地可以缓存的图片文件时可以使用: imageNamed方法,而imageWithContentsOfFile: 不会进行缓存。
但是上述的方法只能加载本地存在的图片,所以我们要用到另外一种方式:

ImageIO 框架 >ImageIo框架详解


    /** 加载本地图片**/
    //  NSString *currentPath = [[NSBundle mainBundle]pathForResource:@"liu_04" ofType:@"png"];
    // NSURL *url = [NSURL fileURLWithPath:currentPath];
    NSString *currentPath = @"云端地址";
    NSURL *url = [NSURL URLWithString:currentPath];
    CGImageSourceRef imageSourceRef = CGImageSourceCreateWithURL((CFURLRef)url, NULL);
    CGImageRef imageRef = CGImageSourceCreateImageAtIndex(imageSourceRef, 0, NULL);
    CFRelease(imageSourceRef);
    UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(0, 400, 100, 100)];
    imageView.image  =  [UIImage imageWithCGImage:imageRef];
    [self.view addSubview:imageView];

猜你喜欢

转载自blog.csdn.net/Abe_liu/article/details/85704334