效果圖
以上是一個關于CollectionViewCell點擊展開為一個二級頁面的轉場動畫,支持手勢過渡及完成度展現。
主要參考Kitten-Yang的文章做的效果。
UIViewControllerAnimatedTransitioning
官方開發文檔
簡單來說,這是一個協議。通過創建遵守該協議的對象,可以創建一個動畫制作者對象,從而高度定制ViewController的轉場動畫。不過這個轉場必須是不能互動的,如果需要創建支持互動的動畫,則必須將你的動畫制作者對象與另一個能夠控制動畫的對象關聯起來。
互動支持
為了讓轉場動畫對手勢等有互動支持,需要與另一個遵守UIViewControllerInteractiveTransitioning協議的對象進行關聯。
一般來說,我們不需要自己去專門寫一個類來遵守這個*** UIViewControllerInteractiveTransitioning協議,系統中有一個類名為UIPercentDrivenInteractiveTransition***已經幫我們寫好了。
UIPercentDrivenInteractiveTransition
該類基于UIViewControllerInteractiveTransitioning協議創建,為我們提供了三個主要方法:
- updateInteractiveTransition:
- cancelInteractiveTransition
- finishInteractiveTransition
第一個方法可以根據我們傳入的progress參數來調整動畫的完成度,后兩個方法就顧名思義了,一個取消互動動畫,一個結束互動動畫。
動畫思路
基本的類都介紹完了,接下來談一下動畫的思路。思路是最重要的一部分。
從效果圖可以看到,從第一個ViewController(以下稱FirstViewController)推到第二個ViewController(以下稱SecondViewController)的過程中,Cell中的圖片變換到了第二個ViewController中圖片的位置。其次是一些界面上透明度的變化。
可以總結一句就是說
Cell.imageView.frame -> SecondViewController.imageView.frame
FirstViewController.view.alpha = 0 SecondViewController.alpha = 1
的一個過程。
動畫執行的關鍵就是我們缺少一個容器去處理這樣視圖位置及透明度的變化。
好在func animateTransition(_ transitionContext: UIViewControllerContextTransitioning)中的transitionContext中有一個ContainView可以在我們進行轉場的時候做容器使用。
截取了一部分代碼:
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) as! DetailViewController
let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) as! ViewController
let containView = transitionContext.containerView()
按上述可以拿到容器View及轉場中的兩個視圖控制器。剩下要做的就是在ContainView中進行Frame等關鍵屬性的變化。
但是需要注意的一點是,我們怎么把Cell中的ImageView提出來然后移到下一個ViewController中?
這時就需要動畫中的一點障眼法,我們不需要移動Cell中的ImageView,移動會造成很多不必要的麻煩(還得移回去),但是我們可以給他做一個截圖,通過移動截圖來營造移動了Cell的ImageView的假象。
下面是截圖及初始化的主要部分代碼,思路參考注釋。
//獲取當前點擊的Cell
let indexPath = fromViewController.collectionView.indexPathsForSelectedItems()![0]
let cell = fromViewController.collectionView.cellForItemAtIndexPath(indexPath) as! CustomCollectionViewCell
//制作截圖
let snapShotView = cell.imageView .snapshotViewAfterScreenUpdates(false)
//注意添加的先后順序,否則會被遮擋。
containView?.addSubview((toViewController.view)!)
containView?.addSubview(snapShotView)
//截圖在ContainView中的初始位置調整
snapShotView.frame = (cell.convertRect(cell.imageView.frame, toView: cell.superview!.superview))
snapShotView.frame = CGRectMake(snapShotView.frame.origin.x, snapShotView.frame.origin.y + 64, snapShotView.frame.size.width, snapShotView.frame.size.height)
//對變換前后的原視圖進行隱藏
cell.imageView.hidden = true
toViewController.detailImageView.hidden = true
//設置第二個控制器位置,透明度
toViewController.view.frame = transitionContext .finalFrameForViewController(toViewController)
toViewController.view.alpha = 0
動畫的變化思路前面已經提過,下面貼上真正的動畫部分代碼:
//動畫
UIView.animateWithDuration(self.transitionDuration(transitionContext), delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 5, options: UIViewAnimationOptions.CurveLinear, animations: {
toViewController.view.alpha = 1
//假裝把Cell的ImageView移動到第二個VC中的ImageView的位置。
snapShotView.frame = (toViewController.view?.convertRect(toViewController.detailImageView.frame, toView: toViewController.view.superview))!
}) { (success) in
toViewController.detailImageView.hidden = false;
cell.imageView.hidden = false;
//刪除截圖
snapShotView.removeFromSuperview()
//完成動畫
transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
}
還有一個必須實現的方法是:
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval
用來決定動畫的執行時間
動畫的使用
轉場代碼都可以保持不變,該Push地方Push,該Pop的地方Pop
在FirstViewController中執行
self.navigationController?.delegate = self
當然FirstViewController需要遵守UINavigationControllerDelegate協議。
然后實現
optional public func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning?
具體如下:
//MARK: - Translation
func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
if toVC.isKindOfClass(DetailViewController)
//建議對第二個ViewController進行判斷,否則pop回來動畫混亂,如果有必要還可以對operation進行判斷(push or pop)
{
//創建一個你剛寫的類的對象即可。
let transition = MoveAnimation()
return transition
}
else{
return nil
}
}
以上就是Push部分動畫,Pop回來的時候原理是一樣的,只是ImageView的Frame變化反過來,alpha的變化也要反過來,所以可以另寫一個Pop的動畫類。
貼一個完成的與上對應的Pop動畫部分:
import UIKit
class MoveInverseTransition: NSObject,UIViewControllerAnimatedTransitioning {
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 0.6
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) as! DetailViewController
let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) as! ViewController
let containView = transitionContext.containerView()
//獲取當前點擊的Cell
let indexPath = toViewController.collectionView.indexPathsForSelectedItems()![0]
let cell = toViewController.collectionView.cellForItemAtIndexPath(indexPath) as! CustomCollectionViewCell
let snapShotView = fromViewController.detailImageView .snapshotViewAfterScreenUpdates(false)
containView?.addSubview((toViewController.view)!)
containView?.addSubview(snapShotView)
snapShotView.frame = (fromViewController.view.convertRect(fromViewController.detailImageView.frame, toView:fromViewController.view.superview))
cell.imageView.hidden = true
//設置第二個控制器位置,透明度
toViewController.view.frame = transitionContext .finalFrameForViewController(toViewController)
toViewController.view.alpha = 0
//動畫
UIView.animateWithDuration(self.transitionDuration(transitionContext), delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 5, options: UIViewAnimationOptions.CurveLinear, animations: {
toViewController.view.alpha = 1
snapShotView.frame = (cell.convertRect(cell.imageView.frame, toView:cell.superview?.superview))
snapShotView.frame = CGRectMake(snapShotView.frame.origin.x, snapShotView.frame.origin.y + 64, snapShotView.frame.size.width, snapShotView.frame.size.height)
}){ (success) in
fromViewController.detailImageView.hidden = false;
cell.imageView.hidden = false;
snapShotView.removeFromSuperview()
transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
}
}
}
動畫的使用方式也與上相同。
手勢交互
之前介紹過UIPercentDrivenInteractiveTransition,我們的手勢交互就是主要建立在它所含的三個方法上。
思路:在動畫過渡過程中,通過手勢的滑動來計算出一個完成度的Progress(比較常見,比如手勢計算縮放比例),執行updateInteractiveTransition:progress 即可完成動畫按百分比顯示。
手勢的設定
func setupGestureRecognizer() {
//設置邊緣觸控手勢及其方位
let edgeGestureRecognizer = UIScreenEdgePanGestureRecognizer(target: self,action: #selector(DetailViewController.edgePanGesture(_:)))
edgeGestureRecognizer.edges = UIRectEdge.Left
self.view.addGestureRecognizer(edgeGestureRecognizer)
}
func edgePanGesture(gestureRecognizer:UIScreenEdgePanGestureRecognizer) {
//計算動畫完成度Progress
var progress = gestureRecognizer.translationInView(self.view).x/self.view.frame.size.width
progress = min(1.0, max(0.0, progress))
if gestureRecognizer.state == UIGestureRecognizerState.Began{
//手勢開始,執行Pop動作,觸發動畫
percentTransition = UIPercentDrivenInteractiveTransition()
self.navigationController?.popViewControllerAnimated(true)
}
else if gestureRecognizer.state == UIGestureRecognizerState.Changed{
//手勢執行過程中,不停通過progress去更新動畫狀態
percentTransition?.updateInteractiveTransition(progress)
}
else if gestureRecognizer.state == UIGestureRecognizerState.Cancelled || gestureRecognizer.state == UIGestureRecognizerState.Ended {
//手勢取消或者結束,判斷是否完成動畫或者取消。
if progress > 0.1
{
percentTransition?.finishInteractiveTransition()
}
else
{
percentTransition?.cancelInteractiveTransition()
}
}
}
交互動畫的使用(實現兩個NavigationController的代理方法):
func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
if toVC.isKindOfClass(ViewController)
{
return MoveInverseTransition()
}
else{
return nil
}
}
func navigationController(navigationController: UINavigationController, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
if animationController.isKindOfClass(MoveInverseTransition)
{
return percentTransition
}
else
{
return nil
}
}
以上內容都是在Kitten-Yang的文章基礎上作了一些自己的理解,這是一篇學習筆記。