iOS核心動(dòng)畫高級(jí)技巧四(專用圖層)

目錄
  • CAShapLayer
  • CATextLayer
  • CATransformLayer
  • CAGradientLayer
  • CAReplicatorLayer
  • CAScrollLayer
  • CATiledLayer
  • CAEmitterLayer
  • CAEAGLLayer
  • AVPlayerLayer
  • 總結(jié)
序言

到目前為止晚岭,我們已經(jīng)探討過CALayer類了揉稚,同時(shí)我們也了解到了一些非常有用的繪圖動(dòng)畫功能。但是Core Animation圖層不僅僅能作用于圖片和顏色而已抹剩。本章就會(huì)學(xué)習(xí)其他的一些圖層類嗽测,進(jìn)一步擴(kuò)展使用Core Animation繪圖的能力绪励。

一 CAShapeLayer

CAShapeLayer是一個(gè)通過矢量圖形而不是bitmap來繪制的圖層子類。你指定諸如顏色線寬等屬性唠粥,用CGPath來定義想要繪制的圖形疏魏,最后CAShapeLayer就自動(dòng)渲染出來了。當(dāng)然晤愧,你也可以用Core Graphics直接向原始的CALyer的內(nèi)容中繪制一個(gè)路徑大莫,相比直下,使用CAShapeLayer有以下一些優(yōu)點(diǎn):

  • 渲染快速官份。CAShapeLayer使用了硬件加速只厘,繪制同一圖形會(huì)比用Core Graphics快很多。
  • 高效使用內(nèi)存舅巷。一個(gè)CAShapeLayer不需要像普通CALayer一樣創(chuàng)建一個(gè)寄宿圖形羔味,所以無論有多大,都不會(huì)占用太多的內(nèi)存悄谐。
  • 不會(huì)被圖層邊界剪裁掉介评。一個(gè)CAShapeLayer可以在邊界之外繪制。你的圖層路徑不會(huì)像在使用Core Graphics的普通CALayer一樣被剪裁掉(如我們?cè)诘诙滤姡?/li>
  • 不會(huì)出現(xiàn)像素化。當(dāng)你給CAShapeLayer做3D變換時(shí)们陆,它不像一個(gè)有寄宿圖的普通圖層一樣變得像素化寒瓦。
1.1 創(chuàng)建一個(gè)CGPath

CAShapeLayer可以用來繪制所有能夠通過CGPath來表示的形狀。這個(gè)形狀不一定要閉合坪仇,圖層路徑也不一定要不可破杂腰,事實(shí)上你可以在一個(gè)圖層上繪制好幾個(gè)不同的形狀。你可以控制一些屬性比如lineWith(線寬椅文,用點(diǎn)表示單位)喂很,lineCap(線條結(jié)尾的樣子),和lineJoin(線條之間的結(jié)合點(diǎn)的樣子)皆刺;但是在圖層層面你只有一次機(jī)會(huì)設(shè)置這些屬性少辣。如果你想用不同顏色或風(fēng)格來繪制多個(gè)形狀,就不得不為每個(gè)形狀準(zhǔn)備一個(gè)圖層了羡蛾。

下面代碼用一個(gè)CAShapeLayer渲染一個(gè)簡(jiǎn)單的火柴人漓帅。CAShapeLayer屬性是CGPathRef類型,但是我們用UIBezierPath幫助類創(chuàng)建了圖層路徑痴怨,這樣我們就不用考慮人工釋放CGPath了忙干。

- (void)shaperLayer {
    UIBezierPath *path = [[UIBezierPath alloc] init];
    [path moveToPoint:CGPointMake(175, 100)];
    
    // add path
    [path addArcWithCenter:CGPointMake(150, 100) radius:25 startAngle:0 endAngle:2*M_PI 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)];
    
    // create shape layer
    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;
    
    // add it to our view
    [self.view.layer addSublayer:shapeLayer];
}
  • 運(yùn)行結(jié)果如下
用CAShapeLayer繪制一個(gè)簡(jiǎn)單的火柴人 .png
1.2 圓角

前面里面提到了CAShapeLayer為創(chuàng)建圓角視圖提供了一個(gè)方法,就是CALayercornerRadius屬性浪藻。雖然使用CAShapeLayer類需要更多的工作捐迫,但是它有一個(gè)優(yōu)勢(shì)就是可以單獨(dú)指定每個(gè)角

我們創(chuàng)建圓角矩形其實(shí)就是人工繪制單獨(dú)的直線和弧度爱葵,但是事實(shí)上UIBezierPath有自動(dòng)繪制圓角矩形的構(gòu)造方法施戴,下面這段代碼繪制了一個(gè)有三個(gè)圓角一個(gè)直角的矩形:

/// 三個(gè)圓角的矩形
- (void)createThreeRadiusRound {
    // define path parameters
    CGRect rect = CGRectMake(100, 100, 100, 100);
    CGSize radii = CGSizeMake(20, 20);
    UIRectCorner corners = UIRectCornerTopRight | UIRectCornerBottomRight | UIRectCornerBottomLeft;
    // create path
    UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect byRoundingCorners:corners cornerRadii:radii];
    
    // create shape layer
    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;
    
    // add it to our view
    [self.view.layer addSublayer:shapeLayer];
}
  • 運(yùn)行結(jié)果如下
image.png
二 CATextLayer

用戶界面是無法從一個(gè)單獨(dú)的圖片里面構(gòu)建的。一個(gè)設(shè)計(jì)良好的圖標(biāo)能夠很好地表現(xiàn)一個(gè)按鈕或控件的意圖钧惧,不過你遲早都要需要一個(gè)不錯(cuò)的老式風(fēng)格的文本標(biāo)簽暇韧。

如果你想在一個(gè)圖層里面顯示文字,完全可以借助圖層代理直接將字符串使用Graphics寫入圖層的內(nèi)容(這就是UILabel的精髓)浓瞪。如果越過寄宿于圖層的視圖懈玻,直接在圖層上操作,那其實(shí)相當(dāng)繁瑣乾颁。你要為每一個(gè)顯示文字的圖層創(chuàng)建一個(gè)能像圖層代理一樣工作的類涂乌,還要邏輯上判斷哪個(gè)圖層需要顯示哪個(gè)字符串,更別提還要記錄不同的字體英岭,顏色等一系列亂七八糟的東西湾盒。

萬幸的是這些都是不必要的,Core Animation提供了一個(gè)CALayer的子類CATextLayer诅妹,它以圖層的形式包含了UILabel幾乎所有的繪制特性罚勾,并且額外提供了一些新的特性毅人。

同樣,CATextLayer也要比UILabel渲染得快得多尖殃。很少有人知道在iOS 6及之前的版本丈莺,UILabel其實(shí)是通過WebKit來實(shí)現(xiàn)繪制的,這樣就造成了當(dāng)有很多文字的時(shí)候就會(huì)有極大的性能壓力送丰。而CATextLayer使用了Core text缔俄,并且渲染得非常快器躏。

讓我們來嘗試用CATextLayer來顯示一些文字

/// CATextLayer
- (void)createCATextLayer {
    // create a text layer
    CATextLayer *textLayer = [CATextLayer layer];
    textLayer.frame = self.contentView.bounds;
    [self.contentView.layer addSublayer:textLayer];
    
    // set text attributes
    textLayer.foregroundColor = [UIColor blackColor].CGColor;
    textLayer.alignmentMode = kCAAlignmentJustified;
    textLayer.wrapped = YES;
    
    // set layer font
    UIFont *font = [UIFont systemFontOfSize:16];
    CFStringRef fontName = (__bridge CFStringRef)font.fontName;
    CGFontRef fontRef = CGFontCreateWithFontName(fontName);
    textLayer.font = fontName;
    textLayer.fontSize = font.pointSize;
    CGFontRelease(fontRef);
    
    // choose some text
    NSString *text = @"Lorem ipsum dolor sit amet, consectetur adipiscing \ elit. Quisque massa arcu, eleifend vel varius in, facilisis pulvinar \ leo. Nunc quis nunc at mauris pharetra condimentum ut ac neque. Nunc elementum, libero ut porttitor dictum, diam odio congue lacus, vel \ fringilla sapien diam at purus. Etiam suscipit pretium nunc sit amet \ lobortis";
    
    // set layer text
    textLayer.string = text;
}
  • 運(yùn)行結(jié)果如下
用CATextLayer來顯示一個(gè)純文本標(biāo)簽.png

如果你仔細(xì)看這個(gè)文本俐载,你會(huì)發(fā)現(xiàn)一個(gè)奇怪的地方:這些文本有一些像素化了。這是因?yàn)椴]有以Retina的方式渲染登失,前面提到了這個(gè)contentScale屬性遏佣,用來決定圖層內(nèi)容應(yīng)該以怎樣的分辨率來渲染。contentsScale并不關(guān)心屏幕的拉伸因素而總是默認(rèn)為1.0壁畸。如果我們想以Retina的質(zhì)量來顯示文字贼急,我們就得手動(dòng)地設(shè)置CATextLayer的contentsScale屬性,如下:

