Swift:超炫的View Controller切換動畫


匿名社交應用Secret的開發者開發了一款叫做Ping的應用,用戶可以他們感興趣的話題的推送。

Ping有一個很炫的東西,就是主界面和之間切換的動畫做的非常的好。每次看到一個非常炫的動畫,都不由得會想:“這個東西我要不要自己實現以下”。哈哈~~~

這個教程里,你會學到如何用Swift實現這樣的很酷的動畫。你會學到如何使用shape layer,遮罩和使用UIViewControllerAnimnatedTransitioning協議和UIPercentDrivenInteractivetransition類等實現View Controller界面切換動畫。

不過需要注意,這里假定你已經有一定的Swift開發基礎。如果只是初學的話,請自行查看我得其他Swift教程。

 

開篇簡介

我們主要介紹Ping里從一個View Controller跳轉到另一個的時候的動畫。

在iOS里,你可以在UINavigationController中放入兩個View Controller,並實現UIViewControllerAnimatedTransitioning協議來實現界面切換的動畫。具體的細節有:

  • 動畫的時間長度
  • 創建一個容器View來控制兩個View Controller的View
  • 可以實現任意你能想到的動畫

這些動畫,你可以用UIView得動畫方法來作,也可以用core animation這樣的比較底層的方法來做。本教程會使用后者。

 

如何實現

現在你已經知道代碼大概會添加到什么地方。下面討論下如何實現那個Ping的那個圈圈動畫。這動畫嚴格的描述起來是:

  • 圓圈是從右側的按鈕產生。並且從圈中可以看到下面一層試圖的內容。
  • 也就是說,這個圓圈是一個遮罩。圓圈里的都可以看到,外面的全部都隱藏。

你可以用CALayer的mask可以達到這個效果。當然還需要設置alpha為0來隱藏下面一個視圖的內容。alpha值設定為1的時候顯示下面視圖的內容。

 

現在你就懂了遮罩了。下一步就是決定用哪一種CAShapeLayer來實現這個遮罩。只需要修改這些CAShapeLayer組成的圓圈的半徑。

 

現在開始

這里就不十分詳細的敘述了,都是些關於創建和配置項目的步驟。

1. 創建一個新的項目。選擇一個single view application

2. 項目名稱設置為CircleTransition。語言選擇Swift。Devices就選擇iPhone

項目到此初步創建好了。在Main.stroyboard里只有一個view controller。但是我們的動畫需要兩個至少的view controller。不過首先需要把現在的這個view controller和UINavigationController關聯起來。選中這個唯一的view controller,之后在菜單欄中選擇Editor->Embed In->Navigation Controller。之后這個navigation controller就會成為initial controller,后面連着最開始生成的那個view controller。之后,選中這個navigation controller,在右側菜單欄的第四個tab中勾去“Shows navigation bar”。因為在我們的app中不需要navigation bar。

 接下來添加另外一個view controller。給這個view controller指定class為ViewController。

然后,給每一個view controller,除了navigation controller,添加一個按鈕。雙擊按鈕,刪除文字,之后把按鈕的背景色設置為黑色。另外一個按鈕也同樣處理。給這兩個按鈕設定autolayout。指定他們在右上角上。指定這兩個按鈕的寬度和高度為40。

最后讓按鈕變成圓形的。右邊菜單的第三個tab中選擇“user defined runtime attributes”。點下面的加號,添加如圖所示的內容。設置button的corner radius為15。

 這樣這個按鈕在運行起來的時候就是圓形的了。設定完成之后暫時看不到這個效果。運行起來以后:

現在需要在每個view controller中添加些內容了。先把這兩個view controller的背景色修改一下。

現在這個app大致已經成型了。不同的顏色可以代表你將來要顯示出來的各種各樣的內容。所需要的就是把這個兩個view controller連起來。在橘色的controller的按鈕中放下鼠標。按下ctrl然后把光標拖動到另外一個controller上。這是會出現一個彈出的菜單。把這個菜單的action用同樣的方法和這個controller再連接一次,並選擇show。這樣,在這個按鈕選擇的時候,navigation controller就會push到下一個view controller中。這是一個segue。后面的教程會需要這個segue所以這里給這個segue一個identifer,叫做“PushSegue”。運行代碼,點擊橘色controller的按鈕就會跳轉到紫色的controller了。

