iOS之CALayer相关使用

CALayer介绍

CALayer在概念和UIView类似,也是一些被层级关系树管理的矩形块,可以包含图片、文字、背景色等内容。和UIView的最大不同是不能够处理与用户的交互。

每一个UIView都有个CALayer实例的图层属性,被称为backing layer(支持层),由视图负责创建并管理这个图层,以确保当子视图在层级关系中添加或被移除时,对应的关联图层也有相同的操作。

  • 图层与视图的底层关系


    1.UIView、UIColor、UIImage都定义于UIKit框架中;
    2.CALayer定义在QuartzCore框架中的CoreAnimation中;
    3.CGImageRef、CGColorRef两种数据类型是定义在CoreGraphics框架中
    4.QuartzCore框架和CoreGraphics框架可以跨平台使用,在iOS和Mac OS上都能使用,但是UIKit只能在iOS中使用;为了保证可移植性,QuartzCore是不能直接使用UIImage和UIColor的,如果使用需要将其转化为CGImageRef,CGColorRef
  • 为什么使用图层?

    在智能手机等设备上,用户希望他们的一切操作都很快。保持连贯的帧速率很关键,这样用户才会觉得「丝滑流畅」。在 iOS 中,帧速率是每秒 60 帧。为了保持系统能在这个速率下运作,一个直接运行在 GPU 上、非常基础但是功能强大的图形功能层诞生了,它就是 OpenGL

    OpenGL 提供了大部分底层的(而且是最快的)访问权限,直达 iOS 设备的图像硬件。然而你需要做出权衡:OpenGL 太靠近底层了,即便是为了完成最简单的任务,都需要大量的代码。

    为了能够缓解这个问题,苹果创建了 Core Graphics,它提供了更高级一些的方法,代码量也随之更少。使用 Core Graphics 的初衷是应用到一些比较底层的功能上。为了使 Core Graphics 的使用更简单,苹果创建了 Core Animation。它提供了 CALayer 类,并且允许一些基本的底层图像操作。

    当苹果认为 Core Animation 中的很多高级高级功能在常规应用中并不总是需要的时候,UIKit 就诞生了,它提供了 iOS 中最顶层的图像访问权限。此设计方案的优点是,在你的应用中,你可以选择你需要的图像访问级别并且应用它,允许你挑选并精准地选择所需要的功能量级,使你不必编写无用的代码。
    缺点是较高级别的图形 API 提供的功能比较少。讲这个故事是为了说明:因为 CALayer 的存在,iOS 系统可以洞悉你的应用中的视图层次结构,快速生成层次结构的位图信息,然后将其传递到 Core Graphics 中去,最终到达 OpenGL,这样通过设备的 GPU 处理后图像就呈现在屏幕上了。尽管在大多数情况下都不需要直接使用 CALayer,但是较为底层的 API 为用户提供了一些更灵活的自定义方案,我们将在本文中讨论。

CALayer的使用

最简单的使用方式

CALayer *redLayer = [CALayer layer];
redLayer.backgroundColor = [UIColor redColor].CGColor;
redLayer.frame = CGRectMake(100, 100, 100, 100);
[self.view.layer addSublayer:redLayer];

contents属性

contents属性被定义为id类型,意味着它可以是任何类型的对象,实际上给contents属性赋任何值都是可以编译通过的,但是,在实践中,如果contents赋值不是CGImage,得到的图层是空白的。

实际上,真正要赋值的类型是CGImageRef,他是一个指向CGImage结构的指针。UIImage有一个CGImage属性,返回一个CGImageRef,因为他不是一个Cocoa对象,而是一个CoreFoundation类型,需要通过桥接__bridge关键字进行转换,如下

layer.contents = (__bridge id)(image.CGImage);

