Core Animation 第四章 视觉效果

往期回顾:
序章
第一章 - 图层树
第二章 - 寄宿图
第三章 - 图层几何
项目中使用的代码

在这一章我们主要会讨论一些通过CALayer属性实现的视觉效果。

圆角


CALayer有一个叫做cornerRadius的属性,他可以帮助我们不借助PS等工具轻松的搞定圆角矩形,这个属性调整的是图层角的曲率或者说是圆角半径,默认为0,也就是直角,而且曲率值只会影响背景颜色,而不会影响背景图片或者子图层。不过将masksToBounds为YES的话,图层里面所有的东西都会被截取。下面来做一个简单的例子。

两个白色的大视图,里面都包含了一个红色的小视图.png
  • 在书中这里提到使用IB(Interface Builder)构建视图的时候IB编辑界面会自动剪裁掉超出子视图的部分,不过这个现象在新的XCode中已经不会出现了。

然后我们设置两个白色视图的圆角半径为 20,并且对第二个白色视图设置masksToBounds为YES,来看一下效果。

@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIView *layerView1;
@property (weak, nonatomic) IBOutlet UIView *layerView2;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //设置圆角半径
    self.layerView1.layer.cornerRadius = 20.0f;
    self.layerView2.layer.cornerRadius = 20.0f;
    
    //设置自动剪裁
    self.layerView2.layer.masksToBounds = YES;
}

@end
设置过圆角后的两个白色视图.png

可以看到两个白色视图都表现出了圆角,但是只有第二个设置了masksToBounds的白色视图剪裁掉了子视图。

图层边框


CALayer另外连个常用的属性就是borderWidthborderColor,两个共同定义了图层边缘的绘制样式,这条线(stroke)沿着图层的bounds绘制,同时也包含图层的角。其中 borderWidth 默认为0, borderColor 默认为黑色。

  • borderColorCGColorRef类型,由于他不是Cocoa的内置对象,所以即便CGColorRef的属性是强引用也只能通过assign关键字来声明
  • 边框是绘制在图层内部的,而且在所有的子图层之前。

下面我们来为白色视图添加边框

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //设置圆角半径
    self.layerView1.layer.cornerRadius = 20.0f;
    self.layerView2.layer.cornerRadius = 20.0f;
    
    //设置边框
    self.layerView1.layer.borderWidth = 5.0f;
    self.layerView2.layer.borderWidth = 5.0f;
    
    //设置自动剪裁
    self.layerView2.layer.masksToBounds = YES;
    
}
为图层添加边框.png
  • 可以看到边框并不会计算自图层的位置和形状而只是沿着图层的边界来绘制。
边框并不会根据图层里面的内容变化.png

阴影


阴影是以前iOS中十分常用的一种设计,用来突出图层的优先级,或者装饰图层,不过随着扁平化的盛行,阴影已经慢慢的被人们所抛弃了。
shadowOpacity属性负责控制阴影的显示,值在0.0 ~ 1.0之间,0.0为阴影不可见,1.0为完全不透明,此外CALayer还有三个属性来协助表现阴影的样子:shadowColor, shadowOffset, shadowRadius
shadowColor 控制阴影的颜色,也是一个CGColorRef类型的属性,shadowOffset控制阴影的位置,是一个CGSize类型的值,默认为{0, -3}即阴影默认Y轴向上偏移三个单位。关于这个默认值书中的解释为:

尽管Core Animation是从图层套装演变而来(可以 认为是为iOS创建的私有动画框架),但是呢,它却是在Mac OS上面世的,前面有 提到,二者的Y轴是颠倒的。这就导致了默认的3个点位移的阴影是向上的。在Mac 上, shadowOffset 的默认阴影向下的,这样你就能理解为什么iOS上的阴影方向是向上的了。

shadowRadius用来控制阴影的模糊程度,值越大,阴影越模糊。

低shadowRadius 与 高shadowRadius

阴影剪裁

与边框不容,阴影是可以继承图层内容的形状的,包括子视图和寄宿图的形状。

阴影会自动计算寄宿图的形状

