[iOS] 核心高級(jí)動(dòng)畫技巧 — Part3

隱性動(dòng)畫

Core Animation基于一個(gè)假設(shè)桑腮,說屏幕上的任何東西都可以(或者可能)做動(dòng)畫。動(dòng)畫并不需要你在Core Animation中手動(dòng)打開咖楣,相反需要明確地關(guān)閉瑟匆,否則他會(huì)一直存在甸箱。

當(dāng)你改變CALayer 的一個(gè)可做動(dòng)畫的屬性育叁,它并不能立刻在屏幕上體現(xiàn)出來。相反芍殖,它是從先前的值平滑過渡到新的值豪嗽。這一切都是默認(rèn)的行為,你不需要做額外的操作豌骏。

  • 但是如果是UIView你改變一個(gè)屬性龟梦,或者改變UIView自帶的layer,它會(huì)直接跳變過去的哦窃躲。

這其實(shí)就是所謂的隱式動(dòng)畫计贰。之所以叫隱式是因?yàn)槲覀儾]有指定任何動(dòng)畫的類型。我們僅僅改變了一個(gè)屬性蒂窒,然后Core Animation來決定如何并且何時(shí)去做動(dòng)畫躁倒。

但當(dāng)你改變一個(gè)屬性,Core Animation是如何判斷動(dòng)畫類型和持續(xù)時(shí)間的呢洒琢?實(shí)際上動(dòng)畫執(zhí)行的時(shí)間取決于當(dāng)前事務(wù)的設(shè)置秧秉,動(dòng)畫類型取決于圖層行為


Transaction

事務(wù)實(shí)際上是Core Animation用來包含一系列屬性動(dòng)畫集合的機(jī)制衰抑,任何用指定事務(wù)去改變可以做動(dòng)畫的圖層屬性都不會(huì)立刻發(fā)生變化象迎,而是當(dāng)事務(wù)一旦提交的時(shí)候開始用一個(gè)動(dòng)畫過渡到新值。

事務(wù)是通過CATransaction類來做管理停士,這個(gè)類的設(shè)計(jì)有些奇怪挖帘,不像你從它的命名預(yù)期的那樣去管理一個(gè)簡(jiǎn)單的事務(wù),而是管理了一疊你不能訪問的事務(wù)恋技。CATransaction沒有屬性或者實(shí)例方法拇舀,并且也不能用+alloc和-init方法創(chuàng)建它。但是可以用+begin和+commit分別來入楎叩祝或者出棧骄崩。

任何可以做動(dòng)畫的圖層屬性都會(huì)被添加到棧頂?shù)氖聞?wù),你可以通過+setAnimationDuration:方法設(shè)置當(dāng)前事務(wù)的動(dòng)畫時(shí)間薄辅,或者通過+animationDuration方法來獲取值(默認(rèn)0.25秒)要拂。

Core Animation在每個(gè)run loop周期中自動(dòng)開始一次新的事務(wù),即使你不顯式的用[CATransaction begin]開始一次事務(wù)站楚,任何在一次run loop循環(huán)中屬性的改變都會(huì)被集中起來脱惰,然后做一次0.25秒的動(dòng)畫。

修改當(dāng)前事務(wù)的時(shí)間可能會(huì)導(dǎo)致同一時(shí)刻別的動(dòng)畫(如屏幕旋轉(zhuǎn))窿春,所以最好還是在調(diào)整動(dòng)畫之前壓入一個(gè)新的事務(wù)拉一。

[CATransaction begin];
//set the animation duration to 1 second
[CATransaction setAnimationDuration:1.0];
//randomize the layer background color
CGFloat red = arc4random() / (CGFloat)INT_MAX;
CGFloat green = arc4random() / (CGFloat)INT_MAX;
CGFloat blue = arc4random() / (CGFloat)INT_MAX;
self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;
//commit the transaction
[CATransaction commit];

如果你用過UIView的動(dòng)畫方法做過一些動(dòng)畫效果采盒,那么應(yīng)該對(duì)這個(gè)模式不陌生。UIView有兩個(gè)方法蔚润,+beginAnimations:context:+commitAnimations磅氨,和CATransaction的+begin+commit方法類似。實(shí)際上在+beginAnimations:context:和+commitAnimations之間所有視圖或者圖層屬性的改變而做的動(dòng)畫都是由于設(shè)置了CATransaction的原因嫡纠。

在iOS4中烦租,蘋果對(duì)UIView添加了一種基于block的動(dòng)畫方法:+animateWithDuration:animations:。這樣寫對(duì)做一堆的屬性動(dòng)畫在語法上會(huì)更加簡(jiǎn)單除盏,但實(shí)質(zhì)上它們都是在做同樣的事情叉橱。

CATransaction的+begin和+commit方法在+animateWithDuration:animations:內(nèi)部自動(dòng)調(diào)用,這樣block中所有屬性的改變都會(huì)被事務(wù)所包含痴颊。這樣也可以避免開發(fā)者由于對(duì)+begin和+commit匹配的失誤造成的風(fēng)險(xiǎn)赏迟。

完成塊

基于UIView的block的動(dòng)畫允許你在動(dòng)畫結(jié)束的時(shí)候提供一個(gè)完成的動(dòng)作。CATransaction接口提供的+setCompletionBlock:方法也有同樣的功能蠢棱。

如果在completion block里面改變layer的屬性锌杀,例如讓它旋轉(zhuǎn),你會(huì)發(fā)現(xiàn)它的速度和你在外邊CATransaction設(shè)置的不一樣泻仙,因?yàn)閎lock是在當(dāng)前事務(wù)提交并出棧之后才被執(zhí)行糕再,于是,block內(nèi)的transaction用默認(rèn)的事務(wù)做變換玉转,默認(rèn)的時(shí)間也就變成了0.25秒突想。


圖層行為

我們知道Core Animation通常對(duì)CALayer的所有屬性(可動(dòng)畫的屬性)做動(dòng)畫,但是UIView把它關(guān)聯(lián)的圖層的這個(gè)特性關(guān)閉了究抓。為了更好說明這一點(diǎn)猾担,我們需要知道隱式動(dòng)畫是如何實(shí)現(xiàn)的。