textLayer.contentsScale = [UIScreen mainScreen].scale;
  • 運(yùn)行結(jié)果如下
設(shè)置contentsScale來匹配屏幕.png

CATextLayerfont屬性不是一個(gè)UIFont類型捏萍,而是一個(gè)CFTypeRef類型。這樣可以根據(jù)你的具體需要來決定字體屬性應(yīng)該是用CGFontRef類型還是CTFontRef類型(Core Text字體)空闲。同時(shí)字體大小也是用fontSize屬性單獨(dú)設(shè)置的令杈,因?yàn)镃TFontRef和CGFontRef并不像UIFont一樣包含點(diǎn)大小。這個(gè)例子會(huì)告訴你如何將UIFont轉(zhuǎn)換成CGFontRef碴倾。

另外逗噩,CATextLayerstring屬性并不是你想象的NSString類型,而是id類型跌榔。這樣你既可以用NSString也可以用NSAttributedString來指定文本了(注意异雁,NSAttributedString并不是NSString的子類)。屬性化字符串是iOS用來渲染字體風(fēng)格的機(jī)制僧须,它以特定的方式來決定指定范圍內(nèi)的字符串的原始信息纲刀,比如字體,顏色担平,字重示绊,斜體等。

2.1 富文本

iOS 6中暂论,Apple給UILabel和其他UIKit文本視圖添加了直接的屬性化字符串的支持面褐,應(yīng)該說這是一個(gè)很方便的特性。不過事實(shí)上從iOS3.2開始CATextLayer就已經(jīng)支持屬性化字符串了取胎。這樣的話展哭,如果你想要支持更低版本的iOS系統(tǒng),CATextLayer無疑是你向界面中增加富文本的好辦法,而且也不用去跟復(fù)雜的Core Text打交道匪傍,也省了用UIWebView的麻煩坝咐。

讓我們編輯一下示例使用到NSAttributedString。iOS 6及以上我們可以用新的NSTextAttributeName實(shí)例來設(shè)置我們的字符串屬性析恢,但是練習(xí)的目的是為了演示在iOS 5及以下墨坚,所以我們用了Core Text,也就是說你需要把Core Text framework添加到你的項(xiàng)目中映挂。否則泽篮,編譯器是無法識(shí)別屬性常量的。

#import <CoreText/CTFont.h>
#import <CoreText/CTStringAttributes.h>

/// 富文本屬性
- (void)mutableAttrbuteString {
    // create a text layer
    CATextLayer *textLayer = [CATextLayer layer];
    textLayer.frame = self.contentView.bounds;
    textLayer.contentsScale = [UIScreen mainScreen].scale;
    [self.contentView.layer addSublayer:textLayer];
   
    //set text attributes
    textLayer.alignmentMode = kCAAlignmentJustified;
    textLayer.wrapped = YES;
    
    UIFont *font = [UIFont systemFontOfSize:15];
    NSString *text = @"Lorem ipsum dolor sit amet, consectetur adipiscing \ elit. Quisque massa arcu, eleifend vel varius in, facilisis pulvinar \ leo. Nunc quis nunc at mauris pharetra condimentum ut ac neque. Nunc elementum, libero ut porttitor dictum, diam odio congue lacus, vel \ fringilla sapien diam at purus. Etiam suscipit pretium nunc sit amet \ lobortis";
    
    // create attrbuted string
    NSMutableAttributedString *string = [[NSMutableAttributedString alloc] initWithString:text];
    
    // convert uifont to a cffont
    CFStringRef fontName = (__bridge CFStringRef)font.fontName;
    CGFloat fontSize = font.pointSize;
    CTFontRef fontRef = CTFontCreateWithName(fontName, fontSize, NULL);
    
    // set text attrbutes
    NSDictionary *attribs = @{
                              (__bridge id)kCTForegroundColorAttributeName:(__bridge id)[UIColor blackColor].CGColor,
                              (__bridge id)kCTFontAttributeName:(__bridge id)fontRef
                              };
    [string setAttributes:attribs range:NSMakeRange(0, [text length])];
    
    attribs = @{
                (__bridge id)kCTForegroundColorAttributeName: (__bridge id)[UIColor redColor].CGColor,
                (__bridge id)kCTUnderlineStyleAttributeName: @(kCTUnderlineStyleSingle),
                (__bridge id)kCTFontAttributeName: (__bridge id)fontRef
                };
    [string setAttributes:attribs range:NSMakeRange(6, 5)];
    
    // release the CTFont we created earlier
    CFRelease(fontRef);
    
    // set layer text
    textLayer.string = string;
}
  • 運(yùn)行結(jié)果如下
用CATextLayer實(shí)現(xiàn)一個(gè)富文本標(biāo)簽.png
2.2 行距和字距

有必要提一下的是柑船,由于繪制的實(shí)現(xiàn)機(jī)制不同(Core TextWebKit)帽撑,用CATextLayer渲染和用UILabel渲染出的文本行距和字距也不是不盡相同的。

二者的差異程度(由使用的字體和字符決定)總的來說挺小鞍时,但是如果你想正確的顯示普通便簽和CATextLayer就一定要記住這一點(diǎn)亏拉。

2.3 UILabel的替代品

我們已經(jīng)證實(shí)了CATextLayerUILabel有著更好的性能表現(xiàn),同時(shí)還有額外的布局選項(xiàng)并且在iOS 5上支持富文本逆巍。但是與一般的標(biāo)簽比較而言會(huì)更加繁瑣一些及塘。如果我們真的在需求一個(gè)UILabel的可用替代品,最好是能夠在Interface Builder上創(chuàng)建我們的標(biāo)簽锐极,而且盡可能地像一般的視圖一樣正常工作笙僚。

我們應(yīng)該繼承UILabel,然后添加一個(gè)子圖層CATextLayer并重寫顯示文本的方法灵再。但是仍然會(huì)有由UILabel的-drawRect:方法創(chuàng)建的空寄宿圖肋层。而且由于CALayer不支持自動(dòng)縮放和自動(dòng)布局,子視圖并不是主動(dòng)跟蹤視圖邊界的大小翎迁,所以每次視圖大小被更改栋猖,我們不得不手動(dòng)更新子圖層的邊界。

我們真正想要的是一個(gè)用CATextLayer作為宿主圖層的UILabel子類汪榔,這樣就可以隨著視圖自動(dòng)調(diào)整大小而且也沒有冗余的寄宿圖啦蒲拉。

就像我們?cè)诘谝徽?code>圖層樹討論的一樣,每一個(gè)UIView都是寄宿在一個(gè)CALayer的示例上揍异。這個(gè)圖層是由視圖自動(dòng)創(chuàng)建和管理的全陨,那我們可以用別的圖層類型替代它么?一旦被創(chuàng)建衷掷,我們就無法代替這個(gè)圖層了辱姨。但是如果我們繼承了UIView,那我們就可以重寫+layerClass方法使得在創(chuàng)建的時(shí)候能返回一個(gè)不同的圖層子類戚嗅。UIView會(huì)在初始化的時(shí)候調(diào)用+layerClass方法雨涛,然后用它的返回類型來創(chuàng)建宿主圖層枢舶。

下面演示了一個(gè)UILabel子類LayerLabelCATextLayer繪制它的問題,而不是調(diào)用一般的UILabel使用的較慢的-drawRect:方法替久。LayerLabel示例既可以用代碼實(shí)現(xiàn)凉泄,也可以在Interface Builder實(shí)現(xiàn),只要把普通的標(biāo)簽拖入視圖之中蚯根,然后設(shè)置它的類是LayerLabel就可以了后众。

代碼如下

#import "LayerLabel.h"

@implementation LayerLabel

+ (Class)layerClass {
    // this makes our label create a catextlayer
    // instead of a regular CALayer for its backing layer
    return [CATextLayer class];
}

- (CATextLayer *)textLayer {
    return (CATextLayer *)self.layer;
}

- (void)setup {
    // set defaults from uilabel settings
    self.text = self.text;
    self.textColor = self.textColor;
    self.font = self.font;
    
    // we should really derive these from the UILabel settings too
    // but that's complicated, so for now we'll just hard-code them
    [self textLayer].alignmentMode = kCAAlignmentJustified;
    
    [self textLayer].wrapped = YES;
    [self.layer display];
}

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

- (void)awakeFromNib {
    // called when creating label using Interface Builder
    [self setup];
}

- (void)setText:(NSString *)text {
    super.text = text;
    // set layer text
    [self textLayer].string = text;
}

- (void)setFont:(UIFont *)font {
    super.font = font;
    // set layer font
    CFStringRef fontName = (__bridge CFStringRef)font.fontName;
    CGFontRef fontRef = CGFontCreateWithFontName(fontName);
    [self textLayer].font = fontRef;
    [self textLayer].fontSize = font.pointSize;
    
    CGFontRelease(fontRef);
}