结合上面说的内容会出现一个问题,那就是我们设置了阴影的同时打开了masksToBounds

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //设置圆角半径
    self.layerView1.layer.cornerRadius = 20.0f;
    self.layerView2.layer.cornerRadius = 20.0f;
    
    //设置边框
    self.layerView1.layer.borderWidth = 5.0f;
    self.layerView2.layer.borderWidth = 5.0f;
    
    //设置剪裁
    self.layerView2.layer.masksToBounds = YES;
    
    //设置阴影
    self.layerView1.layer.shadowOpacity = 0.8f;
    self.layerView2.layer.shadowOpacity = 0.8f;
    
    self.layerView1.layer.shadowOffset = CGSizeMake(0, 3);
    self.layerView2.layer.shadowOffset = CGSizeMake(0, 3);
    
    self.layerView1.layer.shadowRadius = 5.0f;
    self.layerView2.layer.shadowRadius = 5.0f;
}
阴影被masksToBounds剪裁掉了

如果我们既需要masksToBounds同时也想要一个印象效果的话,可以通过一个比较tricky的方法来实现,也就是在当前视图的下面添加一个同样大小的透明视图,使用它来显示阴影。

当前图层树的结构

@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIView *layerView1;
@property (weak, nonatomic) IBOutlet UIView *layerView2;
@property (weak, nonatomic) IBOutlet UIView *shadowView;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //设置圆角半径
    self.layerView1.layer.cornerRadius = 20.0f;
    self.layerView2.layer.cornerRadius = 20.0f;
    
    //设置边框
    self.layerView1.layer.borderWidth = 5.0f;
    self.layerView2.layer.borderWidth = 5.0f;
    
    //设置剪裁
    self.layerView2.layer.masksToBounds = YES;
    
    //设置阴影
    self.shadowView.layer.shadowOpacity = 0.8f;
    self.layerView2.layer.shadowOpacity = 0.8f;
    
    self.shadowView.layer.shadowOffset = CGSizeMake(0, 3);
    self.layerView2.layer.shadowOffset = CGSizeMake(0, 3);
    
    self.shadowView.layer.shadowRadius = 5.0f;
    self.layerView2.layer.shadowRadius = 5.0f;
}

@end
同时拥有masksToBounds和阴影

shadowPath

上面已经提到过阴影会自动计算自图层和寄宿图的形状,但是当自图层很多的时候,这种计算必然会十分消耗性能,所以如果你知道当前图层需要的阴影的形状,可以使用shadowPath传入一个CGPathRef类型的值来进行优化。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.layerView1.layer.contents = (__bridge id _Nullable)([UIImage imageNamed:@"pica"].CGImage);
    self.layerView2.layer.contents = (__bridge id _Nullable)([UIImage imageNamed:@"pica"].CGImage);
    
    //显示阴影
    self.layerView1.layer.shadowOpacity = 0.8f;
    self.layerView2.layer.shadowOpacity = 0.8f;
    
    //绘制一个矩形阴影
    CGMutablePathRef squarePath = CGPathCreateMutable();
    CGPathAddRect(squarePath, NULL, self.layerView1.bounds);
    self.layerView1.layer.shadowPath = squarePath;
    CGPathRelease(squarePath);
    //绘制一个圆形阴影
    CGMutablePathRef circlePath = CGPathCreateMutable();
    CGPathAddEllipseInRect(circlePath, NULL, self.layerView2.bounds);
    self.layerView2.layer.shadowPath = circlePath;
    CGPathRelease(circlePath);
}
使用shadowPath自己绘制阴影形状
  • 这里使用了CGMutablePathRef来进行绘制,由于CGMutablePathRef并不是一个Cocoa对象,所以需要使用CGPathRelease进行手动释放,如果使用UIKit中提供的UIBezierPath进行操作则不需要手动释放。

图层蒙版


有些时候我们需要一个不规则的容器来展现我们需要的内容,比如,你想展示一个有星形框架的图片,又或者想让一些古卷文字慢慢渐变成背景色,而不是一个突兀的边界。
CALayer中有一个属性叫做mask。这个属性本身也是CALayer类型,类似于子图层,与子图层不同的是mask定义了父图层可以显示的区域,可以脑补一下 神奇宝贝 里面 我是谁 的那个过场。效果如下:

使用mask制作一个红色的皮卡丘.png
@interface MaskViewController ()
@property (weak, nonatomic) IBOutlet UIView *layerView1;
@property (weak, nonatomic) IBOutlet UIView *layerView2;
@property (weak, nonatomic) IBOutlet UIView *layerView3;

@end

@implementation MaskViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    UIImage *redImage = [UIImage imageNamed:@"redImage"];
    UIImage *pica = [UIImage imageNamed:@"pica"];
    self.layerView1.layer.contents = (__bridge id)redImage.CGImage;
    self.layerView2.layer.contents = (__bridge id)pica.CGImage;
    
    //设置蒙版
    self.layerView3.layer.contents = self.layerView1.layer.contents;
    CALayer *maskLayer = [CALayer layer];
    maskLayer.frame = self.layerView3.bounds;
    maskLayer.contents = self.layerView2.layer.contents;
    self.layerView3.layer.mask = maskLayer;
}

@end

而且蒙版真正强大的地方在于蒙版是可以通过代码或者动画来生成的,而不是局限于静态的图片。

拉伸过滤


这块我之前没有什么积累,就都直接引用作者的原话好了,不过代码示例我会为大家准备好的。
接下来要说的两个属性分别是minificationFiltermagnificationFilter。总得来讲,当我们视图显示一个图片的时候,都应该正确地显示这个图片(意即:以 正确的比例和正确的1:1像素显示在屏幕上)。原因如下:

  • 能够显示最好的画质,像素既没有被压缩也没有被拉伸。
  • 能更好的使用内存,因为这就是所有你要存储的东西。
  • 最好的性能表现,CPU不需要为此额外的计算。

但很多时候图片的大小并不能很好的和视图的大小保持一致,这个时候有一种叫做拉伸过滤的算法就起到作用了。它作用于原图的像素上并根据需要生成新的像素显示在屏幕上。CALayer 为此提供了三种拉伸过滤方法,他们是:

  • kCAFilterLinear
  • kCAFilterNearest
  • kCAFilterTrilinear
    minification(缩小图片)和magnification(放大图片)默认的过滤器都是 kCAFilterLinear ,这个过滤器采用双线性滤波算法,它在大多数情况下都表现良好。双线性滤波算法通过对多个像素取样最终生成新的值,得到一个平滑的表现不错的拉伸。但是当放大倍数比较大的时候图片就模糊不清了。
    kCAFilterTrilinearkCAFilterLinear 非常相似,大部分情况下二者都看不出来有什么差别。但是,较双线性滤波算法而言,三线性滤波算法存储了多个大小情况下的图片(也叫多重贴图),并三维取样,同时结合大图和小图的存储进而得到最后的结果。
    这个方法的好处在于算法能够从一系列已经接近于最终大小的图片中得到想要的结果,也就是说不要对很多像素同步取样。这不仅提高了性能,也避免了小概率因舍入错误引起的取样失灵的问题。
对于大图来说,双线性滤波和三线性滤波表现得更出色

kCAFilterNearest 是一种比较武断的方法。从名字不难看出,这个算法(也叫最近过滤)就是取样最近的单像素点而不管其他的颜色。这样做非常快,也不会使图片模糊。但是,最明显的效果就是,会使得压缩图片更糟,图片放大之后也显得块状或是马赛克严重。

对于没有斜线的小图来说,最近过滤算法要好很多

总的来说,对于比较小的图或者是差异特别明显,极少斜线的大图,最近过滤算法会保留这种差异明显的特质以呈现更好的结果。但是对于大多数的图尤其是有很多斜线或是曲线轮廓的图片来说,最近过滤算法会导致更差的结果。换句话说,线性过滤保留了形状,最近过滤则保留了像素的差异。

下面来做一个简单的小时钟,

@interface FilterViewController ()
@property (strong, nonatomic) IBOutletCollection(UIView) NSArray *digitViews;
@property (weak, nonatomic) NSTimer *timer;
@end

@implementation FilterViewController

- (void)viewDidLoad {
    [super viewDidLoad]; //get spritesheet image
    UIImage *digits = [UIImage imageNamed:@"numbers"];
    
    //set up digit views
    for (UIView *view in self.digitViews) {
        //set contents
        view.layer.contents = (__bridge id)digits.CGImage;
        view.layer.contentsRect = CGRectMake(0, 0, 0.1, 1.0);
        view.layer.contentsGravity = kCAGravityResizeAspect;
    }
    
    //start timer
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(tick) userInfo:nil repeats:YES];
    
    //set initial clock time
    [self tick];
}

- (void)setDigit:(NSInteger)digit forView:(UIView *)view
{
    //adjust contentsRect to select correct digit
    view.layer.contentsRect = CGRectMake(digit * 0.1, 0, 0.1, 1.0);
}

- (void)tick
{
    //convert time to hours, minutes and seconds
    NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier: NSCalendarIdentifierGregorian];
    NSUInteger units = NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond;

    NSDateComponents *components = [calendar components:units fromDate:[NSDate date]];
    
    //set hours
    [self setDigit:components.hour / 10 forView:self.digitViews[0]];
    [self setDigit:components.hour % 10 forView:self.digitViews[1]];
    
    //set minutes
    [self setDigit:components.minute / 10 forView:self.digitViews[2]];
    [self setDigit:components.minute % 10 forView:self.digitViews[3]];
    
    //set seconds
    [self setDigit:components.second / 10 forView:self.digitViews[4]];
    [self setDigit:components.second % 10 forView:self.digitViews[5]];
}

@end
一个模糊的时钟,由默认的kCAFilterLinear引起

作为修改,我们在for循环中加入以下代码

view.layer.magnificationFilter = kCAFilterNearest;
修改了magnificationFilter后的时钟

组透明


UIView中用来处理透明度的属性叫alpha,CALayer中对应的属性为opacity,这两个属性都会对子层级产生影响,也就是说你给一个图层设置了opacity,他说有的子图层的opacity都会受到影响。下面的图片中是一个白色的视图内部有一个白色的Label,右边的视图被设置了50%的透明度。

右边子视图的label被显示了出来

这是由透明度的混合叠加造成的,当你显示一个50%透明度的图层时,图层的每个像素都会一半显示自己的颜色,另一半显示图层下面的颜色。这是正常的透明度的表现。但是如果图层包含一个同样显示50%透明的子图层时,你所看到的视图,50%来自子视图,25%来了图层本身的颜色,另外的25%则来自背景色。

  • 书中提到了可以将info.plist中的UIViewGroupOpacity设置为YES来达到整个图层树保持相同透明度的效果,但是当前版本的XcodeUIViewGroupOpacity已经默认设置为了YES,所以想出现上图中的效果需要先将UIViewGroupOpacity设置为NOUIViewGroupOpacity的缺点在于他是一个整体配置,整个应用可能会受到不良影响。

除了UIViewGroupOpacity,另一个方法就是启用CALayershouldRasterize属性来组透明效果。为了启用shouldRasterize属性,我们设置了图层的rasterizationScale属性。默认情况下,所有图层拉伸都是1.0, 所以如果你使用了shouldRasterize属性,你就要确保你设置了rasterizationScale属性去匹配屏幕,以防止出现Retina屏幕像素化的问题。

修复后的Label
- (void)viewDidLoad {
    [super viewDidLoad];
    self.layerView.alpha = 0.5f;
    self.layerView.layer.shouldRasterize = YES;
    self.layerView.layer.rasterizationScale = [UIScreen mainScreen].scale;
}

总结


这一章介绍了一些可以通过代码应用到图层上的视觉效果,比如圆角,阴影和蒙板。我们也了解了拉伸过滤器和组透明。
在第五章,『变换』中,我们将会研究图层变化和3D转换。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,009评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,808评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,891评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,283评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,285评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,409评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,809评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,487评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,680评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,499评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,548评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,268评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,815评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,872评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,102评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,683评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,253评论 2 341

推荐阅读更多精彩内容