Core Animation 第四章 視覺效果

往期回顧:
序章
第一章 - 圖層樹
第二章 - 寄宿圖
第三章 - 圖層幾何
項(xiàng)目中使用的代碼

在這一章我們主要會(huì)討論一些通過CALayer屬性實(shí)現(xiàn)的視覺效果。

圓角


CALayer有一個(gè)叫做cornerRadius的屬性,他可以幫助我們不借助PS等工具輕松的搞定圓角矩形,這個(gè)屬性調(diào)整的是圖層角的曲率或者說是圓角半徑,默認(rèn)為0,也就是直角,而且曲率值只會(huì)影響背景顏色,而不會(huì)影響背景圖片或者子圖層。不過將masksToBounds為YES的話,圖層里面所有的東西都會(huì)被截取。下面來做一個(gè)簡單的例子。

兩個(gè)白色的大視圖,里面都包含了一個(gè)紅色的小視圖.png
  • 在書中這里提到使用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
設(shè)置過圓角后的兩個(gè)白色視圖.png

可以看到兩個(gè)白色視圖都表現(xiàn)出了圓角,但是只有第二個(gè)設(shè)置了masksToBounds的白色視圖剪裁掉了子視圖。

圖層邊框


CALayer另外連個(gè)常用的屬性就是borderWidthborderColor,兩個(gè)共同定義了圖層邊緣的繪制樣式,這條線(stroke)沿著圖層的bounds繪制,同時(shí)也包含圖層的角。其中 borderWidth 默認(rèn)為0, borderColor 默認(rèn)為黑色。

  • borderColorCGColorRef類型,由于他不是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;
    
}
為圖層添加邊框.png
  • 可以看到邊框并不會(huì)計(jì)算自圖層的位置和形狀而只是沿著圖層的邊界來繪制。
邊框并不會(huì)根據(jù)圖層里面的內(nèi)容變化.png

陰影


陰影是以前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用來控制陰影的模糊程度,值越大,陰影越模糊。

低shadowRadius 與 高shadowRadius

陰影剪裁

與邊框不容,陰影是可以繼承圖層內(nèi)容的形狀的,包括子視圖和寄宿圖的形狀。

陰影會(huì)自動(dòng)計(jì)算寄宿圖的形狀

結(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剪裁掉了

如果我們既需要masksToBounds同時(shí)也想要一個(gè)印象效果的話,可以通過一個(gè)比較tricky的方法來實(shí)現(xiàn),也就是在當(dāng)前視圖的下面添加一個(gè)同樣大小的透明視圖,使用它來顯示陰影。

當(dāng)前圖層樹的結(jié)構(gòu)

@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
同時(shí)擁有masksToBounds和陰影

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);
}
使用shadowPath自己繪制陰影形狀
  • 這里使用了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è)過場。效果如下:

使用mask制作一個(gè)紅色的皮卡丘.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;
    
    //設(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è)屬性分別是minificationFiltermagnificationFilter。總得來講,當(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í)候圖片就模糊不清了。
    kCAFilterTrilinearkCAFilterLinear 非常相似,大部分情況下二者都看不出來有什么差別。但是,較雙線性濾波算法而言,三線性濾波算法存儲(chǔ)了多個(gè)大小情況下的圖片(也叫多重貼圖),并三維取樣,同時(shí)結(jié)合大圖和小圖的存儲(chǔ)進(jìn)而得到最后的結(jié)果。
    這個(gè)方法的好處在于算法能夠從一系列已經(jīng)接近于最終大小的圖片中得到想要的結(jié)果,也就是說不要對(duì)很多像素同步取樣。這不僅提高了性能,也避免了小概率因舍入錯(cuò)誤引起的取樣失靈的問題。
對(duì)于大圖來說,雙線性濾波和三線性濾波表現(xiàn)得更出色

kCAFilterNearest 是一種比較武斷的方法。從名字不難看出,這個(gè)算法(也叫最近過濾)就是取樣最近的單像素點(diǎn)而不管其他的顏色。這樣做非常快,也不會(huì)使圖片模糊。但是,最明顯的效果就是,會(huì)使得壓縮圖片更糟,圖片放大之后也顯得塊狀或是馬賽克嚴(yán)重。

對(duì)于沒有斜線的小圖來說,最近過濾算法要好很多

總的來說,對(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
一個(gè)模糊的時(shí)鐘,由默認(rèn)的kCAFilterLinear引起

作為修改,我們在for循環(huán)中加入以下代碼

view.layer.magnificationFilter = kCAFilterNearest;
修改了magnificationFilter后的時(shí)鐘

組透明


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%的透明度。

右邊子視圖的label被顯示了出來

這是由透明度的混合疊加造成的,當(dāng)你顯示一個(gè)50%透明度的圖層時(shí),圖層的每個(gè)像素都會(huì)一半顯示自己的顏色,另一半顯示圖層下面的顏色。這是正常的透明度的表現(xiàn)。但是如果圖層包含一個(gè)同樣顯示50%透明的子圖層時(shí),你所看到的視圖,50%來自子視圖,25%來了圖層本身的顏色,另外的25%則來自背景色。

  • 書中提到了可以將info.plist中的UIViewGroupOpacity設(shè)置為YES來達(dá)到整個(gè)圖層樹保持相同透明度的效果,但是當(dāng)前版本的XcodeUIViewGroupOpacity已經(jīng)默認(rèn)設(shè)置為了YES,所以想出現(xiàn)上圖中的效果需要先將UIViewGroupOpacity設(shè)置為NOUIViewGroupOpacity的缺點(diǎn)在于他是一個(gè)整體配置,整個(gè)應(yīng)用可能會(huì)受到不良影響。

除了UIViewGroupOpacity,另一個(gè)方法就是啟用CALayershouldRasterize屬性來組透明效果。為了啟用shouldRasterize屬性,我們設(shè)置了圖層的rasterizationScale屬性。默認(rèn)情況下,所有圖層拉伸都是1.0, 所以如果你使用了shouldRasterize屬性,你就要確保你設(shè)置了rasterizationScale屬性去匹配屏幕,以防止出現(xiàn)Retina屏幕像素化的問題。

修復(fù)后的Label
- (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)換。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容