注意:通过此方法,可以利用CALayer在一个普通的UIView中显示一张图片了。
疑问:那为什么要把contents属性定义成泛型呢?
因为在Mac OS系统上contents属性对CGImageNSImage都起作用,但是在iOS系统中将UIImage类型的值赋给它,得到的依然是一个空白图层。

  • UIView方法绘制自定义寄宿图

    给contents赋值CGImage的值并不是唯一设置寄宿图的方法,我们可以使用CoreGraphics直接绘制寄宿图,即通过继承UIView并实现-drawRect:的方式。

    -drawRect: 方法是UIView没有默认实现的方法,因为寄宿图并不是必须的;但如果UIView检测到此方法被实现了,此方法会被自动调用,然后我们就可以在其中使用CoreGraphics绘制自己需要的内容了;下面的代码就演示了drawRect自定义绘制寄宿图的具体操作,实现一个环形的绘制:

    #import "CustomView.h"
    
    @implementation CustomView
    
    - (void)drawRect:(CGRect)rect {
        [self testFunctionWithTwo];
    }
    
    ///方式一
    - (void)testFunctionWithOne {
        CGRect rect = self.bounds;
        //计算半径
        CGFloat radius = MIN(rect.size.width, rect.size.height) * 0.5;
        //创建圆的路径
        UIBezierPath *bezierPath = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:radius];
        [[UIColor redColor] set];
        [bezierPath fill];
    }
    
    ///方式二
    - (void)testFunctionWithTwo {
        CGRect rect = self.bounds;
        //获取画布
        CGContextRef context = UIGraphicsGetCurrentContext();
        //画笔颜色
        CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
        //画笔宽度
        CGFloat lineWidth = 5;
        CGContextSetLineWidth(context, lineWidth);
        //圆点图标
        CGFloat conterX = CGRectGetWidth(rect)/2.0;
        CGFloat conterY = CGRectGetHeight(rect)/2.0;
        CGFloat cusRadius = (self.frame.size.width/2.0 - lineWidth/2.0f);
        double PI = 3.14159265358979323846;
        
        //绘制路径:初始角度,结束角度
        CGContextAddArc(context, conterX, conterY, cusRadius, 1.5*PI, 1.5*PI + 2*PI, NO);
        CGContextDrawPath(context, kCGPathStroke);
    }
    @end

    绘制效果如下


    特别注意1:如果没有自定义绘制任务不需要寄宿图,就不要在子类中写一个空的-drawRect:方法,否则会造成CPU资源和内存的浪费;

    特别注意2:如果我们将绘制过程的角度参数改为动态,并结合定时器调用-setNeedsDisplay方法,就可以实现环形动画的效果; 

    特别注意3:其实UIView内部负责绘制的layer图层的代理其实就是UIView本身,也是就是说view.layer.delegate = view,相当于这样一个逻辑。而-drewRect:只不过是layer代理方法封装的产物。

  • CALyer方法绘制自定义寄宿图

    虽然-drawRect:方法是实现了自定义寄宿图绘制,但事实上还是底层CALayer重绘并保存了因此产生的图片;CALayer有一个可选的delegate属性,实现了CALayerDelegate非正式协议,当CALayer需要一个内容特定信息时,就会从协议中请求;而当需要被绘制时,CALayer会通过如下的方法来请求代理给它提供寄宿图;

    ///方法1: 可以直接设置contents属性;
    - (void)displayLayer:(CALayer *)layer;
    
    ///方法2: 不在实现方法1时,CALayer就会转而尝试调用此方法;
    - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;

    在调用方法2之前,CALayer会创建了一个合适尺寸的空寄宿图(尺寸由bounds和contentScale决定)和一个CoreGraphics的绘制上下文环境,为绘制寄宿图做准备,并将其以ctx参数传入。

    现在我们以方法2为例,演示CALayer绘制自定义寄宿图的过程,具体代码如下:

    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        [self renderViews];
    }
    #pragma mark - Initail Methods
    
    - (void)renderViews {
        CALayer *blackLayer = [CALayer new];
        blackLayer.backgroundColor = [UIColor blackColor].CGColor;
        blackLayer.frame = CGRectMake(0.0f, 0.0f, 100.f, 200.f);
        blackLayer.position = self.view.center;
        blackLayer.contentsScale = [UIScreen mainScreen].scale;
        blackLayer.delegate = self;
        [self.view.layer addSublayer:blackLayer];
        [blackLayer display];
    }
    
    - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
        CGContextSetLineWidth(ctx, 10.0f);
         CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor);
         CGContextStrokeEllipseInRect(ctx, layer.bounds);
    }
    @end

    效果如下

     
    代码分析:

    • 1.主动绘制

      我们需要显式的调用-display方法;这不同于UIView,当图层显示到屏幕上时,CALayer不会自动重绘它的内容,CALayer把重绘 的决定权交给了开发者;
    • 2.绘制特点

      尽管没有使用masksToBounds属性,但示例中绘制的视图依然被裁减了,这是因为通过CALayer绘制宿寄图并没有对超出边界外的内容提供绘制支持; 
    • 3.设置代理

      CALayerDelegate不能是UIView和UIViewController,如上述代码的演示就会造成崩溃;UIView本身携带的Layer的代理就是自己,如果将一个layer的代理设置成它,那它本身的layer就会受影响,通常表现为表现为野指针崩溃;而UIViewController在经历Push和Pop之后也可能被释放,造成野指针崩溃;所以,对于这个问题的解决方案是:创建继承于NSObject的类,用于实现CALayerDelegate并管理CALayer的绘制逻辑; 
    • 使用总结:

      当我们需要自定义寄宿图时,其实不必要实现displayLayer:和-drawLayer:InContext:方法来绘制寄宿图。通常的做法还是实现UIView的-drawRect方法,这样UIView就会自动帮我们完成剩下的工作,包括需要重绘的时候调用-display方法;

contentsGravity属性

使用contentsGravity属性来设置图层中内容在边界中如何对齐,效果等同于UIView的contentMode。该属性的值都是字符串类型,官方解释如下:

/* A string defining how the contents of the layer is mapped into its bounds rect. Options are center, top, bottom, left,right, topLeft, topRight, bottomLeft, bottomRight, resize, resizeAspect, resizeAspectFill. The default value is resize. Note that "bottom" always means "Minimum Y" and "top" always means "Maximum Y". */

使用方式

layer.contentsGravity = kCAGravityCenter;

contentsScale属性

使用contentsScale属性来定义寄宿图的像素尺寸和视图大小的比利,默认值为1.0。属于支持高分辨率(又称为Hi-DPI或Retina)屏幕机制的一部分,用来判断在绘制图层时候为寄宿图创建的空间大小,和需要显示的图片的拉伸度(假设没有设置contentsGravity属性)。设置为1.0,表示将会以每个点1个像素绘制图片,设置为2.0,将以每个点2个像素绘制图片。

使用代码处理寄宿图时,需要手动设置图层的contentsScale,

redLayer.contentsScale = [UIScreen mainScreen].scale;

maskToBounds属性

使用该属性来决定是否显示超出边界的内容,类似于UIView的clipsToBounds属性。

contentsRect属性

使用该属性来显示寄宿图的一个子域,它使用单位坐标,是一个相对值。(点就是虚拟的像素,也成为逻辑像素。在标准设备上,一个点就是一个像素,但在Retain设备上,一个点等于 2 * 2个像素)

默认的contentsRect是{0,0,1,1},意味着整个寄宿图都是可见的。主要应用是图片拼合,因为单张大图比多张小图载入更快,可以有效提高载入性能