我們把改變屬性時(shí)CALayer自動(dòng)應(yīng)用的動(dòng)畫稱作Action刺下,當(dāng)CALayer的屬性被修改時(shí)候绑嘹,它會(huì)調(diào)用-actionForKey:方法,傳遞屬性的名稱橘茉。剩下的操作都在CALayer的頭文件中有詳細(xì)的說明工腋,實(shí)質(zhì)上是如下幾步:

  • 圖層首先檢測(cè)它是否有委托,并且是否實(shí)現(xiàn)CALayerDelegate協(xié)議指定的-actionForLayer:forKey方法畅卓。如果有擅腰,直接調(diào)用并返回結(jié)果。
  • 如果沒有委托翁潘,或者委托沒有實(shí)現(xiàn)-actionForLayer:forKey方法趁冈,圖層接著檢查包含屬性名稱對(duì)應(yīng)行為映射的actions字典。
  • 如果actions字典沒有包含對(duì)應(yīng)的屬性拜马,那么圖層接著在它的style字典接著搜索屬性名渗勘。
  • 最后矾飞,如果在style里面也找不到對(duì)應(yīng)的行為,那么圖層將會(huì)直接調(diào)用定義了每個(gè)屬性的標(biāo)準(zhǔn)行為的-defaultActionForKey:方法呀邢。

所以一輪完整的搜索結(jié)束之后,-actionForKey:要么返回空(這種情況下將不會(huì)有動(dòng)畫發(fā)生)豹绪,要么是CAAction協(xié)議對(duì)應(yīng)的對(duì)象价淌,最后CALayer拿這個(gè)結(jié)果去對(duì)先前和當(dāng)前的值做動(dòng)畫。

于是這就解釋了UIKit是如何禁用隱式動(dòng)畫的:每個(gè)UIView對(duì)它關(guān)聯(lián)的圖層都扮演了一個(gè)delegate瞒津,并且提供了-actionForLayer:forKey的實(shí)現(xiàn)方法蝉衣。當(dāng)不在一個(gè)動(dòng)畫塊的實(shí)現(xiàn)中,UIView對(duì)所有圖層行為返回nil巷蚪,但是在動(dòng)畫block范圍之內(nèi)病毡,它就返回了一個(gè)CABasicAnimation。

NSLog(@"Outside: %@", [self.layerView actionForLayer:self.layerView.layer forKey:@"backgroundColor"]);
//begin animation block
[UIView beginAnimations:nil context:nil];
//test layer action when inside of animation block
NSLog(@"Inside: %@", [self.layerView actionForLayer:self.layerView.layer forKey:@"backgroundColor"]);
//end animation block
[UIView commitAnimations];

輸出:
$ LayerTest[21215:c07] Outside: <null>
$ LayerTest[21215:c07] Inside: <CABasicAnimation: 0x757f090>

返回nil并不是禁用隱式動(dòng)畫唯一的辦法屁柏,CATransacition有個(gè)方法叫做+setDisableActions:啦膜,可以用來對(duì)所有屬性打開或者關(guān)閉隱式動(dòng)畫:[CATransaction setDisableActions:YES]

如果你想改變默認(rèn)的動(dòng)畫效果淌喻,可以通過修改action字典或者layer的delegate實(shí)現(xiàn)僧家,例如:

// 實(shí)現(xiàn)顏色變化時(shí)從左向右推進(jìn)
self.colorLayer = [CALayer layer];
self.colorLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f);
self.colorLayer.backgroundColor = [UIColor blueColor].CGColor;
//add a custom action
CATransition *transition = [CATransition animation];
transition.type = kCATransitionPush;
transition.subtype = kCATransitionFromLeft;
self.colorLayer.actions = @{@"backgroundColor": transition};
//add it to our view
[self.layerView.layer addSublayer:self.colorLayer];

呈現(xiàn)與模型

CALayer是一個(gè)連接用戶界面(就是MVC中的view)虛構(gòu)的類,但是在界面本身這個(gè)場(chǎng)景下裸删,CALayer的行為更像是存儲(chǔ)了視圖如何顯示和動(dòng)畫的數(shù)據(jù)Model八拱。實(shí)際上,在蘋果自己的文檔中涯塔,圖層樹通常都是值的圖層樹Model肌稻。

每個(gè)圖層屬性的顯示值都被存儲(chǔ)在一個(gè)叫做呈現(xiàn)圖層的獨(dú)立圖層當(dāng)中,他可以通過-presentationLayer方法來訪問匕荸。這個(gè)呈現(xiàn)圖層實(shí)際上是模型圖層的復(fù)制爹谭,但是它的屬性值代表了在任何指定時(shí)刻當(dāng)前外觀效果。換句話說每聪,你可以通過呈現(xiàn)圖層的值來獲取當(dāng)前屏幕上真正顯示出來的值旦棉。

注意呈現(xiàn)圖層僅僅當(dāng)圖層首次被提交(就是首次第一次在屏幕上顯示)的時(shí)候創(chuàng)建,所以在那之前調(diào)用-presentationLayer將會(huì)返回nil药薯。

在呈現(xiàn)圖層上調(diào)用–modelLayer將會(huì)返回它正在呈現(xiàn)所依賴的CALayer绑洛。通常在一個(gè)圖層上調(diào)用-modelLayer會(huì)返回–self(實(shí)際上我們已經(jīng)創(chuàng)建的原始圖層就是一種數(shù)據(jù)模型)。


顯示動(dòng)畫

屬性動(dòng)畫

如果有多個(gè)動(dòng)畫童本,并且我們實(shí)現(xiàn)了CAAnimationDelegate真屯,那么在回調(diào)函數(shù)- (void)animationDidStop:(CABasicAnimation *)anim finished:(BOOL)flag中我們是不能判斷是哪個(gè)動(dòng)畫觸發(fā)了,如果我們通過將anim保存到字典里進(jìn)行比對(duì)也是不ok的穷娱,因?yàn)槠鋵?shí)delegate傳回來的anim是原始創(chuàng)建時(shí)的深拷貝绑蔫,不是那個(gè)對(duì)象本身运沦。

當(dāng)使用 -addAnimation:forKey: 把動(dòng)畫添加到圖層,這里有一個(gè)到目前為止我們都設(shè)置為 nil 的 key 參數(shù)配深。這里的鍵是 -animationForKey: 方法找到對(duì)應(yīng)動(dòng)畫的唯一標(biāo)識(shí)符携添,而當(dāng)前動(dòng)畫的所有鍵都可以用 animationKeys 獲取。如果我們對(duì)每個(gè)動(dòng)畫都關(guān)聯(lián)一個(gè)唯一的鍵篓叶,就可以對(duì)每個(gè)圖層循環(huán)所有鍵烈掠,然后調(diào)用 -animationForKey: 來比對(duì)結(jié)果。

