參照:iOS 自定義頁面的切換動畫與交互動畫 By Swift
oc 版切換動畫
一酱讶、自定義導(dǎo)航欄的Push/Pop動畫
為了在基于UINavigationController下做自定義的動畫切換句惯,
1秕衙、先建立一個簡單的工程搀玖,建一個UINavigationController的子類LSNavigationController
藏斩,另外兩個VC viewController
和secondViewController
,注意:viewController是一個UINavigationController。在這兩個頁面中先做一些準(zhǔn)備工作就是各有一個按鈕栅干,一個做push操作,一個做pop操作捐祠。
2非驮、LSNavigationController ,用來實現(xiàn)
UINavigationControllerDelegate```協(xié)議雏赦。在類中實現(xiàn)代理函數(shù)
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
if operation == UINavigationControllerOperation.push {
return LSPushAnimation.init()
}else if operation == UINavigationControllerOperation.pop {
return LSPopAnimation.init()
}
return nil
}
上面的協(xié)議函數(shù)會在push和pop時返回已經(jīng)實現(xiàn)了動畫方法的類LSPushAnimation
和LSPopAnimation
。
在viewController
的viewDidLoad
中self.navigationController?.delegate = self
芙扎,因為導(dǎo)航器的第一個頁面一直存在星岗,所以只需要在這里設(shè)置就可。
3戒洼、編寫動畫類俏橘,由于pop和push實現(xiàn)方式類似,只拿push為例:
在LSPushAnimation
中實現(xiàn)UIViewControllerAnimatedTransitioning
協(xié)議圈浇。 UIViewControllerAnimatedTransitioning
是蘋果新增加的一個協(xié)議寥掐,其目的是在需要使用自定義動畫的同時,又不影響視圖的其他屬性磷蜀,讓你把焦點集中在動畫實現(xiàn)的本身上召耘,然后通過在這個協(xié)議的回調(diào)里編寫自定義的動畫代碼,即“切換中應(yīng)該會發(fā)生什么”褐隆,負(fù)責(zé)切換的具體內(nèi)容污它,任何實現(xiàn)了這一協(xié)議的對象被稱之為動畫控制器。
實現(xiàn)兩個協(xié)議函數(shù)
//UIViewControllerAnimatedTransitioning
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.4
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
let fromViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)
var destView: UIView!
var destTransform: CGAffineTransform!
containerView.insertSubview((toViewController?.view)!, aboveSubview: (fromViewController?.view)!)
destView = toViewController?.view
destView.transform = CGAffineTransform(scaleX: 0.1, y: 0.1)
destTransform = CGAffineTransform(scaleX: 1, y: 1)
UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
destView.transform = destTransform
}, completion: ({completed in
transitionContext.completeTransition(true)
}))
}
上面第一個方法返回動畫持續(xù)的時間庶弃,而下面這個方法才是具體需要實現(xiàn)動畫的地方衫贬。UIViewControllerAnimatedTransitioning
的協(xié)議都包含一個對象:transitionContext
,通過這個對象能獲取到切換時的上下文信息歇攻,比如從哪個VC切換到哪個VC等固惯。我們從transitionContext
獲取containerView
,這是一個特殊的容器缴守,切換時的動畫將在這個容器中進(jìn)行葬毫;UITransitionContextFromViewControllerKey
和UITransitionContextToViewControllerKey
就是從哪個VC切換到哪個VC,容易理解屡穗;除此之外供常,還有直接獲取view的UITransitionContextFromViewKey
和UITransitionContextToViewKey
等。
我按Push和Pop把動畫簡單的區(qū)分了一下鸡捐,Push時scale由小變大栈暇,Pop時scale由大變小,不同的操作箍镜,toViewController
的視圖層次也不一樣源祈。最后煎源,在動畫完成的時候調(diào)用completeTransition
,告訴transitionContext
你的動畫已經(jīng)結(jié)束香缺,這是非常重要的方法手销,必須調(diào)用。在動畫結(jié)束時沒有對containerView的子視圖進(jìn)行清理(比如把fromViewController
的view移除掉)是因為transitionContext
會自動清理图张,所以我們無須在額外處理锋拖。
4、這樣祸轮,只需在
注意一點兽埃,這樣一來會發(fā)現(xiàn)原來導(dǎo)航欄的交互式返回效果沒有了,如果你想用原來的交互式返回效果的話适袜,在返回動畫控制器的delegate方法里返回nil柄错,如:
if operation == UINavigationControllerOperation.Push {
navigationOperation = operation
return self
}
return nil
然后在LSNavigationController
的viewDidLoad
里,Objective-C
直接self.navigationController.interactivePopGestureRecognizer.delegat = self
就行了苦酱,Swift除了要navigationController.interactivePopGestureRecognizer.delegate = self
之外售貌,還要在self上聲明實現(xiàn)了UIGestureRecognizerDelegate
這個協(xié)議,雖然實際上你并沒有實現(xiàn)疫萤。
一個簡單的自定義導(dǎo)航欄Push/Pop動畫就完成了颂跨。
二、自定義Modal的Present/Dismiss動畫
自定義Modal的Present與Dismiss動畫與之前類似扯饶,都需要提供一個動畫管理器毫捣,我們用詳情頁面來展示一個Modal頁面,詳情頁面就作為動畫管理器:
為了方便帝际,我依然在LSViewController
操作(實際開發(fā)中蔓同,只需要在有特殊需要的頁面中實現(xiàn)即可),
1蹲诀、 LSViewController
實現(xiàn)協(xié)議UIViewControllerTransitioningDelegate
,這個協(xié)議與之前的UINavigationControllerDelegate
協(xié)議具有相似性斑粱,都是返回一個動畫管理器,
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return LSPresentAnimation.init()
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return LSDismissAnimation.init()
}
其中LSPresentAnimation
和LSDismissAnimation
也是實現(xiàn)了UIViewControllerAnimatedTransitioning
協(xié)議的用來實現(xiàn)具體的動畫脯爪。直接上代碼则北。
//UIViewControllerAnimatedTransitioning
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.6
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
let fromViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)
var destView: UIView!
var destTransfrom = CGAffineTransform()
let screenHeight = UIScreen.main
.bounds.size.height
destView = toViewController?.view
destView.transform = CGAffineTransform(translationX: 0, y: screenHeight)
containerView.addSubview((toViewController?.view)!)
UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0,
options: UIViewAnimationOptions.curveLinear, animations: {
destView.transform = destTransfrom
}, completion: {completed in
transitionContext.completeTransition(true)
})
}
3、在 LSViewController
中self.transitioningDelegate = self
(之所以讓viewController
和secondViewcontroller
繼承LSViewController
是因為懶痕慢,都寫在一起了尚揣,不然需要在兩個VC中實現(xiàn))
這樣present就會有動畫,想要dismiss也實現(xiàn)我們自己的動畫需要在viewController
present按鈕中將 secondViewController
定位動畫管理器掖举。
三快骗、自定義導(dǎo)航欄的交互式動畫
與動畫控制器類似,我們把實現(xiàn)了*** UIViewControllerInteractiveTransitioning
協(xié)議的對象稱之為交互控制器***,最常用的就是把交互控制器應(yīng)用到導(dǎo)航欄的Back手勢返回上方篮,而如果要實現(xiàn)一個自定義的交互式動畫名秀,我們有兩種方式來完成:實現(xiàn)一個交互控制器,或者使用iOS提供的UIPercentDrivenInteractiveTransition
類作交互控制器藕溅。
使用
UIPercentDrivenInteractiveTransition
我們這里就用UIPercentDrivenInteractiveTransition來完成導(dǎo)航欄的交互式動畫匕得。先看下UIPercentDrivenInteractiveTransition的定義:
open class UIPercentDrivenInteractiveTransition : NSObject, UIViewControllerInteractiveTransitioning {
/// This is the non-interactive duration that was returned when the
/// animators transitionDuration: method was called when the transition started.
open var duration: CGFloat { get }
/// The last percentComplete value specified by updateInteractiveTransition:
open var percentComplete: CGFloat { get }
/// completionSpeed defaults to 1.0 which corresponds to a completion duration of
/// (1 - percentComplete)*duration. It must be greater than 0.0. The actual
/// completion is inversely proportional to the completionSpeed. This can be set
/// before cancelInteractiveTransition or finishInteractiveTransition is called
/// in order to speed up or slow down the non interactive part of the
/// transition.
open var completionSpeed: CGFloat
/// When the interactive part of the transition has completed, this property can
/// be set to indicate a different animation curve. It defaults to UIViewAnimationCurveEaseInOut.
/// Note that during the interactive portion of the animation the timing curve is linear.
open var completionCurve: UIViewAnimationCurve
/// For an interruptible animator, one can specify a different timing curve provider to use when the
/// transition is continued. This property is ignored if the animated transitioning object does not
/// vend an interruptible animator.
@available(iOS 10.0, *)
open var timingCurve: UITimingCurveProvider?
/// Set this to NO in order to start an interruptible transition non
/// interactively. By default this is YES, which is consistent with the behavior
/// before 10.0.
@available(iOS 10.0, *)
open var wantsInteractiveStart: Bool
/// Use this method to pause a running interruptible animator. This will ensure that all blocks
/// provided by a transition coordinator's notifyWhenInteractionChangesUsingBlock: method
/// are executed when a transition moves in and out of an interactive mode.
@available(iOS 10.0, *)
open func pause()
// These methods should be called by the gesture recognizer or some other logic
// to drive the interaction. This style of interaction controller should only be
// used with an animator that implements a CA style transition in the animator's
// animateTransition: method. If this type of interaction controller is
// specified, the animateTransition: method must ensure to call the
// UIViewControllerTransitionParameters completeTransition: method. The other
// interactive methods on UIViewControllerContextTransitioning should NOT be
// called. If there is an interruptible animator, these methods will either scrub or continue
// the transition in the forward or reverse directions.
open func update(_ percentComplete: CGFloat)
open func cancel()
open func finish()
}
實際上這個類就是實現(xiàn)了UIViewControllerInteractiveTransitioning
協(xié)議的交互控制器,我們使用它就能夠輕松地為動畫控制器添加一個交互動畫巾表。調(diào)用update
更新進(jìn)度汁掠;調(diào)用cancel取消交互,返回到切換前的狀態(tài)集币;調(diào)用finish通知上下文交互已完成考阱,同completeTransition
一樣。我們把交互動畫應(yīng)用到詳情頁面Back回主頁面的地方惠猿,由于之前的動畫管理器的角色是主頁面擔(dān)任的,Navigation Controller
的delegate
同一時間只能有一個负间。
首先我們需要創(chuàng)建一個交互控制器偶妖。新建一個Cocoa Touch Class
文件,命名為LSPercentDrivenInteractiveTransition
政溃,讓它繼承自UIPercentDrivenInteractiveTransition
趾访。
打開LSNavigationController.swift
,在類定義的最開始添加下面這些屬性:
var interactionInProgress = false //用于指示交互是否在進(jìn)行中董虱。
///交互控制器
private var interactivePopTransition : LSPercentDrivenInteractiveTransition!
在viewDidLoad:中添加
self.delegate = self
let gesture = UIScreenEdgePanGestureRecognizer(target:self,action:#selector(handleGesture(gestureRecognizer:)))
gesture.edges = .left
self.view.addGestureRecognizer(gesture)
并實現(xiàn)手勢的方法:
// 以下----使用UIPercentDrivenInteractiveTransition交互控制器
func handleGesture(gestureRecognizer: UIScreenEdgePanGestureRecognizer) {
var progress = gestureRecognizer.translation(in: gestureRecognizer.view?.superview).x / self.view.bounds.size.width
progress = min(1.0, max(0.0, progress))
// let translation = gestureRecognizer.translation(in: gestureRecognizer.view?.superview)
// var progress = Float(translation.x / 200)
// progress = fminf(fmaxf(progress, 0.0), 1.0)
print("\(progress)")
if gestureRecognizer.state == UIGestureRecognizerState.began {
print("Began")
self.interactivePopTransition = LSPercentDrivenInteractiveTransition()
interactionInProgress = true
self.popViewController(animated: true)
} else if gestureRecognizer.state == UIGestureRecognizerState.changed {
self.interactivePopTransition.update(CGFloat(progress))
print("Changed")
} else if gestureRecognizer.state == UIGestureRecognizerState.ended || gestureRecognizer.state == UIGestureRecognizerState.cancelled {
if progress > 0.5 {
self.interactivePopTransition.finish()
print("finished")
} else {
self.interactivePopTransition.cancel()
print("canceled")
}
interactionInProgress = false
self.interactivePopTransition = nil
}
}
- 手勢開始后扼鞋,我們初始化交互控制器self.interactivePopTransition,調(diào)整interactionInProgress的值并觸發(fā)關(guān)閉視圖控制器的操作愤诱。
- 手勢進(jìn)行時云头,我們不斷調(diào)用update方法更新進(jìn)度。它是UIPercentDrivenInteractiveTransition的一個方法淫半,根據(jù)你傳入的百分比值更新過渡動畫溃槐。
- 如果手勢被取消,更新interactionInProgress的值科吭,并回滾過渡動畫昏滴。
- 手勢完成后,根據(jù)當(dāng)前進(jìn)度判斷是取消還是完成過渡動畫对人。
在LSNavigationController
中實現(xiàn)UINavigationControllerDelegate
協(xié)議谣殊,
/// UINavigationControllerDelegate 以下兩個協(xié)議均實現(xiàn)時,以第二個為準(zhǔn)牺弄,
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
if operation == UINavigationControllerOperation.push {
return LSPushAnimation.init()
}else if operation == UINavigationControllerOperation.pop {
return LSPopAnimation.init()
}
return nil
}
/// 當(dāng)返回值為nil時姻几,上面的協(xié)議返回的push和pop動畫才會有作用
func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
if interactivePopTransition != nil {
return interactivePopTransition
}
return nil
}
這里的第一個方法前面已經(jīng)用過,第二個是返回交互控制器,因為如果交互控制器不為空的話鲜棠,就會調(diào)用控制器來控制交互肌厨,這樣就使上面的push和pop失去的效果,所以只有在需要自定義交互控制器時才會返回豁陆,不然則返回nil即可(就像自定義滑動返回手勢)柑爸。所以在上面的手勢處理中才會在開始時初始化控制器,在結(jié)束后制為nil盒音。
使用UIPercentDrivenInteractiveTransition的Demo
自定義交互控制器
在上面的demo基礎(chǔ)上修改表鳍。
LSPercentDrivenInteractiveTransition
需要自己實現(xiàn)UIViewControllerInteractiveTransitioning
協(xié)議。
UIViewControllerInteractiveTransitioning
協(xié)議總共有三個方法祥诽,其中startInteractiveTransition:
是必須實現(xiàn)的方法譬圣,我們在里面初始化動畫的狀態(tài):
///以下是自定義交互控制器
///先初始化需要的變量
var transitionContext : UIViewControllerContextTransitioning!
var transitingView : UIView!
/// 以下----自定義交互控制器
override func startInteractiveTransition(_ transitionContext: UIViewControllerContextTransitioning) {
self.transitionContext = transitionContext
let containerView = transitionContext.containerView
let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
let fromViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)
containerView.insertSubview((toViewController?.view)!, belowSubview: (fromViewController?.view)!)
self.transitingView = fromViewController?.view
}
override func update(_ percentComplete: CGFloat) {
let scale = CGFloat(fabsf(Float(percentComplete - CGFloat(1.0))))
transitingView?.transform = CGAffineTransform(scaleX: scale, y: scale)
transitionContext?.updateInteractiveTransition(percentComplete)
}
func finishBy(cancelled: Bool) {
if cancelled {
UIView.animate(withDuration: 0.4, animations: {
self.transitingView!.transform = CGAffineTransform(scaleX: 1, y: 1)
}, completion: {completed in
self.transitionContext!.cancelInteractiveTransition()
self.transitionContext!.completeTransition(false)
})
} else {
UIView.animate(withDuration: 0.4, animations: {
print(self.transitingView)
self.transitingView!.transform = CGAffineTransform(scaleX: 0.1, y: 0.1)
print(self.transitingView)
}, completion: {completed in
self.transitionContext!.finishInteractiveTransition()
self.transitionContext!.completeTransition(true)
})
}
}
update:方法用來更新view的transform屬性,finishBy:方法主要用來判斷是進(jìn)入下一個頁面還是返回到之前的頁面雄坪,并告知transitionContext目前的狀態(tài)厘熟,以及對當(dāng)前正在scale的view做最后的動畫。這里的transitionContext和transitingView可以在前面的處理手勢識別代碼中取得维哈,因此手勢的處理中變成了:
///以下是自定義交互器
func handleGesture(gestureRecognizer: UIScreenEdgePanGestureRecognizer) {
var progress = gestureRecognizer.translation(in: gestureRecognizer.view?.superview).x / self.view.bounds.size.width
progress = min(1.0, max(0.0, progress))
// let translation = gestureRecognizer.translation(in: gestureRecognizer.view?.superview)
// var progress = Float(translation.x / 200)
// progress = fminf(fmaxf(progress, 0.0), 1.0)
print("\(progress)")
if gestureRecognizer.state == UIGestureRecognizerState.began {
print("Began")
self.interactivePopTransition = LSPercentDrivenInteractiveTransition()
interactionInProgress = true
self.popViewController(animated: true)
} else if gestureRecognizer.state == UIGestureRecognizerState.changed {
interactivePopTransition.update(progress)
print("Changed")
} else if gestureRecognizer.state == UIGestureRecognizerState.ended || gestureRecognizer.state == UIGestureRecognizerState.cancelled {
interactivePopTransition.finishBy(cancelled: progress < 0.5)
interactionInProgress = false
self.interactivePopTransition = nil
}
}
這樣就完成了自定義交互控制器的全部內(nèi)容绳姨。
注意,視圖控制器的這些同樣適用于model視圖的動畫(連接中包括三個工程阔挠,其中一個是圖片瀏覽飘庄,swift代碼還不太熟悉,需要改善)购撼。