contentsCenter

需要注意的是contentsCenter其实是一个CGRect,它定义了一个固定的边框和一个在图层上可拉伸的区域,默认值是{0,0,1,1}。改变contentsCenter的值并不会影响到寄宿图的显示,除非这个图层的大小改变了,才能看到效果。

举例说明:将contentsCenter设置为{0.25,0.25,0.5,0.5},效果如下

还可以在IB中直接设置

最终实现的效果和UIImage的resizableImageWithCapInsets:方法类似。

frame、bounds、position

1、position类似于UIView中的center,表示相对于父图层anchorPoint所在的位置

2、frame是计算属性,是根据bounds,position,transform计算而来的,通常情况下,frame的宽高和bounds的宽高是相同的,所以当其中任何一个发生改变时候,他都会发生改变。

如以下代码执行情况为:

UIView *redView = [[UIView alloc]init];
redView.backgroundColor = [UIColor redColor];
redView.frame = CGRectMake(100, 100, 100, 100);
[self.view addSubview:redView];
    
redView.transform = CGAffineTransformMakeRotation(M_PI_4);
NSLog(@"frame %@ bounds %@",NSStringFromCGRect(redView.frame),NSStringFromCGRect(redView.bounds));

结果为:

frame {{79.289321881345259, 79.289321881345245}, {141.42135623730951, 141.42135623730951}} bounds {{0, 0}, {100, 100}}

anchorPoint 锚点

视图的center属性和图层的position属性都指定了anchorPoint相对于父图层的位置。默认情况下,anchorPoint位于图层的中点,所以图层会以这个点为中心放置。(注:UIView没有暴露锚点的属性,也是视图的position可以被称为center的原因)

锚点anchorPoint是用单位坐标来描述的,而且x和y的值可以大于1或小于0,使他可以放置在图层范围之外。

在聊锚点之前,我们有必要重新认识一下center属性和position属性。我们知道,这两个属性都是设置当前视图(或图层)的中心点在父视图(或父图层)坐标系统中位置,从而确定当前视图(或图层)在父视图(或父图层)位置。作为视图这个点是当前视图的中心点,但是对于图层来说可能就不是中心点了。

在CALayer中有一个属性叫做anchorPoint,也就是我们接下来要说的锚点。但是在UIView中,这个属性并没有被接口暴露出来,这也是为什么UIView的position属性被叫做center的原因,因为center包含了位置+中心两层含义,center其实是由positionanchorPoint两个属性叠加得到的,此时anchorPoint对应的值是默认的中心点位置。

那么positionanchorPoint到底代表什么呢?下面我们通过一个物理模型“图钉说”来帮助大家理解一下(请牢记这个模型):

我们可以把当前图层比作一张纸,父图层比作一面墙。我们通过positionanchorPoint属性将当前图层添加到父图层上,其实就相当于拿一枚图钉将这张纸固定到这面墙上。那么positionanchorPoint分别代表这个物理模型中的什么呢?这里图钉穿过纸张的位置就是anchorPoint锚点的位置,图钉扎在墙上的位置就是position的位置。也就是说anchorPoint锚点的位置是相对于当前图层的,而position是相对于父图层的。

【 这里要说明一点的是:anchorPoint使用的坐标系统跟我们上面提到的contentsRect是一样的,使用的是相对坐标系统,也就是说左上角的坐标是{0,0}右下角的坐标是{1,1}。并且我们也可以通过设置小于0或大于1的值,把锚点放置在图层范围之外。】

看完上面这个物理模型之后,我们再来思考这样一个问题:如果在position不变的情况下改变anchorPoint,此时会发生什么变化?继续利用上面的模型,相当于我们将图钉摘下,从纸的另一个位置穿过,然后重新扎回上次的位置。此时图钉在墙上的位置不变,但是纸张相对墙的位置却发生了变化,其实也就是frame发生了变化。

关于anchorPointposition就先聊这么多,如果想找一个应用场景通过代码来深入理解一下anchorPointposition的话,可以看下面的示例,示例中用到了锚点的知识。这里还要给大家补充另外一个知识点:当一个图层通过transform进行旋转的时候,图层所围绕的中心就是锚点,这跟我们上面提到的物理模型也是相吻合的。

举例说明,实现一个简单的钟表:

@interface ViewController ()
@property(nonatomic,strong) UIView *hourView;
@property(nonatomic,strong) UIView *minuteView;
@property(nonatomic,strong) UIView *secondView;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    self.view.backgroundColor = [UIColor cyanColor];
    
    self.hourView = [[UIView alloc]init];
    self.hourView.backgroundColor = [UIColor greenColor];
    self.hourView.bounds = CGRectMake(0, 0, 20, 100);
    self.hourView.center = self.view.center;
    [self.view addSubview:self.hourView];
    self.hourView.layer.anchorPoint = CGPointMake(0.5, 0.9);

    self.minuteView = [[UIView alloc]init];
    self.minuteView.backgroundColor = [UIColor lightGrayColor];
    self.minuteView.bounds = CGRectMake(0, 0, 15, 100);
    self.minuteView.center = self.view.center;
    [self.view addSubview:self.minuteView];
    self.minuteView.layer.anchorPoint = CGPointMake(0.5, 0.9);

    self.secondView = [[UIView alloc]init];
    self.secondView.backgroundColor = [UIColor purpleColor];
    self.secondView.bounds = CGRectMake(0, 0, 10, 100);
    self.secondView.center = self.view.center;
    [self.view addSubview:self.secondView];
    self.secondView.layer.anchorPoint = CGPointMake(0.5, 0.9);
    NSTimer *timer = [[NSTimer alloc]initWithFireDate:[NSDate date] interval:1 target:self selector:@selector(tick) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
}