還有一種更加簡(jiǎn)單的方法缸托。像所有的NSObject子類一樣左敌,CAAnimation實(shí)現(xiàn)了KVC(鍵-值-編碼)協(xié)議,于是你可以用-setValue:forKey:和-valueForKey:方法來存取屬性俐镐。但是CAAnimation有一個(gè)不同的性能:它更像一個(gè)NSDictionary矫限,可以讓你隨意設(shè)置鍵值對(duì),即使和你使用的動(dòng)畫類所聲明的屬性并不匹配佩抹。


關(guān)鍵幀動(dòng)畫
CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
animation.keyPath = @"backgroundColor";
animation.duration = 2.0;
animation.values = @[
                     (__bridge id)[UIColor blueColor].CGColor,
                     (__bridge id)[UIColor redColor].CGColor,
                     (__bridge id)[UIColor greenColor].CGColor,
                     (__bridge id)[UIColor blueColor].CGColor ];
//apply animation to layer
[self.colorLayer addAnimation:animation forKey:nil];

CAKeyframeAnimation并不能自動(dòng)把當(dāng)前值作為第一幀(就像CABasicAnimation那樣把fromValue設(shè)為nil)叼风。動(dòng)畫會(huì)在開始的時(shí)候突然跳轉(zhuǎn)到第一幀的值,然后在動(dòng)畫結(jié)束的時(shí)候突然恢復(fù)到原始的值棍苹。所以為了動(dòng)畫的平滑特性咬扇,我們需要開始和結(jié)束的關(guān)鍵幀來匹配當(dāng)前屬性的值。

當(dāng)然可以創(chuàng)建一個(gè)結(jié)束和開始值不同的動(dòng)畫廊勃,那樣的話就需要在動(dòng)畫啟動(dòng)之前手動(dòng)更新屬性和最后一幀的值保持一致懈贺。

CAKeyframeAnimation還有另一種方式去指定動(dòng)畫,就是使用CGPath坡垫。path屬性可以用一種直觀的方式梭灿,使用Core Graphics函數(shù)定義運(yùn)動(dòng)的序列來繪制動(dòng)畫。

蘋果給CAKeyFrameAnimation添加了一個(gè)rotationMode的屬性冰悠。設(shè)置它為常量kCAAnimationRotateAuto堡妒,圖層將會(huì)根據(jù)曲線的切線自動(dòng)旋轉(zhuǎn)。


虛擬屬性

如果我們想要讓view旋轉(zhuǎn)就要改transform屬性:

CALayer *shipLayer = [CALayer layer];
shipLayer.frame = CGRectMake(0, 0, 128, 128);
shipLayer.position = CGPointMake(150, 150);
shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage;
[self.containerView.layer addSublayer:shipLayer];
//animate the ship rotation
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"transform";
animation.duration = 2.0;
animation.toValue = [NSValue valueWithCATransform3D: CATransform3DMakeRotation(M_PI, 0, 0, 1)];
[shipLayer addAnimation:animation forKey:nil];

但是這么做會(huì)有很多問題:(1)如果我們把旋轉(zhuǎn)的值從M_PI(180度)調(diào)整到2 * M_PI(360度)溉卓,然后運(yùn)行程序皮迟,會(huì)發(fā)現(xiàn)這時(shí)候飛船完全不動(dòng)了。這是因?yàn)檫@里的矩陣做了一次360度的旋轉(zhuǎn)桑寨,和做了0度是一樣的伏尼,所以最后的值根本沒變;(2)繼續(xù)使用M_PI尉尾,但這次用byValue而不是toValue爆阶。也許你會(huì)認(rèn)為這和設(shè)置toValue結(jié)果一樣,因?yàn)? + 90度 == 90度,但實(shí)際上飛船的圖片變大了辨图,并沒有做任何旋轉(zhuǎn)班套,這是因?yàn)樽儞Q矩陣不能像角度值那樣疊加。

有一個(gè)更好的解決方案:為了旋轉(zhuǎn)圖層故河,我們可以對(duì)transform.rotation關(guān)鍵路徑應(yīng)用動(dòng)畫吱韭,而不是transform本身

CALayer *shipLayer = [CALayer layer];
shipLayer.frame = CGRectMake(0, 0, 128, 128);
shipLayer.position = CGPointMake(150, 150);
shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage;
[self.containerView.layer addSublayer:shipLayer];
//animate the ship rotation
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"transform.rotation";
animation.duration = 2.0;
animation.byValue = @(M_PI * 2);
[shipLayer addAnimation:animation forKey:nil];

用transform.rotation而不是transform做動(dòng)畫的好處如下:

  1. 我們可以不通過關(guān)鍵幀一步旋轉(zhuǎn)多于180度的動(dòng)畫。
  2. 可以用相對(duì)值而不是絕對(duì)值旋轉(zhuǎn)(設(shè)置byValue而不是toValue)鱼的。
  3. 可以不用創(chuàng)建CATransform3D杉女,而是使用一個(gè)簡(jiǎn)單的數(shù)值來指定角度。
  4. 不會(huì)和transform.position或者transform.scale沖突(同樣是使用關(guān)鍵路徑來做獨(dú)立的動(dòng)畫屬性)鸳吸。

transform.rotation屬性有一個(gè)奇怪的問題是它其實(shí)并不存在。這是因?yàn)镃ATransform3D并不是一個(gè)對(duì)象速勇,它實(shí)際上是一個(gè)結(jié)構(gòu)體晌砾,也沒有符合KVC相關(guān)屬性,transform.rotation實(shí)際上是一個(gè)CALayer用于處理動(dòng)畫變換的虛擬屬性烦磁。

你不可以直接設(shè)置transform.rotation或者transform.scale养匈,他們不能被直接使用。當(dāng)你對(duì)他們做動(dòng)畫時(shí)都伪,Core Animation自動(dòng)地根據(jù)通過CAValueFunction來計(jì)算的值來更新transform屬性呕乎。

CAValueFunction用于把我們賦給虛擬的transform.rotation簡(jiǎn)單浮點(diǎn)值轉(zhuǎn)換成真正的用于擺放圖層的CATransform3D矩陣值。你可以通過設(shè)置CAPropertyAnimation的valueFunction屬性來改變陨晶,于是你設(shè)置的函數(shù)將會(huì)覆蓋默認(rèn)的函數(shù)猬仁。

CAValueFunction看起來似乎是對(duì)那些不能簡(jiǎn)單相加的屬性(例如變換矩陣)做動(dòng)畫的非常有用的機(jī)制,但由于CAValueFunction的實(shí)現(xiàn)細(xì)節(jié)是私有的先誉,所以目前不能通過繼承它來自定義湿刽。你可以通過使用蘋果目前已近提供的常量(目前都是和變換矩陣的虛擬屬性相關(guān),所以沒太多使用場(chǎng)景了褐耳,因?yàn)檫@些屬性都有了默認(rèn)的實(shí)現(xiàn)方式)诈闺。