@end

如果你運(yùn)行代碼,你會(huì)發(fā)現(xiàn)文本并沒有像素化颅拦,而我們也沒有設(shè)置contentsScale屬性蒂誉。把CATextLayer作為宿主圖層的另一好處就是視圖自動(dòng)設(shè)置了contentsScale屬性。

在這個(gè)簡(jiǎn)單的例子中距帅,我們只是實(shí)現(xiàn)了UILabel的一部分風(fēng)格和布局屬性右锨,不過稍微再改進(jìn)一下我們就可以創(chuàng)建一個(gè)支持UILabel所有功能甚至更多功能的LayerLabel類(你可以在一些線上的開源項(xiàng)目中找到)。

如果你打算支持iOS 6及以上碌秸,基于CATextLayer的標(biāo)簽可能就有有些局限性绍移。但是總得來說,如果想在app里面充分利用CALayer子類讥电,用+layerClass來創(chuàng)建基于不同圖層的視圖是一個(gè)簡(jiǎn)單可復(fù)用的方法蹂窖。

三 CATransformLayer

當(dāng)我們?cè)跇?gòu)造復(fù)雜的3D事物的時(shí)候,如果能夠組織獨(dú)立元素就太方便了允趟。比如說恼策,你想創(chuàng)造一個(gè)孩子的手臂:你就需要確定哪一部分是孩子的手腕,哪一部分是孩子的前臂潮剪,哪一部分是孩子的肘,哪一部分是孩子的上臂分唾,哪一部分是孩子的肩膀等等抗碰。

當(dāng)然是允許獨(dú)立地移動(dòng)每個(gè)區(qū)域的啦。以肘為指點(diǎn)會(huì)移動(dòng)前臂和手绽乔,而不是肩膀弧蝇。Core Animation圖層很容易就可以讓你在2D環(huán)境下做出這樣的層級(jí)體系下的變換,但是3D情況下就不太可能折砸,因?yàn)樗械膱D層都把他的孩子都平面化到一個(gè)場(chǎng)景中(第五章『變換』有提到)看疗。

CATransformLayer解決了這個(gè)問題,CATransformLayer不同于普通的CALayer睦授,因?yàn)樗荒茱@示它自己的內(nèi)容咐旧。只有當(dāng)存在了一個(gè)能作用域子圖層的變換它才真正存在殖演。CATransformLayer并不平面化它的子圖層,所以它能夠用于構(gòu)造一個(gè)層級(jí)的3D結(jié)構(gòu)饱亿,比如我的手臂示例。

用代碼創(chuàng)建一個(gè)手臂需要相當(dāng)多的代碼懦傍,所以我就演示得更簡(jiǎn)單一些吧:在第五章的立方體示例,我們將通過旋轉(zhuǎn)camara來解決圖層平面化問題而不是像立方體示例代碼中用的sublayerTransform。這是一個(gè)非常不錯(cuò)的技巧淑廊,但是只能作用域單個(gè)對(duì)象上,如果你的場(chǎng)景包含兩個(gè)立方體特咆,那我們就不能用這個(gè)技巧單獨(dú)旋轉(zhuǎn)他們了季惩。

那么,就讓我們來試一試CATransformLayer吧腻格,第一個(gè)問題就來了:在第五章画拾,我們是用多個(gè)視圖來構(gòu)造了我們的立方體荒叶,而不是單獨(dú)的圖層碾阁。我們不能在不打亂已有的視圖層次的前提下在一個(gè)本身不是有寄宿圖的圖層中放置一個(gè)寄宿圖圖層。我們可以創(chuàng)建一個(gè)新的UIView子類寄宿在CATransformLayer(用+layerClass方法)之上些楣。但是脂凶,為了簡(jiǎn)化案例,我們僅僅重建了一個(gè)單獨(dú)的圖層愁茁,而不是使用視圖蚕钦。這意味著我們不能像第五章一樣在立方體表面顯示按鈕和標(biāo)簽,不過我們現(xiàn)在也用不到這個(gè)特性鹅很。

我們以我們?cè)诘谖逭率褂眠^的相同基本邏輯放置立方體嘶居。但是并不像以前那樣直接將立方面添加到容器視圖的宿主圖層,我們將他們放置到一個(gè)CATransformLayer中創(chuàng)建一個(gè)獨(dú)立的立方體對(duì)象促煮,然后將兩個(gè)這樣的立方體放進(jìn)容器中邮屁。我們隨機(jī)地給立方面染色以將他們區(qū)分開來,這樣就不用靠標(biāo)簽或是光亮來區(qū)分他們菠齿。

/// CATransformLayer
- (void)createTransformLayer {
    // setup the perspective transform
    CATransform3D pt = CATransform3DIdentity;
    pt.m34 = -1.0 / 500.0;
    self.contentView.layer.sublayerTransform = pt;
    
    // setup the transform for cube 1 and add it
    CATransform3D c1t = CATransform3DIdentity;
    c1t = CATransform3DTranslate(c1t, -100, 0, 0);
    CALayer *cube1 = [self cubeWithTransform:c1t];
    [self.contentView.layer addSublayer:cube1];
    
    // setup the transform for cube2 and add it
    CATransform3D c2t = CATransform3DIdentity;
    c2t = CATransform3DTranslate(c2t, 100, 0, 0);
    c2t = CATransform3DRotate(c2t, -M_PI_4, 1, 0, 0);
    c2t = CATransform3DRotate(c2t, -M_PI_4, 0, 1, 0);
    CALayer *cube2 = [self cubeWithTransform:c2t];
    [self.contentView.layer addSublayer:cube2];
}

- (CALayer *)faceWithTransform:(CATransform3D)transform {
    // create cube face layer
    CALayer *face = [CALayer layer];
    face.frame = CGRectMake(-50, -50, 100, 100);
    
    // apply a random color
    CGFloat red = (rand() / (double)INT_MAX);
    CGFloat green = (rand() / (double)INT_MAX);
    CGFloat blue = (rand() / (double)INT_MAX);
    face.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;
    
    // apply the transform and return
    face.transform = transform;
    
    return face;
}

- (CALayer *)cubeWithTransform:(CATransform3D)transform {
    // create cube layer
    CATransformLayer *cube = [CATransformLayer layer];
    
    // add cube face 1
    CATransform3D ct = CATransform3DMakeTranslation(0, 0, 50);
    [cube addSublayer:[self faceWithTransform:ct]];
    
    //add cube face 2
    ct = CATransform3DMakeTranslation(50, 0, 0);
    ct = CATransform3DRotate(ct, M_PI_2, 0, 1, 0);
    [cube addSublayer:[self faceWithTransform:ct]];
    
    //add cube face 3
    ct = CATransform3DMakeTranslation(0, -50, 0);
    ct = CATransform3DRotate(ct, M_PI_2, 1, 0, 0);
    [cube addSublayer:[self faceWithTransform:ct]];
    
    //add cube face 4
    ct = CATransform3DMakeTranslation(0, 50, 0);
    ct = CATransform3DRotate(ct, -M_PI_2, 1, 0, 0);
    [cube addSublayer:[self faceWithTransform:ct]];
    
    //add cube face 5
    ct = CATransform3DMakeTranslation(-50, 0, 0);
    ct = CATransform3DRotate(ct, -M_PI_2, 0, 1, 0);
    [cube addSublayer:[self faceWithTransform:ct]];
    
    //add cube face 6
    ct = CATransform3DMakeTranslation(0, 0, -50);
    ct = CATransform3DRotate(ct, M_PI, 0, 1, 0);
    [cube addSublayer:[self faceWithTransform:ct]];
    
    // center the cube layer within the container
    CGSize containerSize = self.contentView.bounds.size;
    cube.position = CGPointMake(containerSize.width * 0.5, containerSize.height * 0.5);
    
    // apply the transform
    cube.transform = transform;
    return cube;
}
  • 運(yùn)行結(jié)果如下
同一視角下的倆不同變換的立方體.png
四 CAGradientLayer

CAGradientLayer是用來生成兩種或更多顏色平滑漸變的佑吝。用Core Graphics復(fù)制一個(gè)CAGradientLayer并將內(nèi)容繪制到一個(gè)普通圖層的寄宿圖也是有可能的,但是CAGradientLayer的真正好處在于繪制使用了硬件加速绳匀。

CAGradientLayer也有startPointendPoint屬性芋忿,他們決定了漸變的方向。這兩個(gè)參數(shù)是以單位坐標(biāo)系進(jìn)行的定義疾棵,所以左上角坐標(biāo)是{0, 0}戈钢,右下角坐標(biāo)是{1, 1}

簡(jiǎn)單的兩種顏色的對(duì)角線漸變