- (void)tick{
    NSCalendar *calendar = [[NSCalendar alloc]initWithCalendarIdentifier:NSCalendarIdentifierChinese];
    NSDateComponents *components = [calendar components:NSCalendarUnitHour|NSCalendarUnitMinute|NSCalendarUnitSecond fromDate:[NSDate date]];
    NSLog(@"Tick...hour %ld minute %ld second %ld",(long)components.hour,(long)components.minute,(long)components.second);
    CGFloat secondAngle = (components.second / 60.0) * M_PI *2.0;
    CGFloat minuteAngle = (components.minute / 60.0) * M_PI * 2.0;
    CGFloat hourAngle = (components.hour / 12.0) * M_PI * 2.0 + (components.minute / 60.0) * M_PI / 6.0;
    
    self.hourView.transform = CGAffineTransformMakeRotation(hourAngle);
    self.minuteView.transform = CGAffineTransformMakeRotation(minuteAngle);
    self.secondView.transform = CGAffineTransformMakeRotation(secondAngle);
}
@end

实现效果如图: 
 

geometryFlipped

一般来说,在iOS上,一个图层的position是相对于父图层的左上角,但在Mac上,是相对于左下角。那么可以通过将属性geometryFlipped设置为YES来改变这种情况。如果将一个视图的geometryFlipped属性设置为YES,那么所有添加到该图层上的图层和视图的frame都将是相对于左下角而言的,

Z坐标轴

UIView是存在于一个二维坐标系中的,而CALayer是存在于一个三维空间中的,描述Z轴上的位置有两个属性zPositionanchorPointZ。zPosition可以用作变换图层,还有改变图层的显示顺序。

conrnerRadius

可以利用该属性控制图层角的曲率,默认值是0(也就是直角),但这个曲率只会影响背景颜色而不会影响背景图片或者子视图,如果需要截取图层里的东西,需要将masksToBounds设置为YES。

redLayer.cornerRadius = 30;
redLayer.masksToBounds = YES;

图层边框

利用属性borderWidthborderColor设置图层边框。borderWidth是以点为单位定义边框粗细的浮点数,默认值为0;borderColor定义边框的颜色,默认是黑色,是CGColorRef类型

redLayer.borderWidth = 3.0;
redLayer.borderColor = [UIColor yellowColor].CGColor;

阴影

设置阴影主要有四个属性来控制,shadowOpacityshadowColorshadowOffsetshadowRadius

  • shadowOpacity

    默认值为0,是一个取值在0.0(不可见)和1.0(完全不透明)之间的浮点数。如果将其设置为1.0,在图层的上方将会显示一个有轻微模糊的黑色阴影。
  • shadowColor

    控制阴影的颜色。默认值是黑色,类型是CGColorRef
  • shadowOffset

    控制阴影的方向和距离,是一个CGSize类型,宽度控制阴影横向的位移,高度控制纵向的位移。默认值是{0,-3},意思是相对Y轴有3个点的向上位移。
  • shadowRadius

    控制阴影的模糊度,值越大,边界线看上去就会越来越模糊和自然(最好设置为非零值)。
  • shadowPath

    我们已经知道图层阴影并不总是方的,而是从图层内容的形状继承而来。这看上去不错,但是实时计算阴影也是一个非常消耗资源的,尤其是图层有多个子图层,每个图层还有一个有透明效果的寄宿图的时候。如果你事先知道你的阴影形状会是什么样子的,你可以通过指定一个shadowPath来提高性能。shadowPath是一个CGPathRef类型(一个指向CGPath的指针)。CGPath是一个Core Graphics对象,用来指定任意的一个矢量图形。我们可以通过这个属性单独于图层形状之外指定阴影的形状。

    下图展示了同一寄宿图的不同阴影设定。如你所见,我们使用的图形很简单,但是它的阴影可以是你想要的任何形状。

    @interface ViewController ()
    @property (nonatomic, weak) IBOutlet UIView *layerView1;
    @property (nonatomic, weak) IBOutlet UIView *layerView2;
    @end
    
    @implementation ViewController
    - (void)viewDidLoad {
      [super viewDidLoad];
      //enable layer shadows
      self.layerView1.layer.shadowOpacity = 0.5f;
      self.layerView2.layer.shadowOpacity = 0.5f;
      //create a square shadow
      CGMutablePathRef squarePath = CGPathCreateMutable();
      CGPathAddRect(squarePath, NULL, self.layerView1.bounds);
      self.layerView1.layer.shadowPath = squarePath; CGPathRelease(squarePath);
      //create a circular shadow
      CGMutablePathRef circlePath = CGPathCreateMutable();
      CGPathAddEllipseInRect(circlePath, NULL, self.layerView2.bounds);
      self.layerView2.layer.shadowPath = circlePath; CGPathRelease(circlePath);
    }
    @end

    如果是一个矩形或者是圆,用CGPath会相当简单明了。但是如果是更加复杂一点的图形,UIBezierPath类会更合适,它是一个由UIKit提供的在CGPath基础上的Objective-C包装类。大一些的阴影位移和角半径会增加图层的深度即视感

mask(遮罩)

使用mask属性,可以利用一个32位有alpha通道的png图片创建无矩形视图。它本身是一个CALayer类型,和其他视图有一样的绘制和布局属性,类似于一个子图层,相对于父图层(即拥有该属性的图层)布局,但不同于那些绘制在父图层中的子图层,mask图层定义了父图层的部分可见区域,mask图层的Color属性是无关紧要的,真正重要的是图层的轮廓。mask属性就像是一个饼干切割机,mask图层实心的部分会被保留下来,其他的则会被抛弃。

