效果展示

Swift版抽屉效果,自定义转场动画管理器_转场动画


iOS7.0加入了自定义转场动画,淘汰了之前左右两大隐藏护法的抽屉效果,并且一些浮窗、弹层都可以用vc来显示了,不再是用view盖在window上

看了一些抽屉Demo发觉都是OC写的,本篇使用Swift4.0编写一个纯正Swift版转场动画管理器,其中用到元组,OC混编可能需要改为字典…目前已经支持oc混编


自定义转场动画协议

1、UINavigationControllerDelegate

push和pop转场动画协议,主要用到的方法有两个

optional public func navigationController(_ 
navigationController: UINavigationController,
animationControllerFor operation: UINavigationControllerOperation,
from fromVC: UIViewController,
to toVC: UIViewController)
-> UIViewControllerAnimatedTransitioning?

参数navigationController当前执行动画的导航栏

operation用来判断push还是pop来实现不同动画

fromVC和toVC,A push B,A是fromVC,B是toVC,B pop,B是fromVC,A是toVC,顾名思义没什么好说的

最后返回需要执行的动画

optional public func navigationController(_ 
navigationController: UINavigationController,
interactionControllerFor animationController: UIViewControllerAnimatedTransitioning)
-> UIViewControllerInteractiveTransitioning?

这个方法是用来支持手势驱动的,第一个参数同上,animationController当前执行的动画,返回UIViewControllerInteractiveTransitioning用来控制手势交互的对象


2、UIViewControllerTransitioningDelegate

present和dismiss转场动画,与push和pop的区别在于没有operation来区分是present和dismiss,而是分为两个方法,需要各自去实现

optional public func animationController(forPresented 
presented: UIViewController,
presenting: UIViewController,
source: UIViewController)
-> UIViewControllerAnimatedTransitioning?

presented参数是被present的vc

source是调用present的vc

presenting是根控制器,举个例子A present B,A又是TabBarController中的一个vc,那presented就是B,source是A,presenting就是TabBarController,此时B present C,B没有TabBarController,所以presented是C,source和presenting就同源都是B

//dismiss同理,没什么好说的
optional public func animationController(forDismissed
dismissed: UIViewController)
-> UIViewControllerAnimatedTransitioning?
//手势驱动动画,和navigation动画一样
optional public func interactionControllerForPresentation(using
animator: UIViewControllerAnimatedTransitioning)
-> UIViewControllerInteractiveTransitioning?

optional public func interactionControllerForDismissal(using
animator: UIViewControllerAnimatedTransitioning)
-> UIViewControllerInteractiveTransitioning?



3、UIViewControllerAnimatedTransitioning

转场动画的实现,想怎么酷炫就靠他了

//动画时长
public func transitionDuration(using
transitionContext: UIViewControllerContextTransitioning?)
-> TimeInterval
//执行动画的方法,根据transitionContext上下文能获取fromVC和toVC
public func animateTransition(using
transitionContext: UIViewControllerContextTransitioning)



4、UIViewControllerContextTransitioning

动画上下文协议

//通过key获取fromVC和toVC
guard let fromVC = transitionContext.viewController(forKey: .from) else {
return
}
guard let toVC = transitionContext.viewController(forKey: .to) else {
return
}

//发生转场动画的视图,add顺序可根据自己业务和动画实现方式变更
let contentView = transitionContext.containerView
contentView.addSubview(toVC.view)
contentView.addSubview(fromVC.view)

//整个转场过渡必须调用的完成方法,不然contentView不会销毁
//通常更具上下文的transitionWasCancelled判断是取消动画还是动画完成
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)

5、UIPercentDrivenInteractiveTransition

手势百分比协议

//关键的三个方法,update根据百分比播放动画进度
open func update(_ percentComplete: CGFloat)
open func cancel()
open func finish()

LVAniamtor

看过一些转场三方实现,通常只实现了push或pop,要么就只封装了present和dismiss,能不能两个都封装在一起统一接口调用呢?试一下吧,就做成了这样子…


使用方式

1、初始化得到动画对象

let animator = LVAnimator()

这个对象相当于动画管理控制器,接收push和present的事件,可根据fromVC和toVC来分配对应的转场动画


2、push转场简单使用

animator.setup { (fromVC, toVC, operation) -> Dictionary<String, Any>? in
//动画时长,自定义动画
return ["duration" : "1", "delegate" : YourPushAnimation()]
}

如不需要自定转场动画,返回nil即可


3、viewWillAppear注册代理

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
animator.registerDelegate(vc: self)
}

4、present动画需特别处理

//present的vc
let vc = LVMineVC()
//present转场比较特殊,需将跳转的vc代理指向当前动画对象
animator.registerDelegate(vc: vc)
present(vc, animated: true)

遇到的坑

起初是从push和pop动画开始封装的,包括手势控制动画一切顺利,但是加入present和dismiss后一切就不对了…  present转场需要把目标vc的transitioningDelegate对象指向当前对象,所以就有了以下代码

let vc = LVMineVC()
animator.registerDelegate(vc: vc)
present(vc, animated: true)

另外一个问题是封装present和dismiss后,手势驱动出了问题,push和pop手势动画时,松手动画会平滑过渡结束,而present和dismiss则是一闪而过直接跳到结束状态,没有中间平滑过渡的动画了…

这怎么办…拆开分为两套方法不是最初的意愿,就一个字… 正面刚!

最后网上也看了一些资料,用了CADisplayLink解决了

func startLink() {
if link == nil {
link = CADisplayLink(target: self, selector: #selector(LVTransitioningDelegateHelper.linkUpdate))
link?.add(to: RunLoop.current, forMode: .commonModes)
}
}

func stopLink() {
link?.invalidate()
link = nil
}

@objc func linkUpdate() {
progress += rate
if progress >= 0.98 {
stopLink()
interactive?.finish()
interactive = nil
} else {
interactive?.update(progress)
}
}

原理就是当手势取消或结束时,使用CADisplayLink补过缺失的过渡动画

case .cancelled, .ended:
if progress > 0.4 {
startLink()
} else {
interactive?.cancel()
interactive = nil
}

另外我想present A用A动画,B用B动画,C用系统的怎么办…

//注意block用要用weak,因为互相包含了
weak var weakSelf = self
animator.setup(panGestureVC: self, transitionAction: {
weakSelf?.myAction()
}) { (fromVC, toVC, operation) -> Dictionary<String, Any>? in
switch operation {
case .present:
if toVC is A {
return ["duration" : "0.4", "delegate" : APresentAnimation()]
} else if toVC is B {
return ["duration" : "0.4", "delegate" : BPresentAnimation()]
} else if toVC is C {
return nil
}
default: break
}
return nil
}

更新

2018.9.11 元组改成Dictionary,setup方法支持oc混编

最后

Github:https:///grvlv/LVAnimator

有什么写的不对的地方,或者有更好的优化方式可以留言,共同学习进步。




Swift版抽屉效果,自定义转场动画管理器_转场动画_02