参照:iOS 自定义页面的切换动画与交互动画 By Swift
oc 版切换动画
一、自定义导航栏的Push/Pop动画
为了在基于UINavigationController下做自定义的动画切换,
1、先建立一个简单的工程,建一个UINavigationController的子类LSNavigationController
,另外两个VC viewController
和secondViewController
,注意:viewController是一个UINavigationController。在这两个页面中先做一些准备工作就是各有一个按钮,一个做push操作,一个做pop操作。
2、LSNavigationController ,用来实现
UINavigationControllerDelegate```协议。在类中实现代理函数
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
}
上面的协议函数会在push和pop时返回已经实现了动画方法的类LSPushAnimation
和LSPopAnimation
。
在viewController
的viewDidLoad
中self.navigationController?.delegate = self
,因为导航器的第一个页面一直存在,所以只需要在这里设置就可。
3、编写动画类,由于pop和push实现方式类似,只拿push为例:
在LSPushAnimation
中实现UIViewControllerAnimatedTransitioning
协议。 UIViewControllerAnimatedTransitioning
是苹果新增加的一个协议,其目的是在需要使用自定义动画的同时,又不影响视图的其他属性,让你把焦点集中在动画实现的本身上,然后通过在这个协议的回调里编写自定义的动画代码,即“切换中应该会发生什么”,负责切换的具体内容,任何实现了这一协议的对象被称之为动画控制器。
实现两个协议函数
//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)
}))
}
上面第一个方法返回动画持续的时间,而下面这个方法才是具体需要实现动画的地方。UIViewControllerAnimatedTransitioning
的协议都包含一个对象:transitionContext
,通过这个对象能获取到切换时的上下文信息,比如从哪个VC切换到哪个VC等。我们从transitionContext
获取containerView
,这是一个特殊的容器,切换时的动画将在这个容器中进行;UITransitionContextFromViewControllerKey
和UITransitionContextToViewControllerKey
就是从哪个VC切换到哪个VC,容易理解;除此之外,还有直接获取view的UITransitionContextFromViewKey
和UITransitionContextToViewKey
等。
我按Push和Pop把动画简单的区分了一下,Push时scale由小变大,Pop时scale由大变小,不同的操作,toViewController
的视图层次也不一样。最后,在动画完成的时候调用completeTransition
,告诉transitionContext
你的动画已经结束,这是非常重要的方法,必须调用。在动画结束时没有对containerView的子视图进行清理(比如把fromViewController
的view移除掉)是因为transitionContext
会自动清理,所以我们无须在额外处理。
4、这样,只需在
注意一点,这样一来会发现原来导航栏的交互式返回效果没有了,如果你想用原来的交互式返回效果的话,在返回动画控制器的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上声明实现了UIGestureRecognizerDelegate
这个协议,虽然实际上你并没有实现。
一个简单的自定义导航栏Push/Pop动画就完成了。
二、自定义Modal的Present/Dismiss动画
自定义Modal的Present与Dismiss动画与之前类似,都需要提供一个动画管理器,我们用详情页面来展示一个Modal页面,详情页面就作为动画管理器:
为了方便,我依然在LSViewController
操作(实际开发中,只需要在有特殊需要的页面中实现即可),
1、 LSViewController
实现协议UIViewControllerTransitioningDelegate
,这个协议与之前的UINavigationControllerDelegate
协议具有相似性,都是返回一个动画管理器,
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return LSPresentAnimation.init()
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return LSDismissAnimation.init()
}
其中LSPresentAnimation
和LSDismissAnimation
也是实现了UIViewControllerAnimatedTransitioning
协议的用来实现具体的动画。直接上代码。
//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中实现)
这样present就会有动画,想要dismiss也实现我们自己的动画需要在viewController
present按钮中将 secondViewController
定位动画管理器。
三、自定义导航栏的交互式动画
与动画控制器类似,我们把实现了*** UIViewControllerInteractiveTransitioning
协议的对象称之为交互控制器***,最常用的就是把交互控制器应用到导航栏的Back手势返回上,而如果要实现一个自定义的交互式动画,我们有两种方式来完成:实现一个交互控制器,或者使用iOS提供的UIPercentDrivenInteractiveTransition
类作交互控制器。
使用
UIPercentDrivenInteractiveTransition
我们这里就用UIPercentDrivenInteractiveTransition来完成导航栏的交互式动画。先看下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()
}
实际上这个类就是实现了UIViewControllerInteractiveTransitioning
协议的交互控制器,我们使用它就能够轻松地为动画控制器添加一个交互动画。调用update
更新进度;调用cancel取消交互,返回到切换前的状态;调用finish通知上下文交互已完成,同completeTransition
一样。我们把交互动画应用到详情页面Back回主页面的地方,由于之前的动画管理器的角色是主页面担任的,Navigation Controller
的delegate
同一时间只能有一个。
首先我们需要创建一个交互控制器。新建一个Cocoa Touch Class
文件,命名为LSPercentDrivenInteractiveTransition
,让它继承自UIPercentDrivenInteractiveTransition
。
打开LSNavigationController.swift
,在类定义的最开始添加下面这些属性:
var interactionInProgress = false //用于指示交互是否在进行中。
///交互控制器
private var interactivePopTransition : LSPercentDrivenInteractiveTransition!
在viewDidLoad:中添加
self.delegate = self
let gesture = UIScreenEdgePanGestureRecognizer(target:self,action:#selector(handleGesture(gestureRecognizer:)))
gesture.edges = .left
self.view.addGestureRecognizer(gesture)
并实现手势的方法:
// 以下----使用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,调整interactionInProgress的值并触发关闭视图控制器的操作。
- 手势进行时,我们不断调用update方法更新进度。它是UIPercentDrivenInteractiveTransition的一个方法,根据你传入的百分比值更新过渡动画。
- 如果手势被取消,更新interactionInProgress的值,并回滚过渡动画。
- 手势完成后,根据当前进度判断是取消还是完成过渡动画。
在LSNavigationController
中实现UINavigationControllerDelegate
协议,
/// UINavigationControllerDelegate 以下两个协议均实现时,以第二个为准,
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
}
/// 当返回值为nil时,上面的协议返回的push和pop动画才会有作用
func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
if interactivePopTransition != nil {
return interactivePopTransition
}
return nil
}
这里的第一个方法前面已经用过,第二个是返回交互控制器,因为如果交互控制器不为空的话,就会调用控制器来控制交互,这样就使上面的push和pop失去的效果,所以只有在需要自定义交互控制器时才会返回,不然则返回nil即可(就像自定义滑动返回手势)。所以在上面的手势处理中才会在开始时初始化控制器,在结束后制为nil。
使用UIPercentDrivenInteractiveTransition的Demo
自定义交互控制器
在上面的demo基础上修改。
LSPercentDrivenInteractiveTransition
需要自己实现UIViewControllerInteractiveTransitioning
协议。
UIViewControllerInteractiveTransitioning
协议总共有三个方法,其中startInteractiveTransition:
是必须实现的方法,我们在里面初始化动画的状态:
///以下是自定义交互控制器
///先初始化需要的变量
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:方法主要用来判断是进入下一个页面还是返回到之前的页面,并告知transitionContext目前的状态,以及对当前正在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
}
}
这样就完成了自定义交互控制器的全部内容。
注意,视图控制器的这些同样适用于model视图的动画(连接中包括三个工程,其中一个是图片浏览,swift代码还不太熟悉,需要改善)。
网友评论