如果mask图层比父图层要小,只有在mask图层里面的内容才是它关心的,除此以外的一切都会被隐藏起来。

如下图

代码示例:

///遮罩层
CALayer *mask = [CALayer layer]; mask.frame = redLayer.bounds; mask.contents = (__bridge id)[UIImage imageNamed:@"circle"].CGImage;
///父图层
redLayer.mask
= mask;

CALayer蒙版图层最厉害的地方在于他不限于静态图,任何有图层构成的都可以作为mask属性,意味着蒙版可以通过代码甚至是动画实时生成。

minificationFilter & magnificationFilter(拉伸过滤)

当图片需要显示不同的大小时候,可以使用拉伸过滤算法,他可以作用于原图的像素上并根据需要生成新的像素显示在屏幕上。CALayer提供了三种拉伸过滤方法:kCAFilterLinearkCAFilterNearestkCAFilterTrilinear。minification(缩小图片)和magnification(放大图片)默认的过滤器都是kCAFilterLinear

一些特定的CALayer使用方式

CAShaperLayer

CAShapLayer是一个通过矢量图形而不是bitmap来绘制的图层子类,可以使用CGPaht来定义想要的任何图形,他可以将其自动渲染出来。

举例说明:

UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(175, 100)];
[path addArcWithCenter:CGPointMake(150, 100) radius:25 startAngle:0 endAngle:(M_PI *2) clockwise:YES];
[path moveToPoint:CGPointMake(150, 125)];
[path addLineToPoint:CGPointMake(150, 175)];
[path addLineToPoint:CGPointMake(125, 225)];
[path moveToPoint:CGPointMake(150, 175)];
[path addLineToPoint:CGPointMake(175, 225)];
[path moveToPoint:CGPointMake(100, 150)];
[path addLineToPoint:CGPointMake(200, 150)];
    
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.strokeColor = [UIColor redColor].CGColor;
shapeLayer.fillColor = [UIColor clearColor].CGColor;
shapeLayer.lineWidth = 5;
shapeLayer.lineJoin = kCALineJoinRound;
shapeLayer.lineCap = kCALineCapRound;
shapeLayer.path = path.CGPath;   
shapeLayer.shadowOpacity = 1.0;   
[self.view.layer addSublayer:shapeLayer];

实现效果:

还可以利用CAShapeLayer实现指定方向有圆角

UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(50, 50, 100, 100) byRoundingCorners:UIRectCornerTopLeft|UIRectCornerBottomRight cornerRadii:CGSizeMake(20, 20)];
    
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.strokeColor = [UIColor redColor].CGColor;
shapeLayer.fillColor = [UIColor clearColor].CGColor;
shapeLayer.lineWidth = 5;
shapeLayer.lineJoin = kCALineJoinRound;
shapeLayer.lineCap = kCALineCapRound;
shapeLayer.path = path.CGPath;
shapeLayer.shadowOpacity = 1.0;
[self.view.layer addSublayer:shapeLayer];

效果如下:

 

CATextLayer

CATextLayer使用了CoreText,比使用UILabel渲染要快得多,而且基本包含了UILabel提供的所有绘制特性。 基本使用方式:

CATextLayer *textLayer = [CATextLayer layer];
textLayer.frame = CGRectMake(100, 100, 200, 80);
[self.view.layer addSublayer:textLayer];
    
//设置文本内容
textLayer.string = @"子曰:君子食无求饱,居无求安,敏於事而慎於言,就有道而正焉,可谓好学也已。";
//设置背景颜色
textLayer.backgroundColor = [UIColor redColor].CGColor;
//设置字体颜色 默认是白色
textLayer.foregroundColor = [UIColor blackColor].CGColor;
    
//设置字体 需要将UIFont类型转换成为CGFontRef类型
UIFont *font = [UIFont systemFontOfSize:15];
CFStringRef fontName = (__bridge CFStringRef)font.fontName;
CGFontRef fontRef = CGFontCreateWithFontName(fontName);
textLayer.font = fontRef;
textLayer.fontSize = font.pointSize;//字体大小需要使用fontSize来进行单独设置,因为CGFontRef类型并不像UIFont一样包含大小
CGFontRelease(fontRef);
    
//设置字体对齐方式
textLayer.alignmentMode = kCAAlignmentJustified;
textLayer.wrapped = YES; 
//设置文字分辨率
textLayer.contentsScale = [UIScreen mainScreen].scale;

实现效果:

富文本的使用方式:

CATextLayer *textLayer = [CATextLayer layer];
textLayer.frame = CGRectMake(100, 100, 200, 80);
[self.view.layer addSublayer:textLayer];
    
textLayer.contentsScale = [UIScreen mainScreen].scale;
    
textLayer.alignmentMode = kCAAlignmentJustified;
textLayer.wrapped = YES;
    
NSMutableAttributedString *string = [[NSMutableAttributedString alloc]initWithString:@"子曰:君子食无求饱,居无求安,敏於事而慎於言,就有道而正焉,可谓好学也已。"];
    
UIFont *font = [UIFont systemFontOfSize:15];
CFStringRef fontName = (__bridge CFStringRef)font.fontName;
CGFloat fontSize = font.pointSize;
CTFontRef fontRef = CTFontCreateWithName(fontName, fontSize, NULL);
    
NSDictionary *attribs = @{
                          (__bridge id)kCTForegroundColorAttributeName : (__bridge id)[UIColor blackColor].CGColor,
                          (__bridge id)kCTFontAttributeName : (__bridge id)fontRef,
                         };
       