動(dòng)畫組

CABasicAnimation和CAKeyframeAnimation僅僅作用于單獨(dú)的屬性,而CAAnimationGroup可以把這些動(dòng)畫組合在一起铃芦。CAAnimationGroup是另一個(gè)繼承于CAAnimation的子類雅镊,它添加了一個(gè)animations數(shù)組的屬性,用來組合別的動(dòng)畫刃滓。

我理解動(dòng)畫組的好處就是仁烹,像duration、fillMode之類的只要給group設(shè)置一次就可以啦咧虎,不用給每個(gè)animation加晃危,最后把group給layer加一次也就OK了,非常方便。


過渡

屬性動(dòng)畫只對(duì)圖層的可動(dòng)畫屬性起作用僚饭,所以如果要改變一個(gè)不能動(dòng)畫的屬性(比如圖片)震叮,或者從層級(jí)關(guān)系中添加或者移除圖層,屬性動(dòng)畫將不起作用鳍鸵。

過渡并不像屬性動(dòng)畫那樣平滑地在兩個(gè)值之間做動(dòng)畫苇瓣,而是影響到整個(gè)圖層的變化。過渡動(dòng)畫首先展示之前的圖層外觀偿乖,然后通過一個(gè)交換過渡到新的外觀击罪。

為了創(chuàng)建一個(gè)過渡動(dòng)畫,我們將使用CATransition贪薪,同樣是另一個(gè)CAAnimation的子類媳禁,和別的子類不同,CAAnimation有一個(gè)type和subtype來標(biāo)識(shí)變換效果画切。type屬性是一個(gè)NSString類型竣稽,可以被設(shè)置成如下類型:

kCATransitionFade (default)
kCATransitionMoveIn 
kCATransitionPush 
kCATransitionReveal
  • kCATransitionFade,當(dāng)你在改變圖層屬性之后霍弹,就創(chuàng)建了一個(gè)平滑的淡入淡出效果毫别。
  • kCATransitionPush創(chuàng)建了一個(gè)新的圖層,從邊緣的一側(cè)滑動(dòng)進(jìn)來典格,把舊圖層從另一側(cè)推出去的效果岛宦。
  • kCATransitionMoveIn和kCATransitionReveal與kCATransitionPush類似,都實(shí)現(xiàn)了一個(gè)定向滑動(dòng)的動(dòng)畫耍缴,但是有一些細(xì)微的不同砾肺,
  • kCATransitionMoveIn從頂部滑動(dòng)進(jìn)入,但不像推送動(dòng)畫那樣把老圖層推走防嗡。
  • kCATransitionReveal把原始的圖層滑動(dòng)出去來顯示新的外觀债沮,而不是把新的圖層滑動(dòng)進(jìn)入。

后面三種過渡類型都有一個(gè)默認(rèn)的動(dòng)畫方向本鸣,它們都從左側(cè)滑入疫衩,但是你可以通過subtype來控制它們的方向,提供了如下四種類型:

kCATransitionFromRight 
kCATransitionFromLeft 
kCATransitionFromTop 
kCATransitionFromBottom

例如通過CATransition讓image改變時(shí)淡入淡出:

CATransition *transition = [CATransition animation];
transition.type = kCATransitionFade;
//apply transition to imageview backing layer
[self.imageView.layer addAnimation:transition forKey:nil];
//cycle to next image
UIImage *currentImage = self.imageView.image;
NSUInteger index = [self.images indexOfObject:currentImage];
index = (index + 1) % [self.images count];
self.imageView.image = self.images[index];

你可以從代碼中看出荣德,過渡動(dòng)畫和之前的屬性動(dòng)畫或者動(dòng)畫組添加到圖層上的方式一致闷煤,都是通過-addAnimation:forKey:方法。但是和屬性動(dòng)畫不同的是涮瞻,對(duì)指定的圖層一次只能使用一次CATransition鲤拿,因此,無論你對(duì)動(dòng)畫的鍵設(shè)置什么值署咽,過渡動(dòng)畫都會(huì)對(duì)它的鍵設(shè)置成“transition”近顷,也就是常量kCATransition生音。

CATransition可以對(duì)圖層任何變化平滑過渡的事實(shí)使得它成為那些不好做動(dòng)畫的屬性圖層行為的理想候選。蘋果當(dāng)然意識(shí)到了這點(diǎn)窒升,并且當(dāng)設(shè)置了CALayer的content屬性的時(shí)候缀遍,CATransition的確是默認(rèn)的行為。但是對(duì)于UIView關(guān)聯(lián)的layer饱须,或者是其他隱式動(dòng)畫的行為域醇,這個(gè)特性依然是被禁用的(不會(huì)自動(dòng)加上transition),但是對(duì)于你自己創(chuàng)建的圖層蓉媳,這意味著對(duì)圖層contents圖片做的改動(dòng)都會(huì)自動(dòng)附上淡入淡出的動(dòng)畫譬挚。

CATransition并不作用于指定的圖層屬性,這就是說你可以在即使不能準(zhǔn)確得知改變了什么的情況下對(duì)圖層做動(dòng)畫酪呻,例如减宣,在不知道UITableView哪一行被添加或者刪除的情況下,直接就可以平滑地刷新它玩荠,或者在不知道UIViewController內(nèi)部的視圖層級(jí)的情況下對(duì)兩個(gè)不同的實(shí)例做過渡動(dòng)畫漆腌。

要確保CATransition添加到的圖層在過渡動(dòng)畫發(fā)生時(shí)不會(huì)在樹狀結(jié)構(gòu)中被移除,否則CATransition將會(huì)和圖層一起被移除姨蟋。一般來說,你只需要將動(dòng)畫添加到被影響圖層的superlayer立帖。

- (void)tabBarController:(UITabBarController *)tabBarController didSelectViewController:(UIViewController *)viewController
{
    //set up crossfade transition
    CATransition *transition = [CATransition animation];
    transition.type = kCATransitionFade;
    //apply transition to tab bar controller's view
    [self.tabBarController.view.layer addAnimation:transition forKey:nil];
}

自定義動(dòng)畫

CATransition是一種對(duì)那些不太好做平滑動(dòng)畫屬性的強(qiáng)大工具眼溶,但是CATransition的提供的動(dòng)畫類型太少了。

蘋果通過UIView +transitionFromView:toView:duration:options:completion:+transitionWithView:duration:options:animations:方法提供了Core Animation的過渡特性晓勇。但是這里的可用的過渡選項(xiàng)和CATransition的type屬性提供的常量完全不同堂飞。UIView過渡方法中options參數(shù)可以由如下常量指定:

UIViewAnimationOptionTransitionFlipFromLeft 
UIViewAnimationOptionTransitionFlipFromRight
UIViewAnimationOptionTransitionCurlUp 
UIViewAnimationOptionTransitionCurlDown
UIViewAnimationOptionTransitionCrossDissolve 
UIViewAnimationOptionTransitionFlipFromTop 
UIViewAnimationOptionTransitionFlipFromBottom

除了UIViewAnimationOptionTransitionCrossDissolve之外,剩下的值和CATransition類型完全沒關(guān)系绑咱。

CATransition基礎(chǔ)就是對(duì)原始的圖層外觀截圖绰筛,然后添加一段動(dòng)畫,平滑過渡到圖層改變之后那個(gè)截圖的效果描融。如果我們知道如何對(duì)圖層截圖铝噩,我們就可以使用屬性動(dòng)畫來代替CATransition或者是UIKit的過渡方法來實(shí)現(xiàn)動(dòng)畫。

事實(shí)證明窿克,對(duì)圖層做截圖還是很簡(jiǎn)單的骏庸。CALayer有一個(gè)-renderInContext:方法,可以通過把它繪制到Core Graphics的上下文中捕獲當(dāng)前內(nèi)容的圖片年叮,然后在另外的視圖中顯示出來具被。如果我們把這個(gè)截屏視圖置于原始視圖之上,就可以遮住真實(shí)視圖的所有變化只损,于是重新創(chuàng)建了一個(gè)簡(jiǎn)單的過渡效果一姿。

- (IBAction)performTransition
{
    //preserve the current view snapshot
    UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, YES, 0.0);
    [self.view.layer renderInContext:UIGraphicsGetCurrentContext()];
    UIImage *coverImage = UIGraphicsGetImageFromCurrentImageContext();
    //insert snapshot view in front of this one
    UIView *coverView = [[UIImageView alloc] initWithImage:coverImage];
    coverView.frame = self.view.bounds;
    [self.view addSubview:coverView];
    //update the view (we'll simply randomize the layer background color)
    CGFloat red = arc4random() / (CGFloat)INT_MAX;
    CGFloat green = arc4random() / (CGFloat)INT_MAX;
    CGFloat blue = arc4random() / (CGFloat)INT_MAX;
    self.view.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0];
    //perform animation (anything you like)
    [UIView animateWithDuration:1.0 animations:^{
        //scale, rotate and fade the view
        CGAffineTransform transform = CGAffineTransformMakeScale(0.01, 0.01);
        transform = CGAffineTransformRotate(transform, M_PI_2);
        coverView.transform = transform;
        coverView.alpha = 0.0;
    } completion:^(BOOL finished) {
        //remove the cover view now we're finished with it
        [coverView removeFromSuperview];
    }];
}

取消動(dòng)畫

為了終止一個(gè)指定的動(dòng)畫,你可以用如下方法把它從圖層移除掉:

- (void)removeAnimationForKey:(NSString *)key;

或者移除所有動(dòng)畫:

- (void)removeAllAnimations;

動(dòng)畫一旦被移除,圖層的外觀就立刻更新到當(dāng)前的model layer的值。一般說來,動(dòng)畫在結(jié)束之后被自動(dòng)移除俺叭,除非設(shè)置removedOnCompletion為NO纵菌,如果你設(shè)置動(dòng)畫在結(jié)束之后不被自動(dòng)移除,那么當(dāng)它不需要的時(shí)候你要手動(dòng)移除它颁褂;否則它會(huì)一直存在于內(nèi)存中,直到圖層被銷毀。


圖層時(shí)間

CAMediaTiming協(xié)議

CAMediaTiming協(xié)議定義了在一段動(dòng)畫內(nèi)用來控制逝去時(shí)間的屬性的集合遥诉,CALayer和CAAnimation都實(shí)現(xiàn)了這個(gè)協(xié)議,所以時(shí)間可以被任意基于一個(gè)圖層或者一段動(dòng)畫的類控制噪叙。

duration是一個(gè)CFTimeInterval的類型(類似于NSTimeInterval的一種雙精度浮點(diǎn)類型)矮锈,對(duì)將要進(jìn)行的動(dòng)畫的一次迭代指定了時(shí)間。

這里的一次迭代是什么意思呢睁蕾?CAMediaTiming另外還有一個(gè)屬性叫做repeatCount苞笨,代表動(dòng)畫重復(fù)的迭代次數(shù)。如果duration是2子眶,repeatCount設(shè)為3.5(三個(gè)半迭代)瀑凝,那么完整的動(dòng)畫時(shí)長(zhǎng)將是7秒。

duration和repeatCount默認(rèn)都是0臭杰。但這不意味著動(dòng)畫時(shí)長(zhǎng)為0秒粤咪,或者0次,這里的0僅僅代表了“默認(rèn)”渴杆,也就是0.25秒和1次寥枝。

autoreverses的屬性(BOOL類型)在每次間隔交替循環(huán)過程中自動(dòng)回放;repeatCount和repeatDuration可能會(huì)相互沖突磁奖,所以你只要對(duì)其中一個(gè)指定非零值囊拜。對(duì)兩個(gè)屬性都設(shè)置非0值的行為沒有被定義。


相對(duì)時(shí)間
  • beginTime指定了動(dòng)畫開始之前的的延遲時(shí)間比搭。這里的延遲從動(dòng)畫添加到可見圖層的那一刻開始測(cè)量冠跷,默認(rèn)是0(就是說動(dòng)畫會(huì)立刻執(zhí)行)。

  • speed是一個(gè)時(shí)間的倍數(shù)身诺,默認(rèn)1.0蔽莱,減少它會(huì)減慢圖層/動(dòng)畫的時(shí)間,增加它會(huì)加快速度戚长。如果2.0的速度盗冷,那么對(duì)于一個(gè)duration為1的動(dòng)畫,實(shí)際上在0.5秒的時(shí)候就已經(jīng)完成了同廉。

  • timeOffset和beginTime類似仪糖,但是和增加beginTime導(dǎo)致的延遲動(dòng)畫不同柑司,增加timeOffset只是讓動(dòng)畫快進(jìn)到某一點(diǎn),例如锅劝,對(duì)于一個(gè)持續(xù)1秒的動(dòng)畫來說攒驰,設(shè)置timeOffset為0.5意味著動(dòng)畫將從一半的地方開始。

和beginTime不同的是故爵,timeOffset并不受speed的影響玻粪。所以如果你把speed設(shè)為2.0,把timeOffset設(shè)置為0.5诬垂,那么你的動(dòng)畫將從動(dòng)畫最后結(jié)束的地方開始劲室,因?yàn)?秒的動(dòng)畫實(shí)際上被縮短到了0.5秒。然而即使使用了timeOffset讓動(dòng)畫從結(jié)束的地方開始结窘,它仍然播放了一個(gè)完整的時(shí)長(zhǎng)很洋,這個(gè)動(dòng)畫僅僅是循環(huán)了一圈,然后從頭開始播放隧枫。


fillMode

對(duì)于beginTime非0的一段動(dòng)畫來說喉磁,會(huì)出現(xiàn)一個(gè)當(dāng)動(dòng)畫添加到圖層上但什么也沒發(fā)生的狀態(tài)。類似的官脓,removeOnCompletion被設(shè)置為NO的動(dòng)畫將會(huì)在動(dòng)畫結(jié)束的時(shí)候仍然保持之前的狀態(tài)协怒。這就產(chǎn)生了一個(gè)問題,當(dāng)動(dòng)畫開始之前和動(dòng)畫結(jié)束之后卑笨,被設(shè)置動(dòng)畫的屬性將會(huì)是什么值呢孕暇?

這種行為就交給開發(fā)者了,它可以被CAMediaTiming的fillMode來控制湾趾。fillMode是一個(gè)NSString類型芭商,可以接受如下四種常量:

kCAFillModeForwards 
kCAFillModeBackwards 
kCAFillModeBoth 
kCAFillModeRemoved

默認(rèn)是kCAFillModeRemoved派草,當(dāng)動(dòng)畫不再播放的時(shí)候就顯示圖層模型指定的值搀缠。剩下的三種類型向前,向后或者即向前又向后去填充動(dòng)畫狀態(tài)近迁,使得動(dòng)畫在開始前或者結(jié)束后仍然保持開始和結(jié)束那一刻的值艺普。

但是當(dāng)用它來解決這個(gè)問題的時(shí)候,需要把removeOnCompletion設(shè)置為NO鉴竭,另外需要給動(dòng)畫添加一個(gè)非空的鍵歧譬,于是可以在不需要?jiǎng)赢嫷臅r(shí)候把它從圖層上移除。


層級(jí)

每個(gè)動(dòng)畫和圖層在時(shí)間上都有它自己的層級(jí)概念搏存,相對(duì)于它的父親來測(cè)量瑰步。對(duì)圖層調(diào)整時(shí)間將會(huì)影響到它本身和子圖層的動(dòng)畫,但不會(huì)影響到父圖層璧眠。另一個(gè)相似點(diǎn)是所有的動(dòng)畫都被按照層級(jí)組合(使用CAAnimationGroup實(shí)例)缩焦。

對(duì)CALayer或者CAAnimationGroup調(diào)整durationrepeatCount/repeatDuration屬性并不會(huì)影響到子動(dòng)畫读虏。但是beginTimetimeOffsetspeed屬性將會(huì)影響到子動(dòng)畫袁滥。然而在層級(jí)關(guān)系中盖桥,beginTime指定了父圖層開始動(dòng)畫(或者組合關(guān)系中的父動(dòng)畫)和對(duì)象將要開始自己動(dòng)畫之間的偏移。類似的题翻,調(diào)整CALayerCAGroupAnimationspeed屬性將會(huì)對(duì)動(dòng)畫以及子動(dòng)畫速度應(yīng)用一個(gè)縮放的因子揩徊。

全局時(shí)間

CoreAnimation有一個(gè)全局時(shí)間的概念,也就是所謂的馬赫時(shí)間(“馬赫”實(shí)際上是iOS和Mac OS系統(tǒng)內(nèi)核的命名)嵌赠。馬赫時(shí)間在設(shè)備上所有進(jìn)程都是全局的--但是在不同設(shè)備上并不是全局的--不過這已經(jīng)足夠?qū)?dòng)畫的參考點(diǎn)提供便利了塑荒,你可以使用CACurrentMediaTime函數(shù)來訪問馬赫時(shí)間:
(它返回了設(shè)備自從上次啟動(dòng)后的秒數(shù),并不是你所關(guān)心的)

CFTimeInterval time = CACurrentMediaTime();

注意當(dāng)設(shè)備休眠的時(shí)候馬赫時(shí)間會(huì)暫停猾普,也就是所有的CAAnimations(基于馬赫時(shí)間)同樣也會(huì)暫停袜炕。

每個(gè)CALayerCAAnimation實(shí)例都有自己本地時(shí)間的概念,是根據(jù)父圖層/動(dòng)畫層級(jí)關(guān)系中的beginTime初家,timeOffsetspeed屬性計(jì)算偎窘。就和轉(zhuǎn)換不同圖層之間坐標(biāo)關(guān)系一樣,CALayer同樣也提供了方法來轉(zhuǎn)換不同圖層之間的本地時(shí)間溜在。如下:

- (CFTimeInterval)convertTime:(CFTimeInterval)t fromLayer:(CALayer *)l;
- (CFTimeInterval)convertTime:(CFTimeInterval)t toLayer:(CALayer *)l;

當(dāng)用來同步不同圖層之間有不同的speed陌知,timeOffsetbeginTime的動(dòng)畫,這些方法會(huì)很有用掖肋。


暫停仆葡、倒退、快進(jìn)

設(shè)置動(dòng)畫的speed屬性為0可以暫停動(dòng)畫志笼,但在動(dòng)畫被添加到圖層之后不太可能再修改它了沿盅,所以不能對(duì)正在進(jìn)行的動(dòng)畫使用這個(gè)屬性。給圖層添加一個(gè)CAAnimation實(shí)際上是給動(dòng)畫對(duì)象做了一個(gè)不可改變的拷貝纫溃,所以對(duì)原始動(dòng)畫對(duì)象屬性的改變對(duì)真實(shí)的動(dòng)畫并沒有作用腰涧。相反,直接用-animationForKey:來檢索圖層正在進(jìn)行的動(dòng)畫可以返回正確的動(dòng)畫對(duì)象紊浩,但是修改它的屬性將會(huì)拋出異常窖铡。

如果移除圖層正在進(jìn)行的動(dòng)畫,圖層將會(huì)急速返回動(dòng)畫之前的狀態(tài)坊谁。但如果在動(dòng)畫移除之前拷貝呈現(xiàn)圖層到模型圖層费彼,動(dòng)畫將會(huì)看起來暫停在那里。但是不好的地方在于之后就不能再恢復(fù)動(dòng)畫了口芍。

一個(gè)簡(jiǎn)單的方法是可以利用CAMediaTiming來暫停圖層本身箍铲。如果把圖層的speed設(shè)置成0,它會(huì)暫停任何添加到圖層上的動(dòng)畫鬓椭。類似的颠猴,設(shè)置speed大于1.0將會(huì)快進(jìn)聋庵,設(shè)置成一個(gè)負(fù)值將會(huì)倒回動(dòng)畫。

self.window.layer.speed = 100;

手動(dòng)控制動(dòng)畫進(jìn)程

timeOffset一個(gè)很有用的功能在于它可以讓你手動(dòng)控制動(dòng)畫進(jìn)程芙粱,通過設(shè)置speed為0祭玉,可以禁用動(dòng)畫的自動(dòng)播放,然后來使用timeOffset來來回顯示動(dòng)畫序列春畔。

因?yàn)樵趧?dòng)畫添加到圖層之后不能再做修改了脱货,我們來通過調(diào)整layertimeOffset達(dá)到同樣的效果。

也許相對(duì)于設(shè)置個(gè)動(dòng)畫然后每次顯示一幀而言律姨,用移動(dòng)手勢(shì)來直接設(shè)置門的transform會(huì)更簡(jiǎn)單振峻。

在這個(gè)例子中的確是這樣,但是對(duì)于比如說關(guān)鍵這這樣更加復(fù)雜的情況择份,或者有多個(gè)圖層的動(dòng)畫組扣孟,相對(duì)于實(shí)時(shí)計(jì)算每個(gè)圖層的屬性而言,這就顯得方便的多了荣赶。


緩沖

CAMediaTimingFunction

那么該如何使用緩沖方程式呢凤价?首先需要設(shè)置CAAnimation的timingFunction屬性,是CAMediaTimingFunction類的一個(gè)對(duì)象拔创。如果想改變隱式動(dòng)畫的計(jì)時(shí)函數(shù)利诺,同樣也可以使用CATransaction的+setAnimationTimingFunction:方法。

這里有一些方式來創(chuàng)建CAMediaTimingFunction剩燥,最簡(jiǎn)單的方式是調(diào)用+timingFunctionWithName:的構(gòu)造方法慢逾。這里傳入如下幾個(gè)常量之一:

kCAMediaTimingFunctionLinear 
kCAMediaTimingFunctionEaseIn 
kCAMediaTimingFunctionEaseOut 
kCAMediaTimingFunctionEaseInEaseOut
kCAMediaTimingFunctionDefault
  • kCAMediaTimingFunctionLinear選項(xiàng)創(chuàng)建了一個(gè)線性的計(jì)時(shí)函數(shù)。
  • kCAMediaTimingFunctionEaseIn常量創(chuàng)建了一個(gè)慢慢加速然后突然停止的方法灭红。對(duì)于之前提到的自由落體的例子來說很適合侣滩,或者比如對(duì)準(zhǔn)一個(gè)目標(biāo)的導(dǎo)彈的發(fā)射。
  • kCAMediaTimingFunctionEaseOut則恰恰相反变擒,它以一個(gè)全速開始君珠,然后慢慢減速停止。它有一個(gè)削弱的效果赁项,應(yīng)用的場(chǎng)景比如一扇門慢慢地關(guān)上葛躏,而不是砰地一聲澈段。
  • kCAMediaTimingFunctionEaseInEaseOut創(chuàng)建了一個(gè)慢慢加速然后再慢慢減速的過程悠菜。這是現(xiàn)實(shí)世界大多數(shù)物體移動(dòng)的方式,也是大多數(shù)動(dòng)畫來說最好的選擇败富。當(dāng)使用UIView的動(dòng)畫方法時(shí)悔醋,他的確是默認(rèn)的,但當(dāng)創(chuàng)建CAAnimation的時(shí)候兽叮,就需要手動(dòng)設(shè)置它了芬骄。
  • kCAMediaTimingFunctionDefault猾愿,它和kCAMediaTimingFunctionEaseInEaseOut很類似,但是加速和減速的過程都稍微有些慢账阻,隱式動(dòng)畫作為默認(rèn)效果蒂秘。雖然它的名字說是默認(rèn)的,但還是要記住當(dāng)創(chuàng)建顯式的CAAnimation它并不是默認(rèn)選項(xiàng)(換句話說淘太,默認(rèn)的圖層行為動(dòng)畫用kCAMediaTimingFunctionDefault作為它們的計(jì)時(shí)方法)姻僧。

UIKit的動(dòng)畫也同樣支持這些緩沖方法的使用,盡管語法和常量有些不同蒲牧,為了改變UIView動(dòng)畫的緩沖選項(xiàng)撇贺,給options參數(shù)添加如下常量之一:

UIViewAnimationOptionCurveEaseInOut 
UIViewAnimationOptionCurveEaseIn 
UIViewAnimationOptionCurveEaseOut 
UIViewAnimationOptionCurveLinear

CAKeyframeAnimation有一個(gè)NSArray類型的timingFunctions屬性,我們可以用它來對(duì)每次動(dòng)畫的步驟指定不同的計(jì)時(shí)函數(shù)冰抢。但是指定函數(shù)的個(gè)數(shù)一定要等于keyframes數(shù)組的元素個(gè)數(shù)減一松嘶,因?yàn)樗敲枋雒恳粠g動(dòng)畫速度的函數(shù)。


自定義緩沖函數(shù)

除了+functionWithName:之外挎扰,CAMediaTimingFunction同樣有另一個(gè)構(gòu)造函數(shù)翠订,一個(gè)有四個(gè)浮點(diǎn)參數(shù)的+functionWithControlPoints::::(注意這里奇怪的語法,并沒有包含具體每個(gè)參數(shù)的名稱遵倦,這在objective-C中是合法的蕴轨,但是卻違反了蘋果對(duì)方法命名的指導(dǎo)方針,而且看起來是一個(gè)奇怪的設(shè)計(jì))骇吭。

使用這個(gè)方法橙弱,我們可以創(chuàng)建一個(gè)自定義的緩沖函數(shù),來匹配我們的時(shí)鐘動(dòng)畫燥狰,為了理解如何使用這個(gè)方法棘脐,我們要了解一些CAMediaTimingFunction是如何工作的。

一個(gè)三次貝塞爾曲線通過四個(gè)點(diǎn)來定義龙致,第一個(gè)和最后一個(gè)點(diǎn)代表了曲線的起點(diǎn)和終點(diǎn)蛀缝,剩下中間兩個(gè)點(diǎn)叫做控制點(diǎn),

