鍍金池/ 教程/ iOS/ View Controller 轉(zhuǎn)場
與四軸無人機(jī)的通訊
在沙盒中編寫腳本
結(jié)構(gòu)體和值類型
深入理解 CocoaPods
UICollectionView + UIKit 力學(xué)
NSString 與 Unicode
代碼簽名探析
測試
架構(gòu)
第二期-并發(fā)編程
Metal
自定義控件
iOS 中的行為
行為驅(qū)動(dòng)開發(fā)
Collection View 動(dòng)畫
截圖測試
MVVM 介紹
使 Mac 應(yīng)用數(shù)據(jù)腳本化
一個(gè)完整的 Core Data 應(yīng)用
插件
字符串
為 iOS 建立 Travis CI
先進(jìn)的自動(dòng)布局工具箱
動(dòng)畫
為 iOS 7 重新設(shè)計(jì) App
XPC
從 NSURLConnection 到 NSURLSession
Core Data 網(wǎng)絡(luò)應(yīng)用實(shí)例
GPU 加速下的圖像處理
自定義 Core Data 遷移
子類
與調(diào)試器共舞 - LLDB 的華爾茲
圖片格式
并發(fā)編程:API 及挑戰(zhàn)
IP,TCP 和 HTTP
動(dòng)畫解釋
響應(yīng)式 Android 應(yīng)用
初識(shí) TextKit
客戶端
View-Layer 協(xié)作
回到 Mac
Android
Core Image 介紹
自定義 Formatters
Scene Kit
調(diào)試
項(xiàng)目介紹
Swift 的強(qiáng)大之處
測試并發(fā)程序
Android 通知中心
調(diào)試:案例學(xué)習(xí)
從 UIKit 到 AppKit
iOS 7 : 隱藏技巧和變通之道
安全
底層并發(fā) API
消息傳遞機(jī)制
更輕量的 View Controllers
用 SQLite 和 FMDB 替代 Core Data
字符串解析
終身學(xué)習(xí)的一代人
視頻
Playground 快速原型制作
Omni 內(nèi)部
同步數(shù)據(jù)
設(shè)計(jì)優(yōu)雅的移動(dòng)游戲
繪制像素到屏幕上
相機(jī)與照片
音頻 API 一覽
交互式動(dòng)畫
常見的后臺(tái)實(shí)踐
糟糕的測試
避免濫用單例
數(shù)據(jù)模型和模型對(duì)象
Core Data
字符串本地化
View Controller 轉(zhuǎn)場
照片框架
響應(yīng)式視圖
Square Register 中的擴(kuò)張
DTrace
基礎(chǔ)集合類
視頻工具箱和硬件加速
字符串渲染
讓東西變得不那么糟
游戲中的多點(diǎn)互聯(lián)
iCloud 和 Core Data
Views
虛擬音域 - 聲音設(shè)計(jì)的藝術(shù)
導(dǎo)航應(yīng)用
線程安全類的設(shè)計(jì)
置換測試: Mock, Stub 和其他
Build 工具
KVC 和 KVO
Core Image 和視頻
Android Intents
在 iOS 上捕獲視頻
四軸無人機(jī)項(xiàng)目
Mach-O 可執(zhí)行文件
UI 測試
值對(duì)象
活動(dòng)追蹤
依賴注入
Swift
項(xiàng)目管理
整潔的 Table View 代碼
Swift 方法的多面性
為什么今天安全仍然重要
Core Data 概述
Foundation
Swift 的函數(shù)式 API
iOS 7 的多任務(wù)
自定義 Collection View 布局
測試 View Controllers
訪談
收據(jù)驗(yàn)證
數(shù)據(jù)同步
自定義 ViewController 容器轉(zhuǎn)場
游戲
調(diào)試核對(duì)清單
View Controller 容器
學(xué)無止境
XCTest 測試實(shí)戰(zhàn)
iOS 7
Layer 中自定義屬性的動(dòng)畫
第一期-更輕量的 View Controllers
精通 iCloud 文檔存儲(chǔ)
代碼審查的藝術(shù):Dropbox 的故事
GPU 加速下的圖像視覺
Artsy
照片擴(kuò)展
理解 Scroll Views
使用 VIPER 構(gòu)建 iOS 應(yīng)用
Android 中的 SQLite 數(shù)據(jù)庫支持
Fetch 請(qǐng)求
導(dǎo)入大數(shù)據(jù)集
iOS 開發(fā)者的 Android 第一課
iOS 上的相機(jī)捕捉
語言標(biāo)簽
同步案例學(xué)習(xí)
依賴注入和注解,為什么 Java 比你想象的要好
編譯器
基于 OpenCV 的人臉識(shí)別
玩轉(zhuǎn)字符串
相機(jī)工作原理
Build 過程