[string setAttributes:attribs range:NSMakeRange(0, string.length)];
    
NSDictionary *underlineAttri = @{
                                 (__bridge id)kCTForegroundColorAttributeName : (__bridge id)[UIColor redColor].CGColor,
                                 (__bridge id)kCTFontAttributeName : (__bridge id)fontRef,
                                 (__bridge id)kCTUnderlineStyleAttributeName : @(kCTUnderlineStyleDouble),
                                };
CFRelease(fontRef);    
[string setAttributes:underlineAttri range:NSMakeRange(0, 2)];    
textLayer.string = string;

实现效果:

CAGradientLayer

使用CAGradientLayer可以生成两种或多种颜色平滑渐变,它使用硬件加速。

简单的两种颜色的渐变

CAGradientLayer *layer = [CAGradientLayer layer];
layer.frame = CGRectMake(100, 100, 100, 100);
[self.view.layer addSublayer:layer];
    
layer.colors = @[
                 (__bridge id)[UIColor redColor].CGColor,
                 (__bridge id)[UIColor blueColor].CGColor,
                ];
layer.startPoint = CGPointMake(0.5, 0.5);//以单位坐标系进行定义
layer.endPoint = CGPointMake(1.0, 1.0);

实现效果:

多重渐变

colors属性可以包含很多颜色,默认情况下,这些颜色是均匀的被渲染,但也可以使用locations属性来调整空间。locations是一个浮点数值的数组,使用NSNumber进行包装。

注:locations和colors两个数组的大小一定要一致,不然将会得到一个空白的渐变

CAGradientLayer *layer = [CAGradientLayer layer];
layer.frame = CGRectMake(100, 100, 100, 100);
[self.view.layer addSublayer:layer];
    
layer.colors = @[
                 (__bridge id)[UIColor redColor].CGColor,
                 (__bridge id)[UIColor blueColor].CGColor,
                 (__bridge id)[UIColor greenColor].CGColor,
                ];
    
layer.locations = @[
                    @(0.0),
                    @(0.25),
                    @(0.5),
                ];
    
layer.startPoint = CGPointMake(0, 0);//以单位坐标系进行定义
layer.endPoint = CGPointMake(1.0, 1.0);

均匀分布效果图:

 

按照locations分布效果图:

CAReplicatorLayer

利用CAReplicatorLayer可以高效生成许多类似的图层,他可以绘制一个或多个图层的子图层,并且在每个复制体上应用不同的变换。

重复图层

使用方式:

CAReplicatorLayer *replicatorLayer = [CAReplicatorLayer layer];
replicatorLayer.frame = CGRectMake(0, 0, 100, 100);
replicatorLayer.position = self.view.center;
[self.view.layer addSublayer:replicatorLayer];
    
replicatorLayer.backgroundColor = [UIColor redColor].CGColor;
    
replicatorLayer.instanceCount = 2;
replicatorLayer.instanceTransform = CATransform3DMakeTranslation(10, 10, 0);
    
replicatorLayer.instanceRedOffset = -0.1;
replicatorLayer.instanceBlueOffset = -0.1;
    
CALayer *layer = [CALayer layer];
layer.frame = CGRectMake(100, 100, 100, 100);
layer.backgroundColor = [UIColor whiteColor].CGColor;
[replicatorLayer addSublayer:layer];

显示效果:

 

实现反射效果

可以使用CAReplicatorLayer并应用一个负比例变换于一个复制图层,可以创建指定视图内容的镜像图片,就实现了反射效果。

封装一个View来实现:

#import "IVReflectionView.h"

@implementation IVReflectionView

+ (Class)layerClass{
    return [CAReplicatorLayer class];
}

- (instancetype)initWithFrame:(CGRect)frame{
    self = [super initWithFrame:frame];
    if (self) {
        [self setup];
    }
    return self;
}

- (void)awakeFromNib{
    [super awakeFromNib];
    //this is called when view is created from a nib
    [self setup];
}

- (void)setup{
    CAReplicatorLayer *layer = (CAReplicatorLayer *)self.layer;
    layer.instanceCount = 2;
    
    CATransform3D transform = CATransform3DIdentity;
    CGFloat verticalOffset = self.bounds.size.height + 2;
    transform = CATransform3DTranslate(transform, 0, verticalOffset, 0);
    transform = CATransform3DScale(transform, 1, -1, 0);
    layer.instanceTransform = transform;
    
    layer.instanceAlphaOffset = -0.6;
}
@end

使用方法:

IVReflectionView *reflectionView = [[IVReflectionView alloc]initWithFrame:CGRectMake(100, 100, 100, 100)];
reflectionView.backgroundColor = [UIColor redColor];
UIImageView *imageView = [[UIImageView alloc]initWithFrame:reflectionView.bounds];
imageView.image = [UIImage imageNamed:@"media"];
[reflectionView addSubview:imageView];
[self.view addSubview:reflectionView];

显示效果:

CAScrollLayer

CAScrollLayer有一个scrollToPoint:方法,他能自动适应bounds的原点以便图层内容出现在滑动的地方,但并不负责将触摸事件转换为滑动事件,既不渲染滚动条,也不实现iOS指定行为例如滑动反弹。

举例说明:将CAScrollLayer作为视图的宿主图层,创建一个自定义的UIView,然后使用UIPanGestureRecognizer实现触摸事件响应,运行效果:显示一个大于frame的UIImageView

#import "IVScrollView.h"
@implementation IVScrollView
+ (Class)layerClass{
    return [CAScrollLayer class];
}

