Swift版抽屉效果,自定义转场动画管理器
效果展示
imageiOS7.0加入了自定义转场动画,淘汰了之前左右两大隐藏护法的抽屉效果,并且一些浮窗、弹层都可以用vc来显示了,不再是用view盖在window上
看了一些抽屉Demo发觉都是OC写的,本篇使用Swift4.0编写一个纯正Swift版转场动画管理器,其中用到元组,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(vc: self) { (fromVC, toVC, operation) -> ((duration: TimeInterval, delegate: LVTransitionAnimationDelegate)?) in
//动画时长,自定义动画
return (1, 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(vc: self, openEdgePan: true, transitionAction: {
weakSelf?.enterMine()
}) { (fromVC, toVC, operation) -> ((duration: TimeInterval, delegate: LVTransitionAnimationDelegate)?) in
switch operation {
case .present:
if toVC is A {
return (0.4, APresentAnimation())
} else if toVC is B {
return (0.4, BPresentAnimation())
} else if toVC is C {
return nil
}
default: break
}
return nil
}
最后
Demo下载地址 https://github.com/grvlv/LVAnimator
有什么写的不对的地方,或者又更好的优化实现方式可以留言,共同学习进步,有问题也可以加QQ:1254129365交流学习,谢谢~