View Controller 轉(zhuǎn)場

自定義轉(zhuǎn)場動(dòng)畫

iOS 7 中最讓我激動(dòng)的特性之一就是提供了新的 API 來支持自定義 view contrioller 之間的轉(zhuǎn)場動(dòng)畫。iOS 7 發(fā)布之前,我自己寫過一些 view controller 之間的轉(zhuǎn)場動(dòng)畫,這是一個(gè)比較頭疼的過程,而且這種做法并不被蘋果完全地支持,尤其是如果你想讓這個(gè)轉(zhuǎn)場動(dòng)畫有交互式的效果就更難了。

在繼續(xù)閱讀之前,我需要先聲明一下:這個(gè) API 是新近才發(fā)布的,目前還沒有所謂的最佳實(shí)踐。通常來說,開發(fā)者需要探索幾個(gè)月才能得出關(guān)于新 API 的最佳實(shí)踐。因此請(qǐng)將本文看做對(duì)一個(gè)新 API 的探索,而非關(guān)于這個(gè)新 API 的最佳實(shí)踐介紹。如果您有更好的關(guān)于這個(gè) API 的實(shí)踐,請(qǐng)不吝賜教,我們會(huì)把您的實(shí)踐更新到這篇文章中。

在開始研究新的 API 之間,我們先來看看在 iOS 7 中 navigation controller 之間的默認(rèn)的行為發(fā)生了那些改變:在 navigation controller 中,切換兩個(gè) view controller 的動(dòng)畫變得更有交互性。比方說你想要 pop 一個(gè) view controller 出去,你可以用手指從屏幕的左邊緣開始拖動(dòng),慢慢地把當(dāng)前的 view controller 向右拖出屏幕去。

接下來,我們來看看這個(gè)新 API。很有趣的一個(gè)現(xiàn)象是,這部分 API 大量的使用了協(xié)議而不是具體的對(duì)象。這初看起來有點(diǎn)奇怪,但我個(gè)人更喜歡這樣的 API 設(shè)計(jì),因?yàn)檫@種設(shè)計(jì)給了我們這些開發(fā)者更大的靈活性。下面,讓我們來做件簡單的事情:在 Navigation Controller 中,實(shí)現(xiàn)一個(gè)自定義的 push 動(dòng)畫效果(本文中的示例代碼托管在 Github)。為了完成這個(gè)任務(wù),需要實(shí)現(xiàn) UINavigationControllerDelegate 中的新方法:

編者注 原文的作者在 Github 上面的示例代碼和文章中的代碼有一些出入(比如下面這里是 Push,但是在示例代碼中是 Pop)。如果需要,您也可以參考這個(gè)修正版示例代碼,和文章的代碼差異要小一點(diǎn)。

- (id<UIViewControllerAnimatedTransitioning>)
                   navigationController:(UINavigationController *)navigationController
        animationControllerForOperation:(UINavigationControllerOperation)operation
                     fromViewController:(UIViewController*)fromVC
                       toViewController:(UIViewController*)toVC
{
    if (operation == UINavigationControllerOperationPush) {
        return self.animator;
    }
    return nil;
}

