概述
這篇文章,我將講述幾種轉場動畫的自定義方式,並且每種方式附上一個示例,畢竟程式碼才是我們的語言,這樣比較容易上手。其中主要有以下三種自定義方法,供大家參考:
- Push & Pop
- Modal
- Segue
前兩種大家都很熟悉,第三種是 Stroyboard
中的拖線,屬於 UIStoryboardSegue
類,通過繼承這個類來自定義轉場過程動畫。
Push & Pop
首先說一下 Push & Pop
這種轉場的自定義,操作步驟如下:
- 建立一個檔案繼承自
NSObject
, 並遵守UIViewControllerAnimatedTransitioning
協議。 - 實現該協議的兩個基本方法:
1234//指定轉場動畫持續的時長func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval//轉場動畫的具體內容func animateTransition(transitionContext: UIViewControllerContextTransitioning) - 遵守
UINavigationControllerDelegate
協議,並實現此方法:
1func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning?
在此方法中指定所用的UIViewControllerAnimatedTransitioning
,即返回 第1步 中建立的類。注意:由於需要
Push
和Pop
,所以需要兩套動畫方案。解決方法為:在 第1步 中,建立兩個檔案,一個用於
Push
動畫,一個用於Pop
動畫,然後 第3步 中在返回動畫類之前,先判斷動畫方式(Push 或 Pop), 使用operation == UINavigationControllerOperation.Push
即可判斷,最後根據不同的方式返回不同的類。到這裡就可以看到轉場動畫的效果了,但是大家都知道,系統預設的
Push 和 Pop
動畫都支援手勢驅動,並且可以根據手勢移動距離改變動畫完成度。幸運的是,Cocoa 已經整合了相關方法,我們只用告訴它百分比就可以了。所以下一步就是 手勢驅動。 - 在第二個
UIViewController
中給View
新增一個滑動(Pan)手勢。
建立一個UIPercentDrivenInteractiveTransition
屬性。
在手勢的監聽方法中計算手勢移動的百分比,並使用UIPercentDrivenInteractiveTransition
屬性的updateInteractiveTransition()
方法實時更新百分比。
最後在手勢的state
為ended
或cancelled
時,根據手勢完成度決定是還原動畫還是結束動畫,使用UIPercentDrivenInteractiveTransition
屬性的cancelInteractiveTransition()
或finishInteractiveTransition()
方法。 - 實現
UINavigationControllerDelegate
中的另一個返回UIViewControllerInteractiveTransitioning
的方法,並在其中返回第4步
建立的UIPercentDrivenInteractiveTransition
屬性。
至此,Push 和 Pop 方式的自定義就完成了,具體細節看下面的示例。
自定義 Push & Pop 示例
此示例來自 Kitten Yang 的blog 實現Keynote中的神奇移動效果,我將其用 Swift 實現了一遍,原始碼地址: MagicMove,下面是執行效果。
初始化
- 建立兩個
ViewController
,一個繼承自UICollectionViewController
,取名ViewController
。另一個繼承UIViewController
,取名DetailViewController
。在Stroyboard
中建立並繫結。 - 在
Stroyboard
中拖一個UINavigationController
,刪去預設的 rootViewController,使ViewController
作為其 rootViewController,再拖一條從ViewController
到DetailViewController
的 segue。 - 在
ViewController
中自定義UICollectionViewCell
,新增UIImageView
和UILabel
。 - 在
DetailViewController
中新增UIImageView
和UITextView
新增 UIViewControllerAnimatedTransitioning
- 新增一個
Cocoa Touch Class
,繼承自NSObject
,取名MagicMoveTransion
,遵守UIViewControllerAnimatedTransitioning
協議。 - 實現協議的兩個方法,並在其中編寫
Push
的動畫。 具體的動畫實現過程都在程式碼的註釋裡 :
123456789101112131415161718192021222324252627282930313233343536func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {return 0.5}func animateTransition(transitionContext: UIViewControllerContextTransitioning) {//1.獲取動畫的源控制器和目標控制器let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) as! ViewControllerlet toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) as! DetailViewControllerlet container = transitionContext.containerView()//2.建立一個 Cell 中 imageView 的截圖,並把 imageView 隱藏,造成使使用者以為移動的就是 imageView 的假象let snapshotView = fromVC.selectedCell.imageView.snapshotViewAfterScreenUpdates(false)snapshotView.frame = container.convertRect(fromVC.selectedCell.imageView.frame, fromView: fromVC.selectedCell)fromVC.selectedCell.imageView.hidden = true//3.設定目標控制器的位置,並把透明度設為0,在後面的動畫中慢慢顯示出來變為1toVC.view.frame = transitionContext.finalFrameForViewController(toVC)toVC.view.alpha = 0//4.都新增到 container 中。注意順序不能錯了container.addSubview(toVC.view)container.addSubview(snapshotView)//5.執行動畫UIView.animateWithDuration(transitionDuration(transitionContext), delay: 0, options: UIViewAnimationOptions.CurveEaseInOut, animations: { () -> Void insnapshotView.frame = toVC.avatarImageView.frametoVC.view.alpha = 1}) { (finish: Bool) -> Void infromVC.selectedCell.imageView.hidden = falsetoVC.avatarImageView.image = toVC.imagesnapshotView.removeFromSuperview()//一定要記得動畫完成後執行此方法,讓系統管理 navigationtransitionContext.completeTransition(true)}}
使用動畫
- 讓
ViewController
遵守UINavigationControllerDelegate
協議。 - 在
ViewController
中設定NavigationController
的代理為自己:
12345override func viewDidAppear(animated: Bool) {super.viewDidAppear(animated)self.navigationController?.delegate = self} - 實現
UINavigationControllerDelegate
協議方法:
1234567func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {if operation == UINavigationControllerOperation.Push {return MagicMoveTransion()} else {return nil}} - 在
ViewController
的controllerCell
的點選方法中,傳送segue
12345override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {self.selectedCell = collectionView.cellForItemAtIndexPath(indexPath) as! MMCollectionViewCellself.performSegueWithIdentifier("detail", sender: nil)} - 在傳送
segue
的時候,把點選的image
傳送給DetailViewController
123456override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {if segue.identifier == "detail" {let detailVC = segue.destinationViewController as! DetailViewControllerdetailVC.image = self.selectedCell.imageView.image}}至此,在點選 Cell 後,就會執行剛剛自定義的動畫了。接下來就要加入手勢驅動。
手勢驅動
- 在
DetailViewController
的ViewDidAppear()
方法中,加入滑動手勢。
123let edgePan = UIScreenEdgePanGestureRecognizer(target: self, action: Selector("edgePanGesture:"))edgePan.edges = UIRectEdge.Leftself.view.addGestureRecognizer(edgePan) - 在手勢監聽方法中,建立
UIPercentDrivenInteractiveTransition
屬性,並實現手勢百分比更新。
1234567891011121314151617func edgePanGesture(edgePan: UIScreenEdgePanGestureRecognizer) {let progress = edgePan.translationInView(self.view).x / self.view.bounds.widthif edgePan.state == UIGestureRecognizerState.Began {self.percentDrivenTransition = UIPercentDrivenInteractiveTransition()self.navigationController?.popViewControllerAnimated(true)} else if edgePan.state == UIGestureRecognizerState.Changed {self.percentDrivenTransition?.updateInteractiveTransition(progress)} else if edgePan.state == UIGestureRecognizerState.Cancelled || edgePan.state == UIGestureRecognizerState.Ended {if progress > 0.5 {self.percentDrivenTransition?.finishInteractiveTransition()} else {self.percentDrivenTransition?.cancelInteractiveTransition()}self.percentDrivenTransition = nil}} - 實現返回
UIViewControllerInteractiveTransitioning
的方法並返回剛剛建立的UIPercentDrivenInteractiveTransition
屬性。
1234567func navigationController(navigationController: UINavigationController, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {if animationController is MagicMovePopTransion {return self.percentDrivenTransition} else {return nil}}
OK,到現在,手勢驅動就寫好了,但是還不能使用,因為還沒有實現 Pop 方法!現在自己去實現 Pop 動畫吧!請參考原始碼:MagicMove
Modal
modal轉場方式即使用 presentViewController()
方法推出的方式,預設情況下,第二個檢視從螢幕下方彈出。下面就來介紹下 modal 方式轉場動畫的自定義。
- 建立一個檔案繼承自
NSObject
, 並遵守UIViewControllerAnimatedTransitioning
協議。 - 實現該協議的兩個基本方法:
1234//指定轉場動畫持續的時長func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval//轉場動畫的具體內容func animateTransition(transitionContext: UIViewControllerContextTransitioning)以上兩個步驟和
Push & Pop
的自定義一樣,接下來就是不同的。 - 如果使用
Modal
方式從一個 VC 到另一個 VC,那麼需要第一個 VC 遵循UIViewControllerTransitioningDelegate
協議,並實現以下兩個協議方法:
12345//present動畫optional func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning?//dismiss動畫optional func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? - 在第一個 VC 的
prepareForSegue()
方法中,指定第二個 VC 的transitioningDelegate
為 self。
由 第3步 中兩個方法就可以知道,在建立轉場動畫時,最好也建立兩個動畫類,一個用於 Present, 一個用於 Dismiss,如果只建立一個動畫類,就需要在實現動畫的時候判斷是
Present
還是Dismiss
。這時,轉場動畫就可以實現了,接下來就手勢驅動了
- 在第一個 VC 中建立一個
UIPercentDrivenInteractiveTransition
屬性,並且在prepareForSegue()
方法中為第二個 VC.view 新增一個手勢,用以 dismiss. 在手勢的監聽方法中處理方式和Push & Pop
相同。 - 實現
UIViewControllerTransitioningDelegate
協議的另外兩個方法,分別返回Present
和Dismiss
動畫的百分比。
12345678//百分比Pushfunc interactionControllerForPresentation(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {return self.percentDrivenTransition}//百分比Popfunc interactionControllerForDismissal(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {return self.percentDrivenTransition}至此,
Modal
方式的自定義轉場動畫就寫完了。自己在編碼的時候有一些小細節需要注意,下面將展示使用Modal
方式的自定義動畫的示例。
自定義 Modal 示例
此示例和上面一個示例一樣,來自 Kitten Yang 的blog 實現3D翻轉效果,我也將其用 Swift 實現了一遍,同樣我的原始碼地址:FlipTransion,執行效果如下:
初始化
- 建立兩個
UIViewController
, 分別命名為:FirstViewController
和SecondViewController
。並在Storyboard
中新增兩個UIViewController
並繫結。 - 分別給兩個檢視新增兩個
UIImageView
,這樣做的目的是為了區分兩個控制器。當然你也可以給兩個控制器設定不同的背景,總之你開心就好。但是,既然做,就做認真點唄。注意:如果使用圖片並設定為Aspect Fill
或者其他的 Fill,一定記得呼叫imageView
的clipsToBounds()
方法裁剪去多餘的部分。 - 分別給兩個控制器新增兩個按鈕,第一個按鈕拖線到第二個控制器,第二個控制器繫結一個方法用來dismiss。
新增 UIViewControllerAnimatedTransitioning
- 新增一個
Cocoa Touch Class
,繼承自NSObject
,取名BWFlipTransionPush
(名字嘛,你開心就好。),遵守UIViewControllerAnimatedTransitioning
協議。 - 實現協議的兩個方法,並在其中編寫
Push
的動畫。 具體的動畫實現過程都在程式碼的註釋裡 :
1234567891011121314151617181920212223242526272829303132333435363738394041424344func animateTransition(transitionContext: UIViewControllerContextTransitioning) {let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) as! FirstViewControllerlet toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) as! SecondViewControllerlet container = transitionContext.containerView()container.addSubview(toVC.view)container.bringSubviewToFront(fromVC.view)//改變m34var transfrom = CATransform3DIdentitytransfrom.m34 = -0.002container.layer.sublayerTransform = transfrom//設定anrchPoint 和 positionlet initalFrame = transitionContext.initialFrameForViewController(fromVC)toVC.view.frame = initalFramefromVC.view.frame = initalFramefromVC.view.layer.anchorPoint = CGPointMake(0, 0.5)fromVC.view.layer.position = CGPointMake(0, initalFrame.height / 2.0)//新增陰影效果let shadowLayer = CAGradientLayer()shadowLayer.colors = [UIColor(white: 0, alpha: 1).CGColor, UIColor(white: 0, alpha: 0.5).CGColor, UIColor(white: 1, alpha: 0.5)]shadowLayer.startPoint = CGPointMake(0, 0.5)shadowLayer.endPoint = CGPointMake(1, 0.5)shadowLayer.frame = initalFramelet shadow = UIView(frame: initalFrame)shadow.backgroundColor = UIColor.clearColor()shadow.layer.addSublayer(shadowLayer)fromVC.view.addSubview(shadow)shadow.alpha = 0//動畫UIView.animateWithDuration(transitionDuration(transitionContext), delay: 0, options: UIViewAnimationOptions.CurveEaseOut, animations: { () -> Void infromVC.view.layer.transform = CATransform3DMakeRotation(CGFloat(-M_PI_2), 0, 1, 0)shadow.alpha = 1.0}) { (finished: Bool) -> Void infromVC.view.layer.anchorPoint = CGPointMake(0.5, 0.5)fromVC.view.layer.position = CGPointMake(CGRectGetMidX(initalFrame), CGRectGetMidY(initalFrame))fromVC.view.layer.transform = CATransform3DIdentityshadow.removeFromSuperview()transitionContext.completeTransition(!transitionContext.transitionWasCancelled())}}
動畫的過程我就不多說了,仔細看就會明白。
使用動畫
- 讓
FirstViewController
遵守UIViewControllerTransitioningDelegate
協議,並將self.transitioningDelegate
設定為 self。 - 實現
UIViewControllerTransitioningDelegate
協議的兩個方法,用來指定動畫類。
12345678//動畫Pushfunc animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {return BWFlipTransionPush()}//動畫Popfunc animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {return BWFlipTransionPop()}OK,如果你完成了Pop動畫,那麼現在就可以實現自定義 Modal 轉場了。現在只差手勢驅動了。
手勢驅動
- 想要同時實現
Push
和Pop
手勢,就需要給兩個viewController.view
新增手勢。首先在FirstViewController
中給自己新增一個螢幕右邊的手勢,在prepareForSegue()
方法中給SecondViewController.view
新增一個螢幕左邊的手勢,讓它們使用同一個手勢監聽方法。 - 實現監聽方法,不多說,和之前一樣,但還是有仔細看,因為本示例中轉場動畫比較特殊,而且有兩個手勢,所以這裡計算百分比使用的是
KeyWindow
。同時不要忘了:UIPercentDrivenInteractiveTransition
屬性。
123456789101112131415161718192021func edgePanGesture(edgePan: UIScreenEdgePanGestureRecognizer) {let progress = abs(edgePan.translationInView(UIApplication.sharedApplication().keyWindow!).x) / UIApplication.sharedApplication().keyWindow!.bounds.widthif edgePan.state == UIGestureRecognizerState.Began {self.percentDrivenTransition = UIPercentDrivenInteractiveTransition()if edgePan.edges == UIRectEdge.Right {self.performSegueWithIdentifier("present", sender: nil)} else if edgePan.edges == UIRectEdge.Left {self.dismissViewControllerAnimated(true, completion: nil)}} else if edgePan.state == UIGestureRecognizerState.Changed {self.percentDrivenTransition?.updateInteractiveTransition(progress)} else if edgePan.state == UIGestureRecognizerState.Cancelled || edgePan.state == UIGestureRecognizerState.Ended {if progress > 0.5 {self.percentDrivenTransition?.finishInteractiveTransition()} else {self.percentDrivenTransition?.cancelInteractiveTransition()}self.percentDrivenTransition = nil}} - 實現
UIViewControllerTransitioningDelegate
協議的另外兩個方法,分別返回Present
和Dismiss
動畫的百分比。
12345678//百分比Pushfunc interactionControllerForPresentation(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {return self.percentDrivenTransition}//百分比Popfunc interactionControllerForDismissal(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {return self.percentDrivenTransition}
現在,基於 Modal
的自定義轉場動畫示例就完成了。獲取完整原始碼:FlipTransion
Segue
這種方法比較特殊,是將 Stroyboard
中的拖線與自定義的 UIStoryboardSegue
類繫結自實現定義轉場過程動畫。
首先我們來看看 UIStoryboardSegue
是什麼樣的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@availability(iOS, introduced=5.0) class UIStoryboardSegue : NSObject { // Convenience constructor for returning a segue that performs a handler block in its -perform method. @availability(iOS, introduced=6.0) convenience init(identifier: String?, source: UIViewController, destination: UIViewController, performHandler: () -> Void) init!(identifier: String?, source: UIViewController, destination: UIViewController) // Designated initializer var identifier: String? { get } var sourceViewController: AnyObject { get } var destinationViewController: AnyObject { get } func perform() } |
以上是 UIStoryboardSegue
類的定義。從中可以看出,只有一個方法 perform()
,所以很明顯,就是重寫這個方法來自定義轉場動畫。
再注意它的其他屬性:sourceViewController
和 destinationViewController
,通過這兩個屬性,我們就可以訪問一個轉場動畫中的兩個主角了,於是自定義動畫就可以隨心所欲了。
只有一點需要注意:在拖線的時候,注意在彈出的選項中選擇 custom
。然後就可以和自定義的 UIStoryboardSegue
繫結了。
那麼,問題來了,這裡只有 perform
,那 返回時的動畫怎麼辦呢?請往下看:
Dismiss
由於 perfrom
的方法叫做:segue
,那麼返回轉場的上一個控制器叫做: unwind segue
- 解除轉場(unwind segue)通常和正常自定義轉場(segue)一起出現。
- 要解除轉場起作用,我們必須重寫perform方法,並應用自定義動畫。另外,導航返回源檢視控制器的過渡效果不需要和對應的正常轉場相同。
其 實現步驟 為:
- 建立一個
IBAction
方法,該方法在解除轉場被執行的時候會選擇地執行一些程式碼。這個方法可以有你想要的任何名字,而且不強制包含其它東西。它需要定義,但可以留空,解除轉場的定義需要依賴這個方法。 - 解除轉場的建立,設定的配置。這和之前的轉場建立不太一樣,等下我們將看看這個是怎麼實現的。
- 通過重寫
UIStoryboardSegue
子類裡的perform()
方法,來實現自定義動畫。 UIViewController類
提供了特定方法的定義,所以系統知道解除轉場即將執行。
當然,這麼說有一些讓人琢磨不透,不知道什麼意思。那麼,下面再通過一個示例來深入瞭解一下。
Segue 示例
這個示例是我自己寫的,原始碼地址:SegueTransion,開門見山,直接上圖。
GIF演示
初始化
- 建立兩個
UIViewController
, 分別命名為:FirstViewController
和SecondViewController
。並在Storyboard
中新增兩個UIViewController
並繫結。 - 分別給兩個控制器新增背景圖片或使用不同的背景色,用以區分。在
FirstViewController
中新增一個觸發按鈕,並拖線到SecondViewController
中,在彈出的選項中選擇custion
。
Present
- 新增一個
Cocoa Touch Class
,繼承自UIStoryboardSegue
,取名FirstSegue
(名字請隨意)。並將其繫結到上一步中拖拽的segue
上。 - 重寫
FirstSegue
中的perform()
方法,在其中編寫動畫邏輯。
12345678910111213141516171819override func perform() {var firstVCView = self.sourceViewController.view as UIView!var secondVCView = self.destinationViewController.view as UIView!let screenWidth = UIScreen.mainScreen().bounds.size.widthlet screenHeight = UIScreen.mainScreen().bounds.size.heightsecondVCView.frame = CGRectMake(0.0, screenHeight, screenWidth, screenHeight)let window = UIApplication.sharedApplication().keyWindowwindow?.insertSubview(secondVCView, aboveSubview: firstVCView)UIView.animateWithDuration(0.5, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0, options: UIViewAnimationOptions.CurveLinear, animations: { () -> Void insecondVCView.frame = CGRectOffset(secondVCView.frame, 0.0, -screenHeight)}) { (finished: Bool) -> Void inself.sourceViewController.presentViewController(self.destinationViewController as! UIViewController,animated: false,completion: nil)}}
還是一樣,動畫的過程自己看,都是很簡單的。
Present手勢
這裡需要注意,使用這種方式自定義的轉場動畫不能動態手勢驅動,也就是說不能根據手勢百分比動態改變動畫完成度。
所以,這裡只是簡單的新增一個滑動手勢(swip)。
- 在
FisrtViewController
中新增手勢:
123var swipeGestureRecognizer: UISwipeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: "showSecondViewController")swipeGestureRecognizer.direction = UISwipeGestureRecognizerDirection.Upself.view.addGestureRecognizer(swipeGestureRecognizer) - 實現手勢監聽方法:
123func showSecondViewController() {self.performSegueWithIdentifier("idFirstSegue", sender: self)}
現在已經可以 present 了,接下來實現 dismiss
。
Dismiss
- 在
FirstViewController
中新增一個IBAction
方法,方法名可以隨便,有沒有返回值都隨便。 - 在
Storyboard
中選擇SecondViewController
按住control鍵
拖線到SecondViewController
的Exit
圖示。並在彈出選項中選擇上一步新增IBAction
的方法。
- 在
Storyboard
左側的文件檢視中找到上一步拖的segue
,並設定identifier
- 再新增一個
Cocoa Touch Class
,繼承自UIStoryboardSegue
,取名FirstSegueUnWind
(名字請隨意)。並重寫其perform()
方法,用來實現dismiss
動畫。 - 在
FirstViewController
中重寫下面方法。並根據identifier
判斷是不是需要 dismiss,如果是就返回剛剛建立的FirstUnWindSegue
。
123456789override func segueForUnwindingToViewController(toViewController: UIViewController, fromViewController: UIViewController, identifier: String?) -> UIStoryboardSegue {if identifier == "firstSegueUnwind" {return FirstUnwindSegue(identifier: identifier, source: fromViewController, destination: toViewController, performHandler: { () -> Void in})}return super.segueForUnwindingToViewController(toViewController, fromViewController: fromViewController, identifier: identifier)} - 最後一步,在
SecondViewController
的按鈕的監聽方法中實現 dismiss, 注意不是呼叫self.dismiss...
!
123@IBAction func shouldDismiss(sender: AnyObject) {self.performSegueWithIdentifier("firstSegueUnwind", sender: self)}給
SecondViewController
新增手勢,將手勢監聽方法也設定為以上這個方法, 參考程式碼:SegueTransion。
總結
一張圖總結一下3種方法的異同點。