我们在第三章【图层几何学】中讨论了图层的frame,第二章【寄宿图】我们讨论了图层的寄宿图,但是图层不仅仅可以是图片或是颜色的容器。还有一系列内建的特性使得创建美丽优雅的令人深刻的界面元素成为可能,在这一章,我们将会探索一些能够通过使用CALayer属性实现的视觉效果。
圆角
圆角矩形是ios中的一个标志性审美特性。这在ios中的每一个地方都得到了体现。不论是主屏幕图标,还是警告弹窗,甚至是文本框,按照这个流行程度,你可能会以为一定有不借助photoshop就能轻易创建圆角矩形的方法,恭喜你,答对了。
CALayer有一个叫做cornerRadius的属性控制着图层角的曲率,它是一个浮点数,默认为0(为0的时候就是直角),但是你可以把它设置成任意值。默认情况下,这个曲率值只影响背景颜色而不影响背景图片或者子图层,不过,如果把maskToBounds设置成yes的话,图层里面的所有东西都不会被截取。
我们通过一个简单的例子来模拟下这个效果。我们放置一些视图,他们有一些子视图,而且这些子视图有一些超出了边界,
效果如下:
然后在代码中,我们设置图层角的半径为20个点,并裁减掉第一个视图的超出部分,技术上来说,这些属性都可以通过interface builder在探测版中分别通过“用户定义运行时属性”和勾选“裁剪子视图(clip subviews)”选择框来直接设置属性的值,不过在本实例中我们用代码来表示更清楚。
代码如下:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor grayColor];
UIView *whiteView = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
whiteView.backgroundColor = [UIColor whiteColor];
whiteView.layer.cornerRadius = 20;
whiteView.layer.masksToBounds = YES;
[self.view addSubview:whiteView];
UIView *redView = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
redView.backgroundColor = [UIColor redColor];
[whiteView addSubview:redView];
}
效果如下:
如你所见,下边的子视图沿边界被裁剪了。
单独控制每个层的圆角曲率也不是不可能的,如果想创建有些直角的图层或者视图时,你可能需要一些不同的方法,比如使用一些图层蒙版(我们在本章讲到或者CAShaperLayer专用图层时讲到)。
图层边框
CALayer另外两个非常有用的属性就是borderWidth和borderColor。二者共同定义了图层边框的绘制样式。这条线(也成为stroke)沿着图层的bounds绘制,同时也包含图层的角**。
borderWidth是以点为单位定义边框粗细的浮点数。默认为0。
borderColor定义了边框的颜色,默认为黑色。
borderColor为CGColorRef类型,不是UIColor类型,所以它不是Cocoa的内置对象。不过呢,你肯定也清楚图层引用了borderColor,虽然属性声明并不能证明这一点。CGColorRef在引用/释放时候的行为表现得与NSObject极其相似,但是Objective-c语法并不支持这一做法,所以在CGColorRef属性即便是强应用也只能通过assign关键字来声明。
边框是绘制在图层边界里面的。而且在所有的子内容之前,也在子视图之前,如果我们在之前的例子中加入图层的边框,你就能看到是怎么一回事了
加上边框
代码如下:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor grayColor];
UIView *whiteView = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
whiteView.backgroundColor = [UIColor whiteColor];
whiteView.layer.cornerRadius = 20;
whiteView.layer.masksToBounds = YES;
whiteView.layer.borderColor = [UIColor blackColor].CGColor;
whiteView.layer.borderWidth = 10.0f;
[self.view addSubview:whiteView];
UIView *redView = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
redView.backgroundColor = [UIColor redColor];
[whiteView addSubview:redView];
}
效果如下:
仔细观察会发现边框并不会把寄宿图和子图层的形状计算进来,如果图层的子图层超出了边界,或者是寄宿图的透明域有一个透明蒙版,边框仍然后沿着边界绘制出来;**边框是跟随图层的边界变化的而不是图层里面的内容 **。
阴影
ios另外一个常见特性呢,就是阴影。阴影往往可以达到图层深度暗示的效果。也能够用来强调正在显示的图层和优先级(比如说一个在其他视图之前的弹出框),不过有时候它们只是单纯的装饰目的。
给shadowOpacity属性一个大于默认值(也就是0)的值,阴影就可以显示在任意图层之下。shadowOpacity是一个必须在0(不可见)到1(完全不透明)之间的浮点数。如果设置为1.0,将会显示一个轻微模糊的黑色阴影稍微在图层之上。若要改动阴影的表现,你可以使用CALayer的另外三个属性:shadowColor、shadowOffset、shadowRadius。
显而易见,shadowColor属性控制着阴影的颜色,和borderColor、backgroundColor一样,它的类型也是CGColorRef,默认颜色为黑色,大多数时候你需要的阴影也是黑色。
shadowOffset属性控制着阴影的方向和距离,它是一个CGSize的值,宽度控制这阴影横向的位移,高度控制着纵向的位移,shadowOffset的默认值是{0,-3};意即阴影相对于Y轴有3个点的向上位移。
为什么要默认向上的阴影呢?尽管 Core - Animation 是从图层套装演变而来(可以认为是给IOS创建的私有的动画框架)。但是呢,它却是在MacOS上面世的,前面有提到,二者的Y轴是颠倒的。这就导致了默认的3个点位移的阴影是向上的。在MAC上,shadowOffset的默认值是阴影向下的,这样你就能理解在为什么在IOS系统上阴影是向上的了。
苹果更倾向于用户界面的阴影应该是垂直向下的,所以在ios把阴影宽度设为0,然后高度设为一个正值不失为一个做法。
shadowRadius控制着阴影的模糊度,当它的值是0时,阴影就和视图一样有一个非常确定的的边界线,当值越来越大的时候,边界线看上去就越来越模糊和自然。苹果自家的应用设计更偏向于自然的阴影。所以一个非零值再合适不过了。
通常来讲,如果你想让视图或者控件非常醒目独立于视图之外(比如弹出窗或者遮罩层)。你就应该给shadowRadius一个稍大的值,阴影越模糊,图层的深度看上去就会越明显。
阴影裁剪
和图层边框不同,图层的阴影继承自内容的外形,而不是根据边界和角半径来确定。为了计算出阴影的形状,Core Animation会将寄宿图(包括子视图、如果有的话)考虑在内,然后通过这些来完美搭配图层形状从而创建一个阴影**。
当阴影和裁剪扯上关系的时候就有一个很头疼的限制:阴影通常就是CALayer的边界之外。如果你开启了masksToBounds属性,所有从图层中突出来的内容都会被裁减掉。如果我们在我们之前的边框示例项目中增加图层的阴影属性时,你会发现问题的的所在。
从技术角度看,这个结果是可以理解的,但确实又不是我们想要的效果。如果你想沿着内容裁切,你需要用到两个图层。一个是只画阴影的空的外图层,和一个用masksToBounds裁剪内容的内图层。
如果我们把之前项目中裁剪后的图层用一个独立视图包起来,我们就可以解决这个问题了。
我们只把阴影用在最外层的视图上,内层视图进行裁剪
代码实现:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor whiteColor];
UIView *whiteView = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
whiteView.backgroundColor = [UIColor whiteColor];
whiteView.layer.cornerRadius = 20;
whiteView.layer.masksToBounds = YES;
whiteView.layer.borderColor = [UIColor blackColor].CGColor;
whiteView.layer.borderWidth = 10.0f;
whiteView.layer.shadowRadius = 10;
whiteView.layer.shadowOffset = CGSizeMake(0, -5);
whiteView.layer.shadowOpacity = 0.7;
// [self.view addSubview:whiteView];
UIView *redView = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
redView.backgroundColor = [UIColor redColor];
[whiteView addSubview:redView];
//一个额外的视图,用来包裹裁剪后的视图
UIView *bigView = [[UIView alloc] initWithFrame:whiteView.frame];
[bigView addSubview:whiteView];
bigView.layer.shadowRadius = 10;
bigView.layer.shadowOffset = CGSizeMake(0, -5);
bigView.layer.shadowOpacity = 0.7;
[self.view addSubview:bigView];
}
实现效果:
shadowPath属性
我们已经知道图层阴影并不总是方形的,而是从图层的内容的形状继承而来的。这看上去不错,但是实时计算阴影也是一件非常消耗资源的,尤其是图层有多个子图层,每个图层还有一个有透明效果的寄宿图的时候。
如果你事先知道你的阴影形状会是什么样子的,你可以通过指定一个shadowPath来提高性能。shadowPath是一个CGPathRef类型(一个指向CGPath的指针),CGPath是一个Core Graphics对象,用来指定一个任意的矢量图形。我们可以通过这个属性单独于图层形状之外指定图层阴影的形状。
展示一下同一个寄宿图不同的阴影设定,如你所见,我们使用的图形很简单,但是他的阴影可以是你想要的任何形状。
创建简单的阴影形状
代码如下:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor yellowColor];
//添加一个圆角矩形
CGMutablePathRef pathRef = CGPathCreateMutable();
CGPathAddRoundedRect(pathRef, NULL, CGRectMake(50, 50, 300, 300), 50, 50);
UIView *whiteView = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
whiteView.backgroundColor = [UIColor whiteColor];
whiteView.layer.cornerRadius = 20;
// whiteView.layer.masksToBounds = YES;
whiteView.layer.borderColor = [UIColor blackColor].CGColor;
whiteView.layer.borderWidth = 10.0f;
whiteView.layer.shadowRadius = 10;
whiteView.layer.shadowOffset = CGSizeMake(0, -5);
whiteView.layer.shadowOpacity = 0.7;
whiteView.layer.shadowPath = pathRef;
[self.view addSubview:whiteView];
UIView *redView = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
redView.backgroundColor = [UIColor redColor];
[whiteView addSubview:redView];
/*
//一个额外的视图,用来包裹裁剪后的视图
UIView *bigView = [[UIView alloc] initWithFrame:whiteView.frame];
[bigView addSubview:whiteView];
bigView.layer.shadowRadius = 10;
bigView.layer.shadowOffset = CGSizeMake(0, -5);
bigView.layer.shadowOpacity = 0.7;
[self.view addSubview:bigView];
*/
}
效果如下:
如果是一个标准的矩形或者圆,用CGPath就相当明了了,但是如果是更加复杂一点的图形,UIBezierPath类会更合适。它是一个由UIKit提供的在CGPath基础上objective-c包装类。
图层蒙版
通过masksToBounds属性,我们可以沿边界裁剪图形。通过cornerRadius属性,我们可以设定一个圆角,但是有时候你希望展现的内容不是在一个矩形或者圆角矩形,比如,你想展示一个有星形框架的图片,又或者想让一些古卷文字慢慢渐变成背景色,而不是一个突兀的边界。
使用一个32位有alpha通道的png图片通常是创建一个无矩形视图最方便的方法。你可以给它指定一个透明蒙版来实现,但是这个方法不能让你以编码的方式动态的生成蒙版,也不能让子图层或者子视图裁剪成同样的形状。
CALayer有一个属性叫做mask可以解决这个问题,这个属性本身就是一个CALayer类型,有和其他图层一样的绘制和布局属性,它类似于一个子图层,相对于父图层(即拥有该属性的图层)布局,但是它却不是一个普通的的子视图。不同于那些绘制在父图层中的子图层。mask图层定义了父图层的部分可见区域。**
如果mask图层比父图层要小,只有在mask图层里面的内容才是它关心的,除此之外的一切都会被隐藏起来。
我们用代码来演示一下这个过程,创建一个简单的项目,通过图层的mask属性来作用于图片之上,
代码如下:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor whiteColor];
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
imageView.image = [UIImage imageNamed:@"tesla.jpg"];
CALayer *maskLayer = [CALayer layer];
maskLayer.frame = CGRectMake(50, 50, 100, 100);
maskLayer.contents = (__bridge id)[UIImage imageNamed:@"badgMessage.png"].CGImage;
imageView.layer.mask = maskLayer;
[self.view addSubview:imageView];
}
效果如下:
CALayer蒙版图层真正厉害的地方在于蒙版图层不局限于静态图,任何有图层构成的都可以作为mask属性,这意味着你的蒙版可以通过代码甚至是动画实时生成。
拉伸过滤
我们最后再来谈谈minificationFilter和magnificationFilter属性,总得来讲,当我们视图显示一张图片的时候,都应该正确的显示这张图片(意即:以正确的比例和正确 的1:1像素显示在屏幕上)。原因如下:
-** 能够显示最好的画质,像素即没有被压缩也没有被拉伸。**
- 能更好的使用内存,因为这这就所有你要存储的东西。
- 最好的性能表现,CPU不需要为此额外计算。
不过有时候,显示一个非真实大小的图片确实是我们需要的效果。比如说一个头像或是图片的缩略图,再比如说一个可以被拖拽或是伸缩的大图。这种情况下,为同一图片的不同大小存储不同的图片就显的不切实际。
当图片需要显示不同大小的时候,有一种叫做拉伸过滤的算法就起到作用了,它作用于原图的像素上并根据需要生成新的像素显示到屏幕上。
事实上,重绘图片大小也没有一个通用的算法,这取决于需要拉伸的内容,放大或是缩小的需求等这些因素。CALayer为此提供了三种拉伸过滤的方法,他们是:
- kCAFilterLinear
- kCAFilterNearest
- kCAFilterTrilinear
minification(缩小图片)和magnification(放大图片)默认的过滤器都是kCAFilterLinear,这个过滤器采用双线性滤波算法,它在大多数情况下都表现良好。双线性滤波算法通过对多个像素取样最终生成新的值,得到一个平滑的表现不错 的拉伸,但是当放大倍数比较大的时候图片就模糊不清了**。
kCAFilterTrilinear和kCAFilterLinear两者非常相似,大部分情况下两者都看不出有什么区别。但是,较双线性滤波算法而言,三线性滤波算法存储了多个大小情况下的图片(也叫多重贴图),并三维取样,同时结合大图和小图的存储进而得到最后的结果。
这个方法的好处在于算法能够从一系列已经接近于最终大小的图片中得到想要的结果,也就是说不要对很多像素同步取样。这不仅提高了性能,也避免了小概率因舍入错误引起的 取样失灵的问题。
对于大图来说,双线性滤波算法和三线性滤波算法表现的更出色
kCAFilterNearest是一种比较武断的方法。从名字不难看出,这个算法就是取样最近的单像素点而不管其他的颜色。这样做非常快,而且不会是图片模糊。但是,最明显的效果就是:会使得压缩图片更糟,图片放大之后也显得块状或是马赛克严重。
对于没有倾斜的小图来说,最近过滤算法要好很多
总的来说,对于比较小的图或者是差异特别明显,极小斜线的大图,最近过滤算法(kCAFilterNearest)会保留这种差异明显的特质以呈现更好的结果。但对于大多数图尤其是有很多斜线或是曲线轮廊的图片来说,最近过滤算法会导致更差的结果。换句话说,线性过滤保留了形状,最近过滤则是保留了像素的差异。
让我们来试验一下,我们对第三章的时钟项目做下调整,用LCD风格的数字方式进行显示,我们用简单的像素字体(一种用像素构成字符的字体,而非矢量图形)创造数字显示方式,用图片存储起来,而且用第二章中介绍的拼合技术来展示。
一个简单的使用拼合技术显示的LCD数字风格的像素字体
我们创建六个视图,小时,分钟,秒各两个,
@interface ViewController ()
@property (nonatomic, strong) IBOutletCollection(UIView) NSArray *digitViews;
@property (nonatomic, weak) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad]; //get spritesheet image
UIImage *digits = [UIImage imageNamed:@"Digits.png"];
//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: NSGregorianCalendar];
NSUInteger units = NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit;
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.magnification = kCAFilterNearest;
组透明
UIView有一个alpha的属性来确定视图的透明度,CALayer有一个等同的属性叫做opacity,这两个属性都是影响子层级的,也就是说,如果你给一个图层设置了opactiy属性,那它的子图层都会受此影响**。
ios通常的做法是把一个控件的alpha值设置成0.5(50%)以使其看上去呈现不可用状态,对于独立的视图来说还不错,但是当一个控件有子视图的时候就很奇怪了,如下展示:一个内嵌了UILabel的自定义UIButton,左边是一个不透明的按钮,右边是50%透明度的相同的按钮。我们可以注意到,里面的标签轮廊跟按钮的背景很不搭调。
右边的渐隐按钮中,里面的标签清晰可见
这是由于透明度的混合叠加造成的, 当你显示一个50%透明度的图层时,图层的每个像素都会一半显示自己的颜色,另一半显示图层下面的颜色。这是正常的透明度表现。但是如果图层包含了一个同样显示50%透明的子图层时,你所看到的视图,50%来自子图层,25%来自图层本身的颜色,25%来自图层的背景色。**
理想情况下,当你设置了一个图层的透明度,你希望它包含的整个图层树像一个整体一样的透明效果。你可以通过设置info.plist文件中UIViewGroupOpacity为yes来达到这一效果,但是这个设置会影响到这个应用,整个app可能会受到不良影响。如果UIViewGroupOpacity未设置,ios6和以前的版本会默认设置为NO**。
另一个方法是,你可以设置CALayer中的shouldRasterize属性来实现组透明的效果。如果它被设置为YES,在应用透明度之前,图层及其子图层都会被整合成一个整体的图片,这样就没有透明度混合的问题了**。
为了层用shouldRasterize属性,我们设置了图层的rasterizationScale属性,默认情况下,所有图层拉伸都是1.0,所以如果你使用了shouldRasterize属性,你就要确保rasterizationScale属性去匹配屏幕,以防止出现Retina屏幕像素化的问题**。
当shouldRasterize和UIViewGroupOpacity在一起使用的时候,性能问题就出现了(我们在后面的性能方面做出解释)
使用shouldRasterize属性解决组透明问题
代码如下
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@end
@implementation ViewController
- (UIButton *)customButton
{
//create button
CGRect frame = CGRectMake(0, 0, 150, 50);
UIButton *button = [[UIButton alloc] initWithFrame:frame];
button.backgroundColor = [UIColor whiteColor];
button.layer.cornerRadius = 10;
//add label
frame = CGRectMake(20, 10, 110, 30);
UILabel *label = [[UILabel alloc] initWithFrame:frame];
label.text = @"Hello World";
label.textAlignment = NSTextAlignmentCenter;
[button addSubview:label];
return button;
}
- (void)viewDidLoad
{
[super viewDidLoad];
//create opaque button
UIButton *button1 = [self customButton];
button1.center = CGPointMake(50, 150);
[self.containerView addSubview:button1];
//create translucent button
UIButton *button2 = [self customButton];
button2.center = CGPointMake(250, 150);
button2.alpha = 0.5;
[self.containerView addSubview:button2];
//enable rasterization for the translucent button
button2.layer.shouldRasterize = YES;
button2.layer.rasterizationScale = [UIScreen mainScreen].scale;
}
@end
效果如下:
注意在ios7之后已经推出了新的 组透明控制参数:allowsGroupOpacity,默认值为yes;**
总结
这一章介绍了一些可以通过代码应用到图层上的视觉效果,比如圆角,阴影和蒙版,我们也了解了拉伸过滤器和组透明。