因為這是一個循環的過程,所以從橘色到紫色之后還需要從紫色回到橘色。現在就完成這個功能。首先,在紫色controller綁定的ViewController類中添加一個action方法。

    @IBAction func circleTapped(sender: UIButton){
        self.navigationController?.popViewControllerAnimated(true)
    }

並添加紫色controller上的按鈕的引用,這個會在后面用到:

@IBOutlet weak var button: UIButton!

之后給紫色controller的按鈕的“touch up inside”事件添加上面的@IBAction。

綁定按鈕的屬性:

再運行起來看看。橘色到紫色,紫色到橘色循環往復!

注意:兩個view controller都需要綁定按鈕和按鈕事件!否則后面的動畫只能執行一次!

 

自定義動畫

這里主要處理的就是navigation controller的push和pop動畫。這需要我們實現UINavigationControllerDelegate協議的animationControllerForOperation方法。直接在ViewController中添加一個新的類:

class NavigationControllerDelegate: NSObject, UINavigationControllerDelegate{
    func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return nil
    }
}

首先,在右側的菜單中選中Object這個item。

之后,把這個東西拖動到navigation controller secene下。

然后選中這個Object,在右側菜單的第三個tab上修改class為我們剛剛定義的NavigationControllerDelegate

下一步,給navigation controller指定delegate。選中navigation controller,然后在右側最后的菜單中連接navigation controller的delegate選項到剛剛拖進來的Object上:

這個時候還是不會有特定的效果出現。因為方法還是空的,只能算是一個placeholder方法。

func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return nil
}

這個方法接受兩個在navigation controller中得controller。從一個跳轉到另一個的兩個controller。並返回一個實現了UIViewControllerAnimatedTransitioning的對象。所以,我們需要創建一個實現了UIViewControllerAnimatedTransitioning協議的類。

class CircleTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning

首先添加一個屬性:

weak var transitionContext: UIViewControllerContextTransitioning?

這個屬性會在后面的代碼中用到。

添加一個方法:

func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {
    return 0.5
}

這個方法返回動畫執行的時間。

添加動畫方法:

    func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
        // 1
        self.transitionContext = transitionContext
        
        // 2
        var containerView = transitionContext.containerView()
        var fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) as ViewController
        var toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) as ViewController
        var button = fromViewController.button
        
        // 3
        containerView.addSubview(toViewController.view)
        
        // 4
        var circleMaskPathInitial = UIBezierPath(ovalInRect: button.frame)
        var extremePoint = CGPointMake(button.center.x, button.center.y - CGRectGetHeight(toViewController.view.bounds)) // need more research
        var radius = sqrt(extremePoint.x * extremePoint.x + extremePoint.y * extremePoint.y)
        var circleMaskPathFinal = UIBezierPath(ovalInRect: CGRectInset(button.frame, -radius, -radius))
        
        // 5
        var maskLayer = CAShapeLayer()
        maskLayer.path = circleMaskPathFinal.CGPath
        toViewController.view.layer.mask = maskLayer
        
        // 6
        var maskLayerAnimation = CABasicAnimation(keyPath: "path")
        maskLayerAnimation.fromValue = circleMaskPathInitial.CGPath
        maskLayerAnimation.toValue = circleMaskPathFinal.CGPath
        maskLayerAnimation.duration = self.transitionDuration(self.transitionContext!)
        maskLayerAnimation.delegate = self
        maskLayer.addAnimation(maskLayerAnimation, forKey: "CircleAnimation")
    }

一步步的解釋:

  1. transitionContext屬性保持了一個類成員的引用。這樣在后面的代碼中可以用到。
  2. 取出containerView以及fromViewController和toViewController和controller上面的button引用。動畫主要還是作用在container view上的。
  3. 把toViewController的view添加到container view上。
  4. 創建兩個路勁,一個就是button的大小(button在運行起來之后是圓形的),另一個要足夠大到可以cover整個screen。動畫就是在這兩個path上來來回回。
  5. 創建一個CAShapeLayer作為mask用。給這個layer的path賦值為circleMaskPathFinal,否則動畫執行完成以后可能又縮回來。
  6. 創建一個CABasicAnimation動畫,key path是“path”,這個動畫作用於layer的path屬性上。動畫從circleMaskPathInitial執行到circleMaskPathFinal。並給這個動畫添加一個delegate,在動畫執行完成以后清理現場。