- (instancetype)initWithFrame:(CGRect)frame{
    self = [super initWithFrame:frame];
    if (self) {
        [self setup];
    }
    return self;
}

- (void)awakeFromNib{
    [super awakeFromNib];
    [self setup];
}

- (void)setup{
    UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(pan:)];
    [self addGestureRecognizer:pan];
}

- (void)pan:(UIPanGestureRecognizer *)recognizer{
    CGPoint moveBy = [recognizer translationInView:self];
    CGPoint origin = self.bounds.origin;
    CGFloat offsetX = origin.x - moveBy.x;
    CGFloat offsetY = origin.y - moveBy.y;
    
    [(CAScrollLayer *)self.layer scrollPoint:CGPointMake(offsetX, offsetY)];
    
    [recognizer setTranslation:CGPointZero inView:self];
}

/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect {
    // Drawing code
}
*/
@end

使用方式:

IVScrollView *scrollView = [[IVScrollView alloc]initWithFrame:CGRectMake(0, 100, 300, 300)];
scrollView.backgroundColor = [UIColor redColor];
[self.view addSubview:scrollView];
    
UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(0, 0, 500, 500)];
imageView.image = [UIImage imageNamed:@"Accounts"];
[scrollView addSubview:imageView];

显示效果:

其他的一些方法和属性

- (void)scrollRectToVisible:(CGRect)r滑动是的指定区域可见

@property(readonly) CGRect visibleRect;获取当前的可视区域

CAEmitterLayer

CAEmitterLayer是一个高性能的粒子引擎,被用来创建实时粒子动画,如:烟雾、火、雨雪等。

使用实例:

CAEmitterLayer *emitter = [CAEmitterLayer layer];
emitter.frame = CGRectMake(100, 100, 200, 200);
[self.view.layer addSublayer:emitter];
    
emitter.renderMode = kCAEmitterLayerAdditive;
emitter.emitterPosition = CGPointMake(emitter.frame.size.width * 0.5f, emitter.frame.size.height * 0.5f);
    
CAEmitterCell *cell = [[CAEmitterCell alloc]init];
cell.contents = (__bridge id)[UIImage imageNamed:@"score"].CGImage;
cell.birthRate = 150;
cell.lifetime = 5.0;
cell.color = [UIColor colorWithRed:1 green:0.5 blue:0.1 alpha:1.0].CGColor;
cell.alphaSpeed = -0.4;
cell.velocity = 50;
cell.velocityRange = 50;
cell.emissionRange = M_PI * 2.0;   
emitter.emitterCells = @[cell];

AVPlayerLayer

需要注意的是,AVPlayerLayer与其上的Layer不同,并不是CoreAnimation框架的一部分,而是属于AVFoundation框架。可以用来播放视频,是MPMoivePlayer的底层实现,提供了显示视频的底层控制。

使用方法:

  • 直接使用playerLayerWithPlayer:方法创建一个已经绑定了视频播放器的图层
  • 可以先创建一个图层,然后用player属性绑定一个AVPlayer实例。

举例说明:

因为AVPlayerLayerCALayer的子类,它继承了父类的所有的特性,可以添加边框、蒙版等特性。

NSURL *url = [[NSBundle mainBundle]URLForResource:@"life" withExtension:@"mp4"];
//NSString *path = [[NSBundle mainBundle]pathForResource:@"life" ofType:@"mp4"];
//NSURL *url = [NSURL URLWithString:path];  //这种方式获取的URL无法播放
AVPlayer *player = [AVPlayer playerWithURL:url];
AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:player];
playerLayer.frame = CGRectMake(100, 100, 200, 200);
[self.view.layer addSublayer:playerLayer];
playerLayer.backgroundColor = [UIColor redColor].CGColor;

CATransform3D transform = CATransform3DIdentity;
transform.m34 = -1.0/500.0;
transform = CATransform3DRotate(transform, M_PI_4, 1, 1, 0);

playerLayer.transform = transform;
playerLayer.masksToBounds = YES;
playerLayer.cornerRadius = 20.0;
playerLayer.borderColor = [UIColor redColor].CGColor;
playerLayer.borderWidth = 5.0f;
    
[player play];

扩展:

导入资源如果无法使用,查看一下设置中的Build Phases > Copy Bundle Resources 是否已经将资源文件添加进去了,如果里面没有,那就需要在这里手动添加了。

CATiledLayer

可以使用CATiledLayer来绘制一个很大的图片,因为由于内存的限制,将整个图片读取到内存中是很不明智的。

- (void)tiledLayer{
    UIScrollView *scrollView = [[UIScrollView alloc]initWithFrame:CGRectMake(100, 100, 200, 200)];
    scrollView.backgroundColor = [UIColor whiteColor];
    [self.view addSubview:scrollView];
    
    CATiledLayer *tileLayer = [CATiledLayer layer];
    tileLayer.frame = CGRectMake(0, 0, 2048, 2048);
    tileLayer.delegate = self;
    tileLayer.contentsScale = [UIScreen mainScreen].scale;//以屏幕的原生分辨率来渲染
    [scrollView.layer addSublayer:tileLayer];
    scrollView.contentSize = tileLayer.frame.size;
    
    [tileLayer setNeedsDisplay];
}

- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx{
    CGRect bounds = CGContextGetClipBoundingBox(ctx);
    
    UIImage *image = [UIImage imageNamed:@"media"];
    UIGraphicsPushContext(ctx);
    [image drawInRect:bounds];
    UIGraphicsPopContext();
} 
效果展示:
 

CATransaction实现动画

先举例说明,改变一个CALayer的backgroundColor,发现并没有立即在屏幕体现出来,而是有一个平滑的过程。

代码:

- (void)changeColor{
    self.redLayer = [CALayer layer];
    self.redLayer.frame = CGRectMake(100, 100, 100, 100);
    [self.view.layer addSublayer:self.redLayer];
    
    self.redLayer.backgroundColor = [UIColor redColor].CGColor;
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.redLayer.backgroundColor = [UIColor greenColor].CGColor;
//    self.view.layer.backgroundColor = [UIColor greenColor].CGColor;
}

效果:(效果展示不太明显,录屏问题)

 

这就是所谓的隐式动画,由CoreAnimation来决定如何并且何时去做动画。实际上动画的类型由图层行为决定,持续时间是由当前事务来设置的。而CATransaction就是用来管理事务的类。

CATransaction没有+allocinit方法,使用+begin+commit进行入栈和出栈管理。

示例,修改动画执行的时间

[CATransaction begin];
[CATransaction setAnimationDuration:2.0];//设置事务的动画时间,默认时间是0.25s
[CATransaction setCompletionBlock:^{
    NSLog(@"The Animation finish");
}];//设置动画结束之后的动作    
self.redLayer.backgroundColor = [UIColor greenColor].CGColor;    
[CATransaction commit];

展示效果:

在UIView中有类似方法,

[UIView beginAnimations:@"" context:nil];    
[UIView commitAnimations];

还有:

[UIView animateWithDuration:1.0 animations:^{
        //do something here
}];

他们本质上都是由CATransaction来实现的。

可会发现一个问题,当我们对UIView关联的图层做同样的动画时,会瞬间切换到新值,而不会出现之前的过渡效果,UIView关联图层的动画被禁用了 (注:completionBlock还是会执行的)

分析原因之前先了解一下隐式动画是如何实现的:

将改变属性时CALayer自动应用的动画成为行为,当CALayer的属性被修改时,他会调用-actionForKey:方法,传递属性的名称,剩下的是下面的几步:

  • 图层首先检测他是否有委托,并且是否是实现CALayerDelegate协议指定的-actionForLayer:forKey方法,如果有,则直接调用并返回结果。
  • 如果没有委托,或没有实现-actionForLayer:forKey方法,图层接着检查包含属性名称对应行为映射的actions字典。
  • 如果actions字典没有对应的属性,那么图层接着会在他的style字典接着搜索属性名
  • 最后,如果在style里面也找不到对应的行为,那么图层将会直接调用定义了每个属性的标准行为的defaultActionForKey:方法。

所以,完成这一轮搜索之后,actionForKey:要么返回空(这种情况下将不会有动画发生),要么是返回CAAction协议定义的对象,最后CALayer那这个结果对先前和当前的值做动画。

这也解释了UIKit是如何禁用隐式动画的:每个UIView对他所关联的图层都扮演一个委托,并且提供了actionForLayer:forKey的实现方法,当不再一个动画块里实现的时候,UIView对所有的图层行为都返回nil,但在动画block范围之内时候,他就返回一个非空值。

测试UIView的actionForLayer:forKey:的实现

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    self.view.backgroundColor = [UIColor cyanColor];
    
    self.redView = [[UIView alloc]initWithFrame:CGRectMake(100, 100, 100, 100)];
    self.redView.backgroundColor = [UIColor redColor];
    [self.view addSubview:self.redView];

    NSLog(@"outside: %@",[self.redView actionForLayer:self.redView.layer forKey:@"backgroundColor"]);
    
    [UIView animateWithDuration:1.0 animations:^{
        NSLog(@"inside: %@",[self.redView actionForLayer:self.redView.layer forKey:@"backgroundColor"]);
    }];
    
    //另一种实现方式
    [UIView beginAnimations:nil context:nil];
    NSLog(@"second inside: %@",[self.redView actionForLayer:self.redView.layer forKey:@"backgroundColor"]);
    [UIView commitAnimations];
}

打印结果为:

iOSFang[921:34878] outside: <null>
iOSFang[921:34878] inside: <CABasicAnimation: 0x600000413d40>
iOSFang[921:34878] second inside: <CABasicAnimation: 0x600000413f80>

当然,返回nil并不是禁用隐式动画的唯一办法,CATransaction有个方法叫+setDisableActions:,可以用来对所有属性打开或者关闭隐式动画。

UIView使用动画的方式

  • 使用UIView的动画函数
  • 继承UIView,覆盖-actionForLayer:forKey:方法,或者直接创建一个显示动画

自定义动画行为

- (void)customLayerAnimation{
    self.redLayer = [CALayer layer];
    self.redLayer.frame = CGRectMake(100, 100, 100, 100);
    self.redLayer.backgroundColor = [UIColor redColor].CGColor;
    [self.view.layer addSublayer:self.redLayer];
    
    CATransition *transition = [CATransition animation];
    transition.type = kCATransitionPush;
    transition.subtype = kCATransitionFromLeft;
    self.redLayer.actions = @{
                              @"backgroundColor":transition,
                              };
}

执行效果为:

  

presentationLayer图层

当改变一个图层的属性时,属性值是立刻更新的,但屏幕上并没有马上发生改变,只是定义了图层动画结束之后的值。

当前显示在屏幕上的属性值记录在一个叫做呈现图层的独立图层中,可以通过presentationLayer来进行访问。这个呈现图层实际上是模型图层的复制,他的属性值代表了任何指定时刻当前外观效果,可以通过它来获取当前屏幕上真正显示出来的值。在呈现图层上调用modelLayer获取它所依赖的CALayer(通常返回的是self)。

 

posted on 2021-11-08 18:51  梁飞宇  阅读(923)  评论(0)    收藏  举报