三次貝塞爾

CAMediaTimingFunction有一個(gè)叫做-getControlPointAtIndex:values:的方法目代,可以用來檢索曲線的控制點(diǎn)屈梁,用這個(gè)方法可以找到系統(tǒng)提供的曲線的參數(shù)。


復(fù)雜動(dòng)畫(反彈)

考慮一個(gè)橡膠球掉落到堅(jiān)硬的地面的場(chǎng)景榛了,當(dāng)開始下落的時(shí)候在讶,它會(huì)持續(xù)加速知道落到地面,然后經(jīng)過幾次反彈霜大,最后停下來构哺。如果用一張圖來說明:

反彈

這種效果沒法用一個(gè)簡(jiǎn)單的三次貝塞爾曲線表示,于是不能用CAMediaTimingFunction來完成。但如果想要實(shí)現(xiàn)這樣的效果曙强,可以用如下幾種方法:

  • 用CAKeyframeAnimation創(chuàng)建一個(gè)動(dòng)畫残拐,然后分割成幾個(gè)步驟,每個(gè)小步驟使用自己的計(jì)時(shí)函數(shù)碟嘴。
  • 使用定時(shí)器逐幀更新實(shí)現(xiàn)動(dòng)畫溪食。

萬能動(dòng)畫思路

無論動(dòng)畫多么復(fù)雜,其實(shí)都可以拆成duration(秒數(shù))*幀數(shù)60的keyframe動(dòng)畫娜扇,也就是保證每幀的位置是對(duì)的眠菇,而兩幀之間可以是線性的動(dòng)畫,位置可以通過插值器計(jì)算出來:

- (void)animate
{
    //reset ball to top of screen
    self.ballView.center = CGPointMake(150, 32);
    //set up animation parameters
    NSValue *fromValue = [NSValue valueWithCGPoint:CGPointMake(150, 32)];
    NSValue *toValue = [NSValue valueWithCGPoint:CGPointMake(150, 268)];
    CFTimeInterval duration = 1.0;
    //generate keyframes
    NSInteger numFrames = duration * 60;
    NSMutableArray *frames = [NSMutableArray array];
    for (int i = 0; i < numFrames; i++) {
        float time = 1/(float)numFrames * i;
        //apply easing
        time = bounceEaseOut(time);
        //add keyframe
        [frames addObject:[self interpolateFromValue:fromValue toValue:toValue time:time]];
    }
    //create keyframe animation
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
    animation.keyPath = @"position";
    animation.duration = 1.0;
    animation.delegate = self;
    animation.values = frames;
    //apply animation
    [self.ballView.layer addAnimation:animation forKey:nil];
}

float bounceEaseOut(float t)
{
    if (t < 4/11.0) {
        return (121 * t * t)/16.0;
    } else if (t < 8/11.0) {
        return (363/40.0 * t * t) - (99/10.0 * t) + 17/5.0;
    } else if (t < 9/10.0) {
        return (4356/361.0 * t * t) - (35442/1805.0 * t) + 16061/1805.0;
    }
    return (54/5.0 * t * t) - (513/25.0 * t) + 268/25.0;
}

float interpolate(float from, float to, float time)
{
    return (to - from) * time + from;
}

- (id)interpolateFromValue:(id)fromValue toValue:(id)toValue time:(float)time
{
    if ([fromValue isKindOfClass:[NSValue class]]) {
        //get type
        const char *type = [fromValue objCType];
        if (strcmp(type, @encode(CGPoint)) == 0) {
            CGPoint from = [fromValue CGPointValue];
            CGPoint to = [toValue CGPointValue];
            CGPoint result = CGPointMake(interpolate(from.x, to.x, time), interpolate(from.y, to.y, time));
            return [NSValue valueWithCGPoint:result];
        }
    }
    //provide safe default implementation
    return (time < 0.5)? fromValue: toValue;
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末袱衷,一起剝皮案震驚了整個(gè)濱河市捎废,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌致燥,老刑警劉巖登疗,帶你破解...
    沈念sama閱讀 218,451評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異嫌蚤,居然都是意外死亡辐益,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門脱吱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來智政,“玉大人,你說我怎么就攤上這事箱蝠⌒妫” “怎么了?”我有些...
    開封第一講書人閱讀 164,782評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵宦搬,是天一觀的道長(zhǎng)牙瓢。 經(jīng)常有香客問我,道長(zhǎng)间校,這世上最難降的妖魔是什么矾克? 我笑而不...
    開封第一講書人閱讀 58,709評(píng)論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮憔足,結(jié)果婚禮上胁附,老公的妹妹穿的比我還像新娘。我一直安慰自己滓彰,他們只是感情好控妻,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,733評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著找蜜,像睡著了一般饼暑。 火紅的嫁衣襯著肌膚如雪稳析。 梳的紋絲不亂的頭發(fā)上洗做,一...
    開封第一講書人閱讀 51,578評(píng)論 1 305
  • 那天弓叛,我揣著相機(jī)與錄音,去河邊找鬼诚纸。 笑死撰筷,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的畦徘。 我是一名探鬼主播毕籽,決...
    沈念sama閱讀 40,320評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼井辆!你這毒婦竟也來了关筒?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,241評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤杯缺,失蹤者是張志新(化名)和其女友劉穎蒸播,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體萍肆,經(jīng)...
    沈念sama閱讀 45,686評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡袍榆,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,878評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了塘揣。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片包雀。...
    茶點(diǎn)故事閱讀 39,992評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖亲铡,靈堂內(nèi)的尸體忽然破棺而出才写,到底是詐尸還是另有隱情,我是刑警寧澤奖蔓,帶...
    沈念sama閱讀 35,715評(píng)論 5 346
  • 正文 年R本政府宣布琅摩,位于F島的核電站,受9級(jí)特大地震影響锭硼,放射性物質(zhì)發(fā)生泄漏房资。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,336評(píng)論 3 330
  • 文/蒙蒙 一檀头、第九天 我趴在偏房一處隱蔽的房頂上張望轰异。 院中可真熱鬧,春花似錦暑始、人聲如沸搭独。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,912評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽牙肝。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間配椭,已是汗流浹背虫溜。 一陣腳步聲響...
    開封第一講書人閱讀 33,040評(píng)論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留股缸,地道東北人衡楞。 一個(gè)月前我還...
    沈念sama閱讀 48,173評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像敦姻,于是被迫代替她去往敵國和親瘾境。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,947評(píng)論 2 355

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