/// 顏色漸變
- (void)createGradientLayer {
    // create gradient layer and add it to our container view
    CAGradientLayer *gradientLayer = [CAGradientLayer layer];
    gradientLayer.frame = self.contentView.bounds;
    [self.contentView.layer addSublayer:gradientLayer];
    
    // set gradient colors
    gradientLayer.colors = @[(__bridge id)[UIColor redColor].CGColor,
                             (__bridge id)[UIColor blueColor].CGColor];
    
    // set gradient start and end points
    gradientLayer.startPoint = CGPointMake(0, 0);
    gradientLayer.endPoint = CGPointMake(1, 1);
}
  • 運(yùn)行結(jié)果如下
用CAGradientLayer實(shí)現(xiàn)簡(jiǎn)單的兩種顏色的對(duì)角線漸變.png
4.2 多重漸變

如果你愿意,colors屬性可以包含很多顏色是尔,所以創(chuàng)建一個(gè)彩虹一樣的多重漸變也是很簡(jiǎn)單的殉了。默認(rèn)情況下,這些顏色在空間上均勻地被渲染嗜历,但是我們可以用locations屬性來調(diào)整空間宣渗。locations屬性是一個(gè)浮點(diǎn)數(shù)值的數(shù)組(以NSNumber包裝)抖所。這些浮點(diǎn)數(shù)定義了colors屬性中每個(gè)不同顏色的位置,同樣的痕囱,也是以單位坐標(biāo)系進(jìn)行標(biāo)定田轧。0.0代表著漸變的開始,1.0代表著結(jié)束鞍恢。

locations數(shù)組并不是強(qiáng)制要求的傻粘,但是如果你給它賦值了就一定要確保locations的數(shù)組大小和colors數(shù)組大小一定要相同,否則你將會(huì)得到一個(gè)空白的漸變帮掉。

下面代碼現(xiàn)在變成了從紅到黃最后到綠色的漸變弦悉。locations數(shù)組指定了0.0,0.25和0.5三個(gè)數(shù)值蟆炊,這樣這三個(gè)漸變就有點(diǎn)像擠在了左上角稽莉。

/// 多重顏色漸變
- (void)createMultiGradientLayer {
    // create gradient layer and add it to our container view
    CAGradientLayer *gradientLayer = [CAGradientLayer layer];
    gradientLayer.frame = self.contentView.bounds;
    [self.contentView.layer addSublayer:gradientLayer];
    
    // set gradient colors
    gradientLayer.colors = @[(__bridge id)[UIColor redColor].CGColor,
                             (__bridge id)[UIColor yellowColor].CGColor,
                             (__bridge id)[UIColor greenColor].CGColor];
    
    // set locations
    gradientLayer.locations = @[@0.0, @0.25, @0.5];
    
    // set gradient start and end points
    gradientLayer.startPoint = CGPointMake(0, 0);
    gradientLayer.endPoint = CGPointMake(1, 1);
}
用locations構(gòu)造偏移至左上角的三色漸變.png
五 CAReplicatorLayer

CAReplicatorLayer的目的是為了高效生成許多相似的圖層。它會(huì)繪制一個(gè)或多個(gè)圖層的子圖層涩搓,并在每個(gè)復(fù)制體上應(yīng)用不同的變換污秆。看上去演示能夠更加解釋這些昧甘,我們來寫個(gè)例子吧。

5.1 重復(fù)圖層(Repeating Layers)

我們?cè)谄聊坏闹虚g創(chuàng)建了一個(gè)小白色方塊圖層庸推,然后用CAReplicatorLayer生成十個(gè)圖層組成一個(gè)圓圈贬媒。instanceCount屬性指定了圖層需要重復(fù)多少次肘习。instanceTransform指定了一個(gè)CATransform3D后3D變換(這種情況下井厌,下一圖層的位移和旋轉(zhuǎn)將會(huì)移動(dòng)到圓圈的下一個(gè)點(diǎn))仅仆。

變換是逐步增加的,每個(gè)實(shí)例都是相對(duì)于前一實(shí)例布局垢袱。這就是為什么這些復(fù)制體最終不會(huì)出現(xiàn)在同意位置上墓拜,圖6.8是代碼運(yùn)行結(jié)果。

/// 5.重復(fù)圖層(Repeating Layers)
- (void)createReplicatorLayer {
    // create a replicator layer and add it to our view
    CAReplicatorLayer *replicator = [CAReplicatorLayer layer];
    replicator.frame = self.contentView.bounds;
    [self.contentView.layer addSublayer:replicator];
    
    // configure the replicator
    replicator.instanceCount = 10;
    
    // apply a transform for each instance
    CATransform3D transform = CATransform3DIdentity;
    transform = CATransform3DTranslate(transform, 0, 200, 0);
    transform = CATransform3DRotate(transform, M_PI / 5.0, 0, 0, 1);
    transform = CATransform3DTranslate(transform, 0, -200, 0);
    replicator.instanceTransform = transform;
    
    // apply a color shift for each instance
    replicator.instanceBlueOffset = -0.1;
    replicator.instanceGreenOffset = -0.1;
    
    // create a sublayer and place it inside the replicator
    CALayer *layer = [CALayer layer];
    layer.frame = CGRectMake(100.0f, 100.0f, 100.0f, 100.0f);
    layer.backgroundColor = [UIColor whiteColor].CGColor;
    [replicator addSublayer:layer];
}
  • 運(yùn)行結(jié)果如下
CAReplicatorLayer創(chuàng)建一圈圖層.png

注意到當(dāng)圖層在重復(fù)的時(shí)候请契,他們的顏色也在變化:這是用instanceBlueOffsetinstanceGreenOffset屬性實(shí)現(xiàn)的咳榜。通過逐步減少藍(lán)色和綠色通道夏醉,我們逐漸將圖層顏色轉(zhuǎn)換成了紅色。這個(gè)復(fù)制效果看起來很酷涌韩,但是CAReplicatorLayer真正應(yīng)用到實(shí)際程序上的場(chǎng)景比如:一個(gè)游戲中導(dǎo)彈的軌跡云畔柔,或者粒子爆炸(盡管iOS 5已經(jīng)引入了CAEmitterLayer,它更適合創(chuàng)建任意的粒子效果)臣樱。除此之外靶擦,還有一個(gè)實(shí)際應(yīng)用是:反射。

5.2 反射

使用CAReplicatorLayer并應(yīng)用一個(gè)負(fù)比例變換于一個(gè)復(fù)制圖層雇毫,你就可以創(chuàng)建指定視圖(或整個(gè)視圖層次)內(nèi)容的鏡像圖片玄捕,這樣就創(chuàng)建了一個(gè)實(shí)時(shí)的反射效果。讓我們來嘗試實(shí)現(xiàn)這個(gè)創(chuàng)意:指定一個(gè)繼承于UIView的ReflectionView棚放,它會(huì)自動(dòng)產(chǎn)生內(nèi)容的反射效果枚粘。實(shí)現(xiàn)這個(gè)效果的代碼很簡(jiǎn)單,實(shí)際上用ReflectionView實(shí)現(xiàn)這個(gè)效果會(huì)更簡(jiǎn)單柬姚,我們只需生成ReflectionView的實(shí)例,它就會(huì)實(shí)時(shí)生成子視圖的反射撕捍。

#import "ReflectionView.h"

@implementation ReflectionView

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

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

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

- (void)setup {
    CAReplicatorLayer *layer = (CAReplicatorLayer *)self.layer;
    layer.instanceCount = 2;
    
    // move reflection instance below original and flip vertically
    CATransform3D transform = CATransform3DIdentity;
    CGFloat verticalOffset = self.bounds.size.height * 0.5;
    transform = CATransform3DTranslate(transform, 0, verticalOffset, 0);
    transform = CATransform3DScale(transform, 1, -1, 0);
    layer.instanceTransform = transform;
    
    //reduce alpha of reflection layer
    replicatorLayer.instanceAlphaOffset = -0.6;

    UIImage *image = [UIImage imageNamed:@"cat"];
    
    CALayer *imageLayer = [CALayer layer];
    [imageLayer setContentsScale:[[UIScreen mainScreen] scale]];
    [imageLayer setContents:(__bridge id)[image CGImage]];
    [imageLayer setBounds:CGRectMake(0.0, 0.0, [image size].width, [image size].height)];
    [imageLayer setAnchorPoint:CGPointMake(0.0, 0.0)];
    
    [replicatorLayer addSublayer:imageLayer];
}

@end
  • 運(yùn)行結(jié)果如下
image.png
六 CAScrollLayer

對(duì)于一個(gè)未轉(zhuǎn)換的圖層狮腿,它的bounds和它的frame是一樣的,frame屬性是由bounds屬性自動(dòng)計(jì)算而出的贴硫,所以更改任意一個(gè)值都會(huì)更新其他值。

但是如果你只想顯示一個(gè)大圖層里面的一小部分呢汁尺。比如說,你可能有一個(gè)很大的圖片苞也,你希望用戶能夠隨意滑動(dòng),或者是一個(gè)數(shù)據(jù)或文本的長列表殷勘。在一個(gè)典型的iOS應(yīng)用中,你可能會(huì)用到UITableView或是UIScrollView贤斜,但是對(duì)于獨(dú)立的圖層來說,什么會(huì)等價(jià)于剛剛提到的UITableView和UIScrollView呢?

在第二章中阳堕,我們探索了圖層的contentsRect屬性的用法,它的確是能夠解決在圖層中小地方顯示大圖片的解決方法。但是如果你的圖層包含子圖層那它就不是一個(gè)非常好的解決方案道偷,因?yàn)椴⑽。@樣做的話每次你想滑動(dòng)可視區(qū)域的時(shí)候,你就需要手工重新計(jì)算并更新所有的子圖層位置剃执。

這個(gè)時(shí)候就需要CAScrollLayer了。CAScrollLayer有一個(gè)-scrollToPoint:方法,它自動(dòng)適應(yīng)bounds的原點(diǎn)以便圖層內(nèi)容出現(xiàn)在滑動(dòng)的地方遣耍。注意,這就是它做的所有事情棋傍。前面提到過,Core Animation并不處理用戶輸入麸拄,所以CAScrollLayer并不負(fù)責(zé)將觸摸事件轉(zhuǎn)換為滑動(dòng)事件,既不渲染滾動(dòng)條,也不實(shí)現(xiàn)任何iOS指定行為例如滑動(dòng)反彈(當(dāng)視圖滑動(dòng)超多了它的邊界的將會(huì)反彈回正確的地方)主穗。

讓我們來用CAScrollLayer來常見一個(gè)基本的UIScrollView替代品争拐。我們將會(huì)用CAScrollLayer作為視圖的宿主圖層,并創(chuàng)建一個(gè)自定義的UIView绑雄,然后用UIPanGestureRecognizer實(shí)現(xiàn)觸摸事件響應(yīng)。

@implementation ScrollView

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

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

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

- (void)setup {
    // enable clipping
    self.layer.masksToBounds = YES;
    
    // attach pan gesture recongnizer
    UIPanGestureRecognizer *recognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)];
    [self addGestureRecognizer:recognizer];
    
    UIImage *image = [UIImage imageNamed:@"cat"];
    
    CALayer *imageLayer = [CALayer layer];
    [imageLayer setContentsScale:[[UIScreen mainScreen] scale]];
    [imageLayer setContents:(__bridge id)[image CGImage]];
    [imageLayer setBounds:CGRectMake(0.0, 0.0, [image size].width, [image size].height)];
    [imageLayer setAnchorPoint:CGPointMake(0.0, 0.0)];
    
    [(CAScrollLayer *)self.layer addSublayer:imageLayer];
}

- (void)pan:(UIPanGestureRecognizer *)recognizer {
    //get the offset by subtracting the pan gesture
    //translation from the current bounds origin
    CGPoint offset = self.bounds.origin;
    offset.x -= [recognizer translationInView:self].x;
    offset.y -= [recognizer translationInView:self].y;
    
    // scroll the layer
    [(CAScrollLayer *)self.layer scrollToPoint:offset];
    
    // reset the pan gesture translation
    [recognizer setTranslation:CGPointZero inView:self];
}

@end
  • 運(yùn)行結(jié)果如下
Aug-31-2019 17-40-01.gif

不同于UIScrollView已脓,我們定制的滑動(dòng)視圖類并沒有實(shí)現(xiàn)任何形式的邊界檢查(bounds checking)厕宗。圖層內(nèi)容極有可能滑出視圖的邊界并無限滑下去。CAScrollLayer并沒有等同于UIScrollViewcontentSize的屬性,所以當(dāng)CAScrollLayer滑動(dòng)的時(shí)候完全沒有一個(gè)全局的可滑動(dòng)區(qū)域的概念膜楷,也無法自適應(yīng)它的邊界原點(diǎn)至你指定的值。它之所以不能自適應(yīng)邊界大小是因?yàn)樗恍枰卦福瑑?nèi)容完全可以超過邊界宗收。

那你一定會(huì)奇怪用CAScrollLayer的意義到底何在采驻,因?yàn)槟憧梢院?jiǎn)單地用一個(gè)普通的CALayer然后手動(dòng)適應(yīng)邊界原點(diǎn)啊膳叨。真相其實(shí)并不復(fù)雜,UIScrollView并沒有用CAScrollLayer龄坪,事實(shí)上,就是簡(jiǎn)單的通過直接操作圖層邊界來實(shí)現(xiàn)滑動(dòng)妓局。

CAScrollLayer有一個(gè)潛在的有用特性好爬。如果你查看CAScrollLayer的頭文件型豁,你就會(huì)注意到有一個(gè)擴(kuò)展分類實(shí)現(xiàn)了一些方法和屬性:

- (void)scrollPoint:(CGPoint)p;
- (void)scrollRectToVisible:(CGRect)r;
@property(readonly) CGRect visibleRect;

看到這些方法和屬性名充尉,你也許會(huì)以為這些方法給每個(gè)CALayer實(shí)例增加了滑動(dòng)功能姿鸿。但是事實(shí)上他們只是放置在CAScrollLayer中的圖層的實(shí)用方法。

scrollPoint:方法從圖層樹中查找并找到第一個(gè)可用的CAScrollLayer热某,然后滑動(dòng)它使得指定點(diǎn)成為可視的。scrollRectToVisible:方法實(shí)現(xiàn)了同樣的事情只不過是作用在一個(gè)矩形上的秘遏。visibleRect屬性決定圖層(如果存在的話)的哪部分是當(dāng)前的可視區(qū)域。如果你自己實(shí)現(xiàn)這些方法就會(huì)相對(duì)容易明白一點(diǎn),但是CAScrollLayer幫你省了這些麻煩审丘,所以當(dāng)涉及到實(shí)現(xiàn)圖層滑動(dòng)的時(shí)候就可以用上了。

七 CATiledLayer

有些時(shí)候你可能需要繪制一個(gè)很大的圖片脓钾,常見的例子就是一個(gè)高像素的照片或者是地球表面的詳細(xì)地圖。iOS應(yīng)用通暢運(yùn)行在內(nèi)存受限的設(shè)備上握截,所以讀取整個(gè)圖片到內(nèi)存中是不明智的。載入大圖可能會(huì)相當(dāng)?shù)芈枧切?duì)你看上去比較方便的做法(在主線程調(diào)用UIImage的-imageNamed:方法或者-imageWithContentsOfFile:方法)將會(huì)阻塞你的用戶界面蒲讯,至少會(huì)引起動(dòng)畫卡頓現(xiàn)象。

能高效繪制在iOS上的圖片也有一個(gè)大小限制。所有顯示在屏幕上的圖片最終都會(huì)被轉(zhuǎn)化為OpenGL紋理,同時(shí)OpenGL有一個(gè)最大的紋理尺寸(通常是20482048独郎,或40964096贪婉,這個(gè)取決于設(shè)備型號(hào))莫湘。如果你想在單個(gè)紋理中顯示一個(gè)比這大的圖腰池,即便圖片已經(jīng)存在于內(nèi)存中了,你仍然會(huì)遇到很大的性能問題,因?yàn)?code>Core Animation強(qiáng)制用CPU處理圖片而不是更快的GPU后面會(huì)更加詳細(xì)地解釋了軟件繪制和硬件繪制)拍皮。

CATiledLayer為載入大圖造成的性能問題提供了一個(gè)解決方案:將大圖分解成小片然后將他們單獨(dú)按需載入咆耿。讓我們用實(shí)驗(yàn)來證明一下。

7.1 小片裁剪

我們將會(huì)從一個(gè)2048*2048分辨率的雪人圖片入手。為了能夠從CATiledLayer中獲益,我們需要把這個(gè)圖片裁切成許多小一些的圖片艾帐。你可以通過代碼來完成這件事情,但是如果你在運(yùn)行時(shí)讀入整個(gè)圖片并裁切,那CATiledLayer這些所有的性能優(yōu)點(diǎn)就損失殆盡了。理想情況下來說腥泥,最好能夠逐個(gè)步驟來實(shí)現(xiàn)夹厌。

演示了一個(gè)簡(jiǎn)單的Mac OS命令行程序裆悄,它用CATiledLayer將一個(gè)圖片裁剪成小圖并存儲(chǔ)到不同的文件中矛纹。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // handle incorrect arguments
        if (argc < 2) {
            NSLog(@"TileCutter arguments: inputfile");
            return 0;
        }
        
        // input file
        NSString *inputFile = [NSString stringWithCString:argv[1] encoding:NSUTF8StringEncoding];
        
        // tile size
        CGFloat tileSize = 256; // output path
        NSString *outputPath = [inputFile stringByDeletingPathExtension];
        
        // load image
        NSImage *image = [[NSImage alloc] initWithContentsOfFile:inputFile];
        NSSize size = [image size];
        NSArray *representations = [image representations];
        
        if ([representations count]) {
            NSBitmapImageRep *representation = representations[0];
            size.width = [representation pixelsWide];
            size.height = [representation pixelsHigh];
        }
        
        NSRect rect = NSMakeRect(0, 0, size.width, size.height);
        CGImageRef imageRef = [image CGImageForProposedRect:&rect context:NULL hints:nil];

        // calculate rows and columns
        NSInteger rows = ceil(size.height / tileSize);
        NSInteger cols = ceil(size.width / tileSize);
        
        // generate tiles
        for (int y = 0; y < rows; ++y) {
            for (int x = 0; x < cols; ++x) {
                // extract tile image
                CGRect tileRect = CGRectMake(x * tileSize, y * tileSize, tileSize, tileSize);
                CGImageRef tileImage = CGImageCreateWithImageInRect(imageRef, tileRect);
                
                // convert to jpeg data
                NSBitmapImageRep *imageRep = [[NSBitmapImageRep alloc] initWithCGImage:tileImage];
                NSData *data = [imageRep representationUsingType:NSJPEGFileType properties:nil];
                CGImageRelease(tileImage);
                
                // save file
                NSString *path = [outputPath stringByAppendingFormat: @"_%02i_%02i.jpg", x, y];
                [data writeToFile:path atomically:NO];
            }
        }
    }
    
    return NSApplicationMain(argc, argv);
}

這個(gè)程序?qū)?code>2048*2048分辨率的圖案裁剪成了64個(gè)不同的256*256的小圖。(256*256是CATiledLayer的默認(rèn)小圖大小光稼,默認(rèn)大小可以通過tileSize屬性更改)或南。程序接受一個(gè)圖片路徑作為命令行的第一個(gè)參數(shù)孩等。我們可以在編譯的scheme將路徑參數(shù)硬編碼然后就可以在Xcode中運(yùn)行了权她,但是以后作用在另一個(gè)圖片上就不方便了虏肾。所以藻雌,我們編譯了這個(gè)程序并把它保存到敏感的地方滚局,然后從終端調(diào)用,如下面所示:

> path/to/TileCutterApp path/to/Snowman.jpg

這個(gè)程序相當(dāng)基礎(chǔ)略贮,但是能夠輕易地?cái)U(kuò)展支持額外的參數(shù)比如小圖大小拄丰,或者導(dǎo)出格式等等。運(yùn)行結(jié)果是64個(gè)新圖的序列牡整,如下面命名:

Snowman_00_00.jpg
Snowman_00_01.jpg
Snowman_00_02.jpg
...
Snowman_07_07.jpg

既然我們有了裁切后的小圖,我們就要讓iOS程序用到他們昼弟。CATiledLayer很好地和UIScrollView集成在一起旬盯。除了設(shè)置圖層和滑動(dòng)視圖邊界以適配整個(gè)圖片大小,我們真正要做的就是實(shí)現(xiàn)-drawLayer:inContext:方法员萍,當(dāng)需要載入新的小圖時(shí),CATiledLayer就會(huì)調(diào)用到這個(gè)方法极景。

一個(gè)簡(jiǎn)單的滾動(dòng)CATiledLayer實(shí)現(xiàn)

/// 7 CATiledLayer
- (void)createTiledLayer {
    // create scrollView
    self.scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, 300, 300)];
    self.scrollView.center = self.view.center;
    [self.view addSubview:self.scrollView];
    
    //add the tiled layer
    CATiledLayer *tileLayer = [CATiledLayer layer];
    tileLayer.frame = CGRectMake(0, 0, 2048, 2048);
    tileLayer.delegate = self;
    [self.scrollView.layer addSublayer:tileLayer];
    
    //configure the scroll view
    self.scrollView.contentSize = tileLayer.frame.size;
    
    //draw layer
    [tileLayer setNeedsDisplay];
}

- (void)drawLayer:(CATiledLayer *)layer inContext:(CGContextRef)ctx {
    // determine tile coordinate
    CGRect bounds = CGContextGetClipBoundingBox(ctx);
    NSInteger x = floor(bounds.origin.x / layer.tileSize.width);
    NSInteger y = floor(bounds.origin.y / layer.tileSize.height);
    
    // load tile image
    NSString *imageName = [NSString stringWithFormat: @"Snowman_%02li_%02li", (long)x, (long)y];
    NSString *imagePath = [[NSBundle mainBundle] pathForResource:imageName ofType:@"jpg"];
    UIImage *tileImage = [UIImage imageWithContentsOfFile:imagePath];
    
    //draw tile
    UIGraphicsPushContext(ctx);
    [tileImage drawInRect:bounds];
    UIGraphicsPopContext();
}

當(dāng)你滑動(dòng)這個(gè)圖片与境,你會(huì)發(fā)現(xiàn)當(dāng)CATiledLayer載入小圖的時(shí)候,他們會(huì)淡入到界面中家坎。這是CATiledLayer的默認(rèn)行為。(你可能已經(jīng)在iOS 6之前的蘋果地圖程序中見過這個(gè)效果)你可以用fadeDuration屬性改變淡入時(shí)長或直接禁用掉绩衷。CATiledLayer(不同于大部分的UIKit和Core Animation方法)支持多線程繪制蜕衡,-drawLayer:inContext:方法可以在多個(gè)線程中同時(shí)地并發(fā)調(diào)用,所以請(qǐng)小心謹(jǐn)慎地確保你在這個(gè)方法中實(shí)現(xiàn)的繪制代碼是線程安全的悴侵。

7.2 Retina小圖

你也許已經(jīng)注意到了這些小圖并不是以Retina的分辨率顯示的。為了以屏幕的原生分辨率來渲染CATiledLayer,我們需要設(shè)置圖層的contentsScale來匹配UIScreen的scale屬性:

tileLayer.contentsScale = [UIScreen mainScreen].scale;

有趣的是厢岂,tileSize是以像素為單位馍管,而不是點(diǎn)谊路,所以增大了contentsScale就自動(dòng)有了默認(rèn)的小圖尺寸(現(xiàn)在它是128128的點(diǎn)而不是256256).所以,我們不需要手工更新小圖的尺寸或是在Retina分辨率下指定一個(gè)不同的小圖根悼。我們需要做的是適應(yīng)小圖渲染代碼以對(duì)應(yīng)安排scale的變化凶异,然而:

//determine tile coordinate
CGRect bounds = CGContextGetClipBoundingBox(ctx);
CGFloat scale = [UIScreen mainScreen].scale;
NSInteger x = floor(bounds.origin.x / layer.tileSize.width * scale);
NSInteger y = floor(bounds.origin.y / layer.tileSize.height * scale);

通過這個(gè)方法糾正scale也意味著我們的雪人圖將以一半的大小渲染在Retina設(shè)備上(總尺寸是10241024,而不是20482048)挤巡。這個(gè)通常都不會(huì)影響到用CATiledLayer正常顯示的圖片類型(比如照片和地圖剩彬,他們?cè)谠O(shè)計(jì)上就是要支持放大縮小,能夠在不同的縮放條件下顯示)矿卑,但是也需要在心里明白喉恋。

八 CAEmitterLayer

在iOS 5中,蘋果引入了一個(gè)新的CALayer子類叫做CAEmitterLayer母廷。CAEmitterLayer是一個(gè)高性能的粒子引擎轻黑,被用來創(chuàng)建實(shí)時(shí)例子動(dòng)畫如:煙霧,火琴昆,雨等等這些效果氓鄙。

CAEmitterLayer看上去像是許多CAEmitterCell的容器,這些CAEmitierCell定義了一個(gè)例子效果业舍。你將會(huì)為不同的例子效果定義一個(gè)或多個(gè)CAEmitterCell作為模版抖拦,同時(shí)CAEmitterLayer負(fù)責(zé)基于這些模版實(shí)例化一個(gè)粒子流。一個(gè)CAEmitterCell類似于一個(gè)CALayer:它有一個(gè)contents屬性可以定義為一個(gè)CGImage舷暮,另外還有一些可設(shè)置屬性控制著表現(xiàn)和行為态罪。我們不會(huì)對(duì)這些屬性逐一進(jìn)行詳細(xì)的描述,你們可以在CAEmitterCell類的頭文件中找到下面。

/// 8 CAEmitterLayer 粒子效果
- (void)createCAEmitterLayer {
    // create particle emitter layer
    CAEmitterLayer *emitter = [CAEmitterLayer layer];
    emitter.frame = self.contentView.bounds;
    [self.contentView.layer addSublayer:emitter];
    
    // configure emitter
    emitter.renderMode = kCAEmitterLayerAdditive;
    emitter.emitterPosition = CGPointMake(emitter.frame.size.width * 0.5, emitter.frame.size.height * 0.5);
    
    // create a particle template
    CAEmitterCell *cell = [[CAEmitterCell alloc] init];
    cell.contents = (__bridge id)[UIImage imageNamed:@"red"].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;
    
    // add particle template to emitter
    emitter.emitterCells = @[cell];
}
  • 運(yùn)行效果如下
火焰爆炸效果.gif

CAEMitterCell的屬性基本上可以分為三種:

  • 這種粒子的某一屬性的初始值复颈。比如,color屬性指定了一個(gè)可以混合圖片內(nèi)容顏色的混合色沥割。在示例中耗啦,我們將它設(shè)置為桔色。

  • 例子某一屬性的變化范圍驯遇。比如emissionRange屬性的值是2π芹彬,這意味著例子可以從360度任意位置反射出來。如果指定一個(gè)小一些的值叉庐,就可以創(chuàng)造出一個(gè)圓錐形

  • 指定值在時(shí)間線上的變化舒帮。比如,在示例中陡叠,我們將alphaSpeed設(shè)置為-0.4玩郊,就是說例子的透明度每過一秒就是減少0.4,這樣就有發(fā)射出去之后逐漸小時(shí)的效果枉阵。

CAEmitterLayer的屬性它自己控制著整個(gè)例子系統(tǒng)的位置和形狀译红。一些屬性比如birthRatelifetimecelocity兴溜,這些屬性在CAEmitterCell中也有侦厚。這些屬性會(huì)以相乘的方式作用在一起耻陕,這樣你就可以用一個(gè)值來加速或者擴(kuò)大整個(gè)例子系統(tǒng)。其他值得提到的屬性有以下這些:

  • preservesDepth刨沦,是否將3D例子系統(tǒng)平面化到一個(gè)圖層(默認(rèn)值)或者可以在3D空間中混合其他的圖層

  • renderMode诗宣,控制著在視覺上粒子圖片是如何混合的。你可能已經(jīng)注意到了示例中我們把它設(shè)置為kCAEmitterLayerAdditive想诅,它實(shí)現(xiàn)了這樣一個(gè)效果:合并例子重疊部分的亮度使得看上去更亮召庞。如果我們把它設(shè)置為默認(rèn)的kCAEmitterLayerUnordered,效果就沒那么好看了

emitter.renderMode = kCAEmitterLayerUnordered;
  • 運(yùn)行效果如下
禁止混色之后的火焰粒子.gif
九 CAEAGLLayer

當(dāng)iOS要處理高性能圖形繪制来破,必要時(shí)就是OpenGL篮灼。應(yīng)該說它應(yīng)該是最后的殺手锏,至少對(duì)于非游戲的應(yīng)用來說是的徘禁。因?yàn)橄啾?code>Core Animation和UIkit框架诅诱,它不可思議地復(fù)雜。

OpenGL提供了Core Animation的基礎(chǔ)送朱,它是底層的C接口逢艘,直接和iPhone,iPad的硬件通信骤菠,極少地抽象出來的方法它改。OpenGL沒有對(duì)象或是圖層的繼承概念。它只是簡(jiǎn)單地處理三角形商乎。OpenGL中所有東西都是3D空間中有顏色和紋理的三角形央拖。用起來非常復(fù)雜和強(qiáng)大,但是用OpenGL繪制iOS用戶界面就需要很多很多的工作了鹉戚。

為了能夠以高性能使用Core Animation鲜戒,你需要判斷你需要繪制哪種內(nèi)容(矢量圖形,例子抹凳,文本遏餐,等等),但后選擇合適的圖層去呈現(xiàn)這些內(nèi)容赢底,Core Animation中只有一些類型的內(nèi)容是被高度優(yōu)化的失都;所以如果你想繪制的東西并不能找到標(biāo)準(zhǔn)的圖層類,想要得到高性能就比較費(fèi)事情了幸冻。

因?yàn)?code>OpenGL根本不會(huì)對(duì)你的內(nèi)容進(jìn)行假設(shè)粹庞,它能夠繪制得相當(dāng)快。利用OpenGL洽损,你可以繪制任何你知道必要的集合信息和形狀邏輯的內(nèi)容庞溜。所以很多游戲都喜歡用OpenGL(這些情況下,Core Animation的限制就明顯了:它優(yōu)化過的內(nèi)容類型并不一定能滿足需求)碑定,但是這樣依賴流码,方便的高度抽象接口就沒了又官。

在iOS 5中,蘋果引入了一個(gè)新的框架叫做GLKit漫试,它去掉了一些設(shè)置OpenGL的復(fù)雜性赏胚,提供了一個(gè)叫做CLKView的UIView的子類,幫你處理大部分的設(shè)置和繪制工作商虐。前提是各種各樣的OpenGL繪圖緩沖的底層可配置項(xiàng)仍然需要你用CAEAGLLayer完成,它是CALayer的一個(gè)子類崖疤,用來顯示任意的OpenGL圖形秘车。

大部分情況下你都不需要手動(dòng)設(shè)置CAEAGLLayer(假設(shè)用GLKView),過去的日子就不要再提了劫哼。特別的叮趴,我們將設(shè)置一個(gè)OpenGL ES 2.0的上下文,它是現(xiàn)代的iOS設(shè)備的標(biāo)準(zhǔn)做法权烧。

盡管不需要GLKit也可以做到這一切眯亦,但是GLKit囊括了很多額外的工作,比如設(shè)置頂點(diǎn)和片段著色器般码,這些都以類C語言叫做GLSL自包含在程序中妻率,同時(shí)在運(yùn)行時(shí)載入到圖形硬件中。編寫GLSL代碼和設(shè)置EAGLayer沒有什么關(guān)系板祝,所以我們將用GLKBaseEffect類將著色邏輯抽象出來宫静。其他的事情,我們還是會(huì)有以往的方式券时。

在開始之前孤里,你需要將GLKitOpenGLES框架加入到你的項(xiàng)目中,然后就可以實(shí)現(xiàn)了橘洞,里面是設(shè)置一個(gè)GAEAGLLayer的最少工作捌袜,它使用了OpenGL ES 2.0 的繪圖上下文,并渲染了一個(gè)有色三角炸枣。

#import <GLKit/GLKit.h>
#import <OpenGLES/OpenGLESAvailability.h>

#pragma mark - CAEAGLLayer 相關(guān)變量
/** glView */
@property(nonatomic, strong)UIView *glView;
/** glContext */
@property(nonatomic, strong)EAGLContext *glContext;
/** glLayer */
@property(nonatomic, strong)CAEAGLLayer *glLayer;
/** frameBuffer */
@property(nonatomic, assign)GLuint frameBuffer;
/** colorRenderbuffer */
@property(nonatomic, assign)GLuint colorRenderbuffer;
/** framebufferWidth */
@property(nonatomic, assign)GLuint frameBufferWidth;
/** framebufferHeight */
@property(nonatomic, assign)GLuint frameBufferHeight;
/** effect */
@property(nonatomic, strong)GLKBaseEffect *effect;

/// 9.CAEAGLLayer
- (void)createEAGLayer {
    // setup context
    self.glContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
    [EAGLContext setCurrentContext:self.glContext];
    
    self.glView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
    self.glView.center = self.view.center;
    [self.view addSubview:self.glView];
    
    // setup layer
    self.glLayer = [CAEAGLLayer layer];
    self.glLayer.frame = self.glView.bounds;
    [self.glView.layer addSublayer:self.glLayer];
    self.glLayer.drawableProperties = @{kEAGLDrawablePropertyRetainedBacking:@NO, kEAGLDrawablePropertyColorFormat: kEAGLColorFormatRGBA8};

    // setup base effect
    self.effect = [[GLKBaseEffect alloc] init];
    
    // setup buffers
    [self setUpBuffers];
    
    // draw frame
    [self drawFrame];
}

- (void)viewDidUnload {
    [self tearDownBuffers];
    [super viewDidLoad];
}

- (void)dealloc {
    [self tearDownBuffers];
    [EAGLContext setCurrentContext:nil];
}

- (void)setUpBuffers {
    // setup frame buffer
    glGenFramebuffers(1, &_frameBuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, _frameBuffer);
    
    // setup color render buffer
    glGenRenderbuffers(1, &_colorRenderbuffer);
    glBindRenderbuffer(GL_RENDERBUFFER, _colorRenderbuffer);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _colorRenderbuffer);
    [self.glContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.glLayer];
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &_frameBufferWidth);
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &_frameBufferHeight);
    
    // check success
    if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
        NSLog(@"Failed to make complete framebuffer object: %i", glCheckFramebufferStatus(GL_FRAMEBUFFER));
    }
}

- (void)tearDownBuffers {
    if (_frameBuffer) {
        // delete frameBuffer
        glDeleteFramebuffers(1, &_frameBuffer);
        _frameBuffer = 0;
    }
    
    if (_colorRenderbuffer) {
        // delete color render buffer
        glDeleteRenderbuffers(1, &_colorRenderbuffer);
        _colorRenderbuffer = 0;
    }
}

- (void)drawFrame {
    // bind framebuffer & set viewPort
    glBindFramebuffer(GL_FRAMEBUFFER, _frameBuffer);
    glViewport(0, 0, _frameBufferWidth, _frameBufferHeight);
    
    // bind shader program
    [self.effect prepareToDraw];
    
    // clear the screen
    glClear(GL_COLOR_BUFFER_BIT);
    glClearColor(0, 0, 0, 1.0);
    
    // setup vertices
    GLfloat vertices[] = {
        -0.5f, -0.5f, -1.0f, 0.0f, 0.5f, -1.0f, 0.5f, -0.5f, -1.0f,
    };
    
    // setup colors
    GLfloat colors[] = {
        0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f,
    };
    
    //draw triangle
    glEnableVertexAttribArray(GLKVertexAttribPosition);
    glEnableVertexAttribArray(GLKVertexAttribColor);
    glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FLOAT, 0, vertices);
    glVertexAttribPointer(GLKVertexAttribColor, 4, GL_FLOAT, GL_FLOAT, 0, colors);
    glDrawArrays(GL_TRIANGLES, 0, 3);
    
    // present render buffer
    glBindRenderbuffer(GL_RENDERBUFFER, _colorRenderbuffer);
    [self.glContext presentRenderbuffer:GL_RENDERBUFFER];
}
  • 運(yùn)行結(jié)果如下
用OpenGL渲染的CAEAGLLayer圖層.png

在一個(gè)真正的OpenGL應(yīng)用中虏等,我們可能會(huì)用NSTimerCADisplayLink周期性地每秒鐘調(diào)用-drawRrame方法60次,同時(shí)會(huì)將幾何圖形生成和繪制分開以便不會(huì)每次都重新生成三角形的頂點(diǎn)(這樣也可以讓我們繪制其他的一些東西而不是一個(gè)三角形而已)适肠,不過上面這個(gè)例子已經(jīng)足夠演示了繪圖原則了博其。

十 AVPlayerLayer

最后一個(gè)圖層類型是AVPlayerLayer。盡管它不是Core Animation框架的一部分(AV前綴看上去像)迂猴,AVPlayerLayer是有別的框架(AVFoundation)提供的慕淡,它和Core Animation緊密地結(jié)合在一起,提供了一個(gè)CALayer子類來顯示自定義的內(nèi)容類型沸毁。

AVPlayerLayer是用來在iOS上播放視頻的峰髓。他是高級(jí)接口例如MPMoivePlayer的底層實(shí)現(xiàn)傻寂,提供了顯示視頻的底層控制。AVPlayerLayer的使用相當(dāng)簡(jiǎn)單:你可以用+playerLayerWithPlayer:方法創(chuàng)建一個(gè)已經(jīng)綁定了視頻播放器的圖層携兵,或者你可以先創(chuàng)建一個(gè)圖層疾掰,然后用player屬性綁定一個(gè)AVPlayer實(shí)例。

/// 十 AVPlayerLayer
- (void)createAVPlayerLayer {
    // get video url
    NSURL *url = [[NSBundle mainBundle] URLForResource:@"video" withExtension:@"mp4"];
    
    // create player and player layer
    AVPlayer *player = [AVPlayer playerWithURL:url];
    AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:player];
    
    // set player layer frame and attach it to our view
    playerLayer.frame = self.contentView.bounds;
    [self.contentView.layer addSublayer:playerLayer];
    
    // play the video
    [player play];
  • 運(yùn)行結(jié)果如下
Sep-06-2019 09-32-36.gif

我們用代碼創(chuàng)建了一個(gè)AVPlayerLayer徐紧,但是我們?nèi)匀话阉砑拥搅艘粋€(gè)容器視圖中静檬,而不是直接在controller中的主視圖上添加。這樣其實(shí)是為了可以使用自動(dòng)布局限制使得圖層在最中間并级;否則拂檩,一旦設(shè)備被旋轉(zhuǎn)了我們就要手動(dòng)重新放置位置,因?yàn)镃ore Animation并不支持自動(dòng)大小和自動(dòng)布局嘲碧。

當(dāng)然稻励,因?yàn)锳VPlayerLayer是CALayer的子類,它繼承了父類的所有特性愈涩。我們并不會(huì)受限于要在一個(gè)矩形中播放視頻望抽;下面顯示了在3D,圓角履婉,有色邊框煤篙,蒙板,陰影等效果毁腿。

// 給視頻增加變換舰蟆,邊框和圓角
- (void)createMultiAVPlayerLayer {
    // get video url
    NSURL *url = [[NSBundle mainBundle] URLForResource:@"video" withExtension:@"mp4"];
    
    // create player and player layer
    AVPlayer *player = [AVPlayer playerWithURL:url];
    AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:player];
    
    // set player layer frame and attach it to our view
    playerLayer.frame = self.contentView.bounds;
    [self.contentView.layer addSublayer:playerLayer];
    
    // add transform layer
    CATransform3D transform = CATransform3DIdentity;
    transform.m34 = -1.0 / 500.0;
    transform = CATransform3DRotate(transform, M_PI_4, 1, 1, 0);
    playerLayer.transform = transform;
    
    // add rounded corners and border
    playerLayer.masksToBounds = YES;
    playerLayer.cornerRadius = 20.0;
    playerLayer.borderColor = [UIColor redColor].CGColor;
    playerLayer.borderWidth = 5.0;
    
    // play the video
    [player play];
}
  • 運(yùn)行效果如下
Sep-06-2019 09-34-08.gif
十一 總結(jié)

這一章我們簡(jiǎn)要概述了一些專用圖層以及用他們實(shí)現(xiàn)的一些效果,我們只是了解到這些圖層的皮毛狸棍,像CATiledLayerCAEMitterLayer這些類可以單獨(dú)寫一章的身害。但是,重點(diǎn)是記住CALayer是用處很大的草戈,而且它并沒有為所有可能的場(chǎng)景進(jìn)行優(yōu)化塌鸯。為了獲得Core Animation最好的性能,你需要為你的工作選對(duì)正確的工具唐片,希望你能夠挖掘這些不同的CALayer子類的功能丙猬。 這一章我們通過CAEmitterLayerAVPlayerLayer類簡(jiǎn)單地接觸到了一些動(dòng)畫。


本文摘自 iOS核心動(dòng)畫高級(jí)技巧 - 專用圖層


項(xiàng)目鏈接地址 - AnimationCA***Layer


?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末费韭,一起剝皮案震驚了整個(gè)濱河市歼争,隨后出現(xiàn)的幾起案子蕊程,更是在濱河造成了極大的恐慌桂躏,老刑警劉巖宋渔,帶你破解...
    沈念sama閱讀 218,122評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡揪垄,警方通過查閱死者的電腦和手機(jī)穷吮,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來饥努,“玉大人捡鱼,你說我怎么就攤上這事】崂ⅲ” “怎么了驾诈?”我有些...
    開封第一講書人閱讀 164,491評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長溶浴。 經(jīng)常有香客問我乍迄,道長,這世上最難降的妖魔是什么戳葵? 我笑而不...
    開封第一講書人閱讀 58,636評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮汉匙,結(jié)果婚禮上拱烁,老公的妹妹穿的比我還像新娘。我一直安慰自己噩翠,他們只是感情好戏自,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著伤锚,像睡著了一般擅笔。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上屯援,一...
    開封第一講書人閱讀 51,541評(píng)論 1 305
  • 那天猛们,我揣著相機(jī)與錄音,去河邊找鬼狞洋。 笑死弯淘,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的吉懊。 我是一名探鬼主播庐橙,決...
    沈念sama閱讀 40,292評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼借嗽!你這毒婦竟也來了态鳖?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,211評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤恶导,失蹤者是張志新(化名)和其女友劉穎浆竭,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,655評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡兆蕉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評(píng)論 3 336
  • 正文 我和宋清朗相戀三年羽戒,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片虎韵。...
    茶點(diǎn)故事閱讀 39,965評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡易稠,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出包蓝,到底是詐尸還是另有隱情驶社,我是刑警寧澤,帶...
    沈念sama閱讀 35,684評(píng)論 5 347
  • 正文 年R本政府宣布测萎,位于F島的核電站亡电,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏硅瞧。R本人自食惡果不足惜份乒,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評(píng)論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望腕唧。 院中可真熱鬧或辖,春花似錦、人聲如沸枣接。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽但惶。三九已至耳鸯,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間膀曾,已是汗流浹背县爬。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留添谊,地道東北人捌省。 一個(gè)月前我還...
    沈念sama閱讀 48,126評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像碉钠,于是被迫代替她去往敵國和親纲缓。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評(píng)論 2 355

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