往期回顧:
序章
第一章 - 圖層樹
第二章 - 寄宿圖
第三章 - 圖層幾何
項(xiàng)目中使用的代碼
在這一章我們主要會(huì)討論一些通過CALayer
屬性實(shí)現(xiàn)的視覺效果。
圓角
CALayer
有一個(gè)叫做cornerRadius
的屬性,他可以幫助我們不借助PS等工具輕松的搞定圓角矩形,這個(gè)屬性調(diào)整的是圖層角的曲率或者說是圓角半徑,默認(rèn)為0,也就是直角,而且曲率值只會(huì)影響背景顏色,而不會(huì)影響背景圖片或者子圖層。不過將masksToBounds
為YES的話,圖層里面所有的東西都會(huì)被截取。下面來做一個(gè)簡單的例子。
- 在書中這里提到使用IB(Interface Builder)構(gòu)建視圖的時(shí)候IB編輯界面會(huì)自動(dòng)剪裁掉超出子視圖的部分,不過這個(gè)現(xiàn)象在新的XCode中已經(jīng)不會(huì)出現(xiàn)了。
然后我們設(shè)置兩個(gè)白色視圖的圓角半徑為 20,并且對(duì)第二個(gè)白色視圖設(shè)置masksToBounds
為YES,來看一下效果。
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIView *layerView1;
@property (weak, nonatomic) IBOutlet UIView *layerView2;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//設(shè)置圓角半徑
self.layerView1.layer.cornerRadius = 20.0f;
self.layerView2.layer.cornerRadius = 20.0f;
//設(shè)置自動(dòng)剪裁
self.layerView2.layer.masksToBounds = YES;
}
@end
可以看到兩個(gè)白色視圖都表現(xiàn)出了圓角,但是只有第二個(gè)設(shè)置了masksToBounds的白色視圖剪裁掉了子視圖。
圖層邊框
CALayer
另外連個(gè)常用的屬性就是borderWidth
和 borderColor
,兩個(gè)共同定義了圖層邊緣的繪制樣式,這條線(stroke
)沿著圖層的bounds
繪制,同時(shí)也包含圖層的角。其中 borderWidth
默認(rèn)為0, borderColor
默認(rèn)為黑色。
-
borderColor
為CGColorRef
類型,由于他不是Cocoa
的內(nèi)置對(duì)象,所以即便CGColorRef
的屬性是強(qiáng)引用也只能通過assign
關(guān)鍵字來聲明 - 邊框是繪制在圖層內(nèi)部的,而且在所有的子圖層之前。
下面我們來為白色視圖添加邊框
- (void)viewDidLoad {
[super viewDidLoad];
//設(shè)置圓角半徑
self.layerView1.layer.cornerRadius = 20.0f;
self.layerView2.layer.cornerRadius = 20.0f;
//設(shè)置邊框
self.layerView1.layer.borderWidth = 5.0f;
self.layerView2.layer.borderWidth = 5.0f;
//設(shè)置自動(dòng)剪裁
self.layerView2.layer.masksToBounds = YES;
}
- 可以看到邊框并不會(huì)計(jì)算自圖層的位置和形狀而只是沿著圖層的邊界來繪制。
陰影
陰影是以前iOS中十分常用的一種設(shè)計(jì),用來突出圖層的優(yōu)先級(jí),或者裝飾圖層,不過隨著扁平化的盛行,陰影已經(jīng)慢慢的被人們所拋棄了。
shadowOpacity
屬性負(fù)責(zé)控制陰影的顯示,值在0.0 ~ 1.0之間,0.0為陰影不可見,1.0為完全不透明,此外CALayer還有三個(gè)屬性來協(xié)助表現(xiàn)陰影的樣子:shadowColor
, shadowOffset
, shadowRadius
。
shadowColor
控制陰影的顏色,也是一個(gè)CGColorRef類型的屬性,shadowOffset
控制陰影的位置,是一個(gè)CGSize類型的值,默認(rèn)為{0, -3}即陰影默認(rèn)Y軸向上偏移三個(gè)單位。關(guān)于這個(gè)默認(rèn)值書中的解釋為:
盡管Core Animation是從圖層套裝演變而來(可以 認(rèn)為是為iOS創(chuàng)建的私有動(dòng)畫框架),但是呢,它卻是在Mac OS上面世的,前面有 提到,二者的Y軸是顛倒的。這就導(dǎo)致了默認(rèn)的3個(gè)點(diǎn)位移的陰影是向上的。在Mac 上, shadowOffset 的默認(rèn)陰影向下的,這樣你就能理解為什么iOS上的陰影方向是向上的了。
shadowRadius
用來控制陰影的模糊程度,值越大,陰影越模糊。
陰影剪裁
與邊框不容,陰影是可以繼承圖層內(nèi)容的形狀的,包括子視圖和寄宿圖的形狀。
結(jié)合上面說的內(nèi)容會(huì)出現(xiàn)一個(gè)問題,那就是我們設(shè)置了陰影的同時(shí)打開了masksToBounds
。
- (void)viewDidLoad {
[super viewDidLoad];
//設(shè)置圓角半徑
self.layerView1.layer.cornerRadius = 20.0f;
self.layerView2.layer.cornerRadius = 20.0f;
//設(shè)置邊框
self.layerView1.layer.borderWidth = 5.0f;
self.layerView2.layer.borderWidth = 5.0f;
//設(shè)置剪裁
self.layerView2.layer.masksToBounds = YES;
//設(shè)置陰影
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
同時(shí)也想要一個(gè)印象效果的話,可以通過一個(gè)比較tricky的方法來實(shí)現(xiàn),也就是在當(dāng)前視圖的下面添加一個(gè)同樣大小的透明視圖,使用它來顯示陰影。
@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];
//設(shè)置圓角半徑
self.layerView1.layer.cornerRadius = 20.0f;
self.layerView2.layer.cornerRadius = 20.0f;
//設(shè)置邊框
self.layerView1.layer.borderWidth = 5.0f;
self.layerView2.layer.borderWidth = 5.0f;
//設(shè)置剪裁
self.layerView2.layer.masksToBounds = YES;
//設(shè)置陰影
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
shadowPath
上面已經(jīng)提到過陰影會(huì)自動(dòng)計(jì)算自圖層和寄宿圖的形狀,但是當(dāng)自圖層很多的時(shí)候,這種計(jì)算必然會(huì)十分消耗性能,所以如果你知道當(dāng)前圖層需要的陰影的形狀,可以使用shadowPath
傳入一個(gè)CGPathRef
類型的值來進(jìn)行優(yōu)化。
- (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;
//繪制一個(gè)矩形陰影
CGMutablePathRef squarePath = CGPathCreateMutable();
CGPathAddRect(squarePath, NULL, self.layerView1.bounds);
self.layerView1.layer.shadowPath = squarePath;
CGPathRelease(squarePath);
//繪制一個(gè)圓形陰影
CGMutablePathRef circlePath = CGPathCreateMutable();
CGPathAddEllipseInRect(circlePath, NULL, self.layerView2.bounds);
self.layerView2.layer.shadowPath = circlePath;
CGPathRelease(circlePath);
}
- 這里使用了
CGMutablePathRef
來進(jìn)行繪制,由于CGMutablePathRef
并不是一個(gè)Cocoa對(duì)象,所以需要使用CGPathRelease
進(jìn)行手動(dòng)釋放,如果使用UIKit
中提供的UIBezierPath
進(jìn)行操作則不需要手動(dòng)釋放。
圖層蒙版
有些時(shí)候我們需要一個(gè)不規(guī)則的容器來展現(xiàn)我們需要的內(nèi)容,比如,你想展示一個(gè)有星形框架的圖片,又或者想讓一些古卷文字慢慢漸變成背景色,而不是一個(gè)突兀的邊界。
CALayer中有一個(gè)屬性叫做mask。這個(gè)屬性本身也是CALayer類型,類似于子圖層,與子圖層不同的是mask定義了父圖層可以顯示的區(qū)域,可以腦補(bǔ)一下 神奇寶貝 里面 我是誰 的那個(gè)過場。效果如下:
@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;
//設(shè)置蒙版
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
而且蒙版真正強(qiáng)大的地方在于蒙版是可以通過代碼或者動(dòng)畫來生成的,而不是局限于靜態(tài)的圖片。
拉伸過濾
這塊我之前沒有什么積累,就都直接引用作者的原話好了,不過代碼示例我會(huì)為大家準(zhǔn)備好的。
接下來要說的兩個(gè)屬性分別是minificationFilter
和 magnificationFilter
。總得來講,當(dāng)我們視圖顯示一個(gè)圖片的時(shí)候,都應(yīng)該正確地顯示這個(gè)圖片(意即:以 正確的比例和正確的1:1像素顯示在屏幕上)。原因如下:
- 能夠顯示最好的畫質(zhì),像素既沒有被壓縮也沒有被拉伸。
- 能更好的使用內(nèi)存,因?yàn)檫@就是所有你要存儲(chǔ)的東西。
- 最好的性能表現(xiàn),CPU不需要為此額外的計(jì)算。
但很多時(shí)候圖片的大小并不能很好的和視圖的大小保持一致,這個(gè)時(shí)候有一種叫做拉伸過濾的算法就起到作用了。它作用于原圖的像素上并根據(jù)需要生成新的像素顯示在屏幕上。CALayer
為此提供了三種拉伸過濾方法,他們是:
- kCAFilterLinear
- kCAFilterNearest
- kCAFilterTrilinear
minification
(縮小圖片)和magnification
(放大圖片)默認(rèn)的過濾器都是 kCAFilterLinear ,這個(gè)過濾器采用雙線性濾波算法,它在大多數(shù)情況下都表現(xiàn)良好。雙線性濾波算法通過對(duì)多個(gè)像素取樣最終生成新的值,得到一個(gè)平滑的表現(xiàn)不錯(cuò)的拉伸。但是當(dāng)放大倍數(shù)比較大的時(shí)候圖片就模糊不清了。
kCAFilterTrilinear
和kCAFilterLinear
非常相似,大部分情況下二者都看不出來有什么差別。但是,較雙線性濾波算法而言,三線性濾波算法存儲(chǔ)了多個(gè)大小情況下的圖片(也叫多重貼圖),并三維取樣,同時(shí)結(jié)合大圖和小圖的存儲(chǔ)進(jìn)而得到最后的結(jié)果。
這個(gè)方法的好處在于算法能夠從一系列已經(jīng)接近于最終大小的圖片中得到想要的結(jié)果,也就是說不要對(duì)很多像素同步取樣。這不僅提高了性能,也避免了小概率因舍入錯(cuò)誤引起的取樣失靈的問題。
kCAFilterNearest
是一種比較武斷的方法。從名字不難看出,這個(gè)算法(也叫最近過濾)就是取樣最近的單像素點(diǎn)而不管其他的顏色。這樣做非常快,也不會(huì)使圖片模糊。但是,最明顯的效果就是,會(huì)使得壓縮圖片更糟,圖片放大之后也顯得塊狀或是馬賽克嚴(yán)重。
總的來說,對(duì)于比較小的圖或者是差異特別明顯,極少斜線的大圖,最近過濾算法會(huì)保留這種差異明顯的特質(zhì)以呈現(xiàn)更好的結(jié)果。但是對(duì)于大多數(shù)的圖尤其是有很多斜線或是曲線輪廓的圖片來說,最近過濾算法會(huì)導(dǎo)致更差的結(jié)果。換句話說,線性過濾保留了形狀,最近過濾則保留了像素的差異。
下面來做一個(gè)簡單的小時(shí)鐘,
@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
作為修改,我們在for循環(huán)中加入以下代碼
view.layer.magnificationFilter = kCAFilterNearest;
組透明
UIView
中用來處理透明度的屬性叫alpha
,CALayer中對(duì)應(yīng)的屬性為opacity
,這兩個(gè)屬性都會(huì)對(duì)子層級(jí)產(chǎn)生影響,也就是說你給一個(gè)圖層設(shè)置了opacity
,他說有的子圖層的opacity
都會(huì)受到影響。下面的圖片中是一個(gè)白色的視圖內(nèi)部有一個(gè)白色的Label,右邊的視圖被設(shè)置了50%的透明度。
這是由透明度的混合疊加造成的,當(dāng)你顯示一個(gè)50%透明度的圖層時(shí),圖層的每個(gè)像素都會(huì)一半顯示自己的顏色,另一半顯示圖層下面的顏色。這是正常的透明度的表現(xiàn)。但是如果圖層包含一個(gè)同樣顯示50%透明的子圖層時(shí),你所看到的視圖,50%來自子視圖,25%來了圖層本身的顏色,另外的25%則來自背景色。
- 書中提到了可以將
info.plist
中的UIViewGroupOpacity
設(shè)置為YES
來達(dá)到整個(gè)圖層樹保持相同透明度的效果,但是當(dāng)前版本的Xcode
中UIViewGroupOpacity
已經(jīng)默認(rèn)設(shè)置為了YES
,所以想出現(xiàn)上圖中的效果需要先將UIViewGroupOpacity
設(shè)置為NO
。UIViewGroupOpacity
的缺點(diǎn)在于他是一個(gè)整體配置,整個(gè)應(yīng)用可能會(huì)受到不良影響。
除了UIViewGroupOpacity
,另一個(gè)方法就是啟用CALayer
的shouldRasterize
屬性來組透明效果。為了啟用shouldRasterize
屬性,我們設(shè)置了圖層的rasterizationScale
屬性。默認(rèn)情況下,所有圖層拉伸都是1.0, 所以如果你使用了shouldRasterize
屬性,你就要確保你設(shè)置了rasterizationScale
屬性去匹配屏幕,以防止出現(xiàn)Retina屏幕像素化的問題。
- (void)viewDidLoad {
[super viewDidLoad];
self.layerView.alpha = 0.5f;
self.layerView.layer.shouldRasterize = YES;
self.layerView.layer.rasterizationScale = [UIScreen mainScreen].scale;
}
總結(jié)
這一章介紹了一些可以通過代碼應(yīng)用到圖層上的視覺效果,比如圓角,陰影和蒙板。我們也了解了拉伸過濾器和組透明。
在第五章,『變換』中,我們將會(huì)研究圖層變化和3D轉(zhuǎn)換。