從上面的代碼可以看出,我們可以根據(jù)不同的 operation(Push 或 Pop)返回不同的 animator。我們可以把 animator 存到一個(gè)屬性中,從而在多個(gè) operation 之間實(shí)現(xiàn)共享,或者我們也可以為每個(gè) operation 都創(chuàng)建一個(gè)新的 animator 對(duì)象,這里的靈活性很大。

為了讓動(dòng)畫運(yùn)行起來,我們創(chuàng)建一個(gè)自定義類,并且實(shí)現(xiàn) UIViewControllerContextTransitioning 這個(gè)協(xié)議:

@interface Animator : NSObject <UIViewControllerAnimatedTransitioning>

@end

這個(gè)協(xié)議要求我們實(shí)現(xiàn)兩個(gè)方法,其中一個(gè)定義了動(dòng)畫的持續(xù)時(shí)間:

- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext
{
    return 0.25;
}

另一個(gè)方法描述整個(gè)動(dòng)畫的執(zhí)行效果:

- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
    UIViewController* toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    UIViewController* fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    [[transitionContext containerView] addSubview:toViewController.view];
    toViewController.view.alpha = 0;

    [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
        fromViewController.view.transform = CGAffineTransformMakeScale(0.1, 0.1);
        toViewController.view.alpha = 1;
    } completion:^(BOOL finished) {
        fromViewController.view.transform = CGAffineTransformIdentity;
        [transitionContext completeTransition:![transitionContext transitionWasCancelled]];

    }];

}

從上面的例子中,你可以看到如何運(yùn)用協(xié)議的:這個(gè)方法中通過接受一個(gè)類型為 id<UIViewControllerContextTransitioning> 的參數(shù),來獲取 transition context。值得注意的是,執(zhí)行完動(dòng)畫之后,我們需要調(diào)用 transitionContext 的 completeTransition: 這個(gè)方法來更新 view controller 的狀態(tài)。剩下的代碼和 iOS 7 之前的一樣了,我們從 transition context 中得到了需要做轉(zhuǎn)場的兩個(gè) view controller,然后使用最簡單的 UIView animation 來實(shí)現(xiàn)了轉(zhuǎn)場動(dòng)畫。這就是全部代碼了,我們已經(jīng)實(shí)現(xiàn)了一個(gè)縮放效果的轉(zhuǎn)場動(dòng)畫。

注意,這里只是為 Push 操作實(shí)現(xiàn)了自定義效果的轉(zhuǎn)場動(dòng)畫,對(duì)于 Pop 操作,還是會(huì)使用默認(rèn)的滑動(dòng)效果,另外,上面我們實(shí)現(xiàn)的轉(zhuǎn)場動(dòng)畫無法交互,下面我們就來看看解決這個(gè)問題。

交互式的轉(zhuǎn)場動(dòng)畫

想要?jiǎng)赢嬜兊乜梢越换シ浅:唵?,我們只需要覆蓋另一個(gè) UINavigationControllerDelegate 的方法:

- (id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController*)navigationController
                          interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>)animationController
{
    return self.interactionController;
}

注意,在非交互式動(dòng)畫效果中,該方法返回 nil。

這里返回的 interaction controller 是 UIPercentDrivenInteractionTransition 類的一個(gè)實(shí)例,開發(fā)者不需要任何配置就可工作。我們創(chuàng)建了一個(gè)拖動(dòng)手勢(Pan Recognizer),下面是處理該手勢的代碼:

if (panGestureRecognizer.state == UIGestureRecognizerStateBegan) {
    if (location.x >  CGRectGetMidX(view.bounds)) {
        navigationControllerDelegate.interactionController = [[UIPercentDrivenInteractiveTransition alloc] init];
        [self performSegueWithIdentifier:PushSegueIdentifier sender:self];
    }
} 

編者注 這里的代碼有一點(diǎn)示意的意思,和實(shí)際代碼有些出入,為了尊重原作者,我們沒有進(jìn)行修改,您可以參考原文在 Github 上的示例代碼進(jìn)行對(duì)比,也可以參考這個(gè)修正版示例代碼。

只有當(dāng)用戶從屏幕右半部分開始觸摸的時(shí)候,我們才把下一次動(dòng)畫效果設(shè)置為交互式的(通過設(shè)置 interactionController 這個(gè)屬性來實(shí)現(xiàn)),然后執(zhí)行方法 performSegueWithIdentifier:(如果你不是使用的 storyboards,那么就直接調(diào)用 pushViewController... 這類方法)。為了讓轉(zhuǎn)場動(dòng)畫持續(xù)進(jìn)行,我們需要調(diào)用 interaction controller 的一個(gè)方法:

else if (panGestureRecognizer.state == UIGestureRecognizerStateChanged) {
    CGFloat d = (translation.x / CGRectGetWidth(view.bounds)) * -1;
    [interactionController updateInteractiveTransition:d];
} 

該方法會(huì)根據(jù)用戶手指拖動(dòng)的距離計(jì)算一個(gè)百分比,切換的動(dòng)畫效果也隨著這個(gè)百分比來走。最酷的是,interaction controller 會(huì)和 animation controller 一起協(xié)作,我們只使用了簡單的 UIView animation 的動(dòng)畫效果,但是interaction controller 卻控制了動(dòng)畫的執(zhí)行進(jìn)度,我們并不需要把 interaction controller 和 animation controller 關(guān)聯(lián)起來,因?yàn)樗羞@些系統(tǒng)都以一種解耦的方式自動(dòng)地替我們完成了。

最后,我們需要根據(jù)用戶手勢的停止?fàn)顟B(tài)來判斷該操作是結(jié)束還是取消,并調(diào)用 interaction controller 中對(duì)應(yīng)的方法:

else if (panGestureRecognizer.state == UIGestureRecognizerStateEnded) {
    if ([panGestureRecognizer velocityInView:view].x < 0) {
        [interactionController finishInteractiveTransition];
    } else {
        [interactionController cancelInteractiveTransition];
    }
    navigationControllerDelegate.interactionController = nil;
}

注意,當(dāng)切換完成或者取消的時(shí)候,記得把 interaction controller 設(shè)置為 nil。因?yàn)槿绻乱淮蔚霓D(zhuǎn)場是非交互的, 我們不應(yīng)該返回這個(gè)舊的 interaction controller。

現(xiàn)在我們已經(jīng)實(shí)現(xiàn)了一個(gè)完全自定義的可交互的轉(zhuǎn)場動(dòng)畫了。通過簡單的手勢識(shí)別和 UIKit 提供的一個(gè)類,用幾行代碼就達(dá)到完成了。對(duì)于大部分的應(yīng)用場景,你讀到這兒就夠用了,使用上面提到的方法就可以達(dá)到你想要的動(dòng)畫效果了。但如果你想更對(duì)轉(zhuǎn)場動(dòng)畫或者交互效果進(jìn)行深度定制,請(qǐng)繼續(xù)閱讀下面一節(jié)。

使用 GPUImage 定制動(dòng)畫

下面我們就來看看如何真正的,徹底的定制動(dòng)畫效果。這一次我們不使用 UIView animation,甚至連 Core Animation 也不用,完全自己來實(shí)現(xiàn)所有的動(dòng)畫效果。在 Letterpress-style 這個(gè)項(xiàng)目中,剛開始我嘗試使用 Core Image 來做動(dòng)畫效果,但是在我的 iPhone 4 上,動(dòng)畫的渲染最高只能達(dá)到 9 幀/秒,離我想要的 60 幀/秒差得很遠(yuǎn)。

但是當(dāng)我使用了 GPUImage 之后,實(shí)現(xiàn)一個(gè)非常漂亮的動(dòng)畫變的異常簡單。這里我們要實(shí)現(xiàn)的轉(zhuǎn)場效果是:兩個(gè) view controller 像素化,然后相互消融在一起。實(shí)現(xiàn)方法是先對(duì)兩個(gè) view controller 進(jìn)行截屏,然后再用 GPUImage 的圖片濾鏡(filter)處理這兩張截圖。

首先,我們先創(chuàng)建一個(gè)自定義類,這個(gè)類實(shí)現(xiàn)了 UIViewControllerAnimatedTransitioningUIViewControllerInteractiveTransitioning 這兩個(gè)協(xié)議:

@interface GPUImageAnimator : NSObject
  <UIViewControllerAnimatedTransitioning,
   UIViewControllerInteractiveTransitioning>

@property (nonatomic) BOOL interactive;
@property (nonatomic) CGFloat progress;

- (void)finishInteractiveTransition;
- (void)cancelInteractiveTransition;

@end

為了加速動(dòng)畫的運(yùn)行,我們可以把圖片一次加載到 GPU 中,然后所有的處理和繪圖都直接在 GPU 上執(zhí)行,不需要再傳送到 CPU 處理(這種數(shù)據(jù)傳輸非常慢)。通過使用 GPUImageView,我們就可以直接使用 OpenGL 畫圖(我們不需要手寫 OpenGL 這種底層的代碼,只要繼續(xù)使用 GPUImage 封裝好的接口就可以)。

創(chuàng)建濾鏡鏈(filter chain)也非常的直觀,我們可以直接在樣例代碼的 setup 方法中看到如何構(gòu)造它。比較有挑戰(zhàn)的是如何讓濾鏡也“動(dòng)”起來。GPUImage 沒有直接提供給我們動(dòng)畫效果,因此我們需要每渲染一幀就更新一下濾鏡來實(shí)現(xiàn)動(dòng)態(tài)的濾鏡效果。使用 CADisplayLink 可以完成這個(gè)工作:

編者注 原文中的示例代碼中缺少了這一章的內(nèi)容,我在原作者的 Github Gist 上找到了相關(guān)的源碼,整理之后放到了 Github 上,您可以在這里找到它。

self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(frame:)];
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];

frame 方法中,我們可以根據(jù)時(shí)間來更新動(dòng)畫進(jìn)度,并相應(yīng)地更新濾鏡:

- (void)frame:(CADisplayLink*)link
{
    self.progress = MAX(0, MIN((link.timestamp - self.startTime) / duration, 1));
    self.blend.mix = self.progress;
    self.sourcePixellateFilter.fractionalWidthOfAPixel = self.progress *0.1;
    self.targetPixellateFilter.fractionalWidthOfAPixel = (1- self.progress)*0.1;
    [self triggerRenderOfNextFrame];
}

好了,基本上這樣就完成了。如果你想要實(shí)現(xiàn)交互式的轉(zhuǎn)場效果,那么在這里,就不能使用時(shí)間,而是要根據(jù)手勢來更新動(dòng)畫進(jìn)度,其他的代碼基本差不多。

這個(gè)功能非常強(qiáng)大,你可以使用 GPUImage 中任何已有的濾鏡,或者寫一個(gè)自己的 OpenGL 著色器(shader)來達(dá)到你想要的效果。

結(jié)論

本文只探討了在 navigation controller 中的兩個(gè) view controller 之間的轉(zhuǎn)場動(dòng)畫,但是這些做法在 tab bar controller 或者任何你自己定義的 view controller 容器中也是通用的。另外,在 iOS 7 中,UICollectionViewController 也進(jìn)行了擴(kuò)展,現(xiàn)在你可以在布局之間進(jìn)行自動(dòng)以及交互的動(dòng)畫切換,背后使用的也是同樣的機(jī)制。這真是太強(qiáng)大了。

在和 Orta 討論這個(gè) API 的時(shí)候,他提到他已經(jīng)在大量地使用這些機(jī)制以創(chuàng)建更輕量的 view controller。與其在一個(gè) view controller 中維護(hù)各種狀態(tài),不如再創(chuàng)建一個(gè)新的 view controller,使用自定義的轉(zhuǎn)場動(dòng)畫,然后在這個(gè)轉(zhuǎn)場動(dòng)畫中來移動(dòng)你的各種 view。

擴(kuò)展閱讀

上一篇:代碼簽名探析下一篇:圖片格式