實現animation代理的方法:

    override func animationDidStop(anim: CAAnimation!, finished flag: Bool) {
        self.transitionContext?.completeTransition(!self.transitionContext!.transitionWasCancelled())
        self.transitionContext?.viewControllerForKey(UITransitionContextFromViewControllerKey)?.view.layer.mask = nil
    }

現在就可以用CircleTransitionAnimator來實現動畫的效果了。修改代碼NavigationControllerDelegate的代碼:

class NavigationControllerDelegate: NSObject, UINavigationControllerDelegate{
    func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return CircleTransitionAnimator()
    }
}

運行起來吧。點擊黑色的按鈕,動畫效果就出現了。

感覺不錯吧,但是這個是不夠的!

 

給動畫添加手勢響應

我們還要給這個動畫添加一個可以響應手勢的transition。響應手勢需要用到一個方法:navigationController->interactionControllerForAnimationController。這是UINavigationControllerDelegate中得一個方法。這個方法返回一個實現了協議UIViewControllerInteractiveTransitioning的對象。

iOS的SDK中提供了一個UIPercentDrivenInteractiveTransition的類。這個類實現了上面的協議,並且提供了很多其他的手勢處理實現。

NavigationControllerDelegate類中添加以下的屬性和方法:

class NavigationControllerDelegate: NSObject, UINavigationControllerDelegate{
    
    var interactionController: UIPercentDrivenInteractiveTransition?
    
    func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return CircleTransitionAnimator()
    }
    
    func navigationController(navigationController: UINavigationController, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { return self.interactionController }
}

既然是響應手勢的,那么一個pan的手勢是必不可少的了。不過首先要添加一些輔助的東西。

1. 在NavigationControllerDelegate中添加對navigation controller的引用。

@IBOutlet weak var navigationController: UINavigationController?

給這個引用添加對navigation controller的引用,如圖:

實現awakeFromNib方法:

    override func awakeFromNib() {
        super.awakeFromNib()
        
        var pan = UIPanGestureRecognizer(target: self, action: "panned:")
        self.navigationController!.view.addGestureRecognizer(pan)
    }

當pan這個動作在navigation controller的view上發生的時候就會觸發panned回調方法。給這個方法添加如下代碼:

    func panned(gestureRecognizer: UIPanGestureRecognizer){
        switch gestureRecognizer.state {
        case .Began:
            self.interactionController = UIPercentDrivenInteractiveTransition()
            if self.navigationController?.viewControllers.count > 1 {
                self.navigationController?.popViewControllerAnimated(true)
            }
            else{
                self.navigationController?.topViewController.performSegueWithIdentifier("PushSegue", sender: nil)
            }
        case .Changed:
            var translation = gestureRecognizer.translationInView(self.navigationController!.view)
            var completionProgress = translation.x / CGRectGetWidth(self.navigationController!.view.bounds)
            self.interactionController?.updateInteractiveTransition(completionProgress)
        case .Ended:
            if gestureRecognizer.velocityInView(self.navigationController!.view).x > 0 {
                self.interactionController?.finishInteractiveTransition()
            }
            else{
                self.interactionController?.cancelInteractiveTransition()
            }
            self.interactionController = nil
        default:
            self.interactionController?.cancelInteractiveTransition()
            self.interactionController = nil
        }
    }

在Begin中,pan手勢一開始執行就初始化出UIPercentDrivenInteractiveTransition對象,並作為值賦給屬性self.interactionController。

  • 如果在第一個view controller就設定一個push(在早先定義的一個segue),在第二個view controller的時候就設定一個pop。
  • 在navigation controller的push或者pop的時候則觸發NavigationControllerDelegate的返回self.interactionController對象的方法。

Changed,在這個方法中根據手勢移動的距離讓動畫移動不同的距離。這里apple已經替我們做了很多。

Ended,這里你會看到手勢的移動速度。如果是正則transition結束,如果是負則取消。同時,把interactionController值設置為nil。

default,如果是其他的狀態就直接取消trnasition並把interactionController值設置為nil。

 

運行程序,在屏幕上左右移動你的手指看看效果吧!

 

 

 

 

 

 

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM