iOS自定义转场动画

发表于 2016-05-24 21:23 显示全部楼层 16 3402

在iOS开发中转场动画几乎每个app都会用到.在iOS默认系统提供了2中方式: push&present,本文简单的介绍一下如何进行自定义转场动画.

我们在测试的界面上添加一个UIButton,并监听Button的点击事件.执行以下代码,从一个Storyboard加载一个控制器,并执行present动作:

let sb = UIStoryboard(name: "Test", bundle: nil)  
let vc = sb.instantiateInitialViewController()!  
presentViewController(vc, animated: true, completion: nil)

blob.png

我们切换到视图调试中看下~可以看到,视图的层级关系,当新的视图从屏幕底下出来完全覆盖原来的视图之后,而原来的视图已经不在界面上了.而像一些用户体验度比较高的应用上那些炫酷的转场的动画是如果实现的呢.

圆归正传,首先我们要自定义转场动画必须遵守UIViewControllerTransitioningDelegate,UIViewControllerAnimatedTransitioning代理协议,并实现

optional public func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning?
 
optional public func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning?
 
optional public func presentationControllerForPresentedViewController(presented: UIViewController, presentingViewController presenting: UIViewController, sourceViewController source: UIViewController) -> UIPresentationController?
 
public func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval
 
public func animateTransition(transitionContext: UIViewControllerContextTransitioning)


上面代理方法有什么作用,我们等下一一介绍, 修改上面的代码为

let sb = UIStoryboard(name: "Test", bundle: nil)  
let vc = sb.instantiateInitialViewController()!  
vc.transitioningDelegate = self  
vc.modalPresentationStyle = UIModalPresentationStyle.Custom  
presentViewController(vc, animated: true, completion: nil)


并实现代理方法

// MARK: - 自定义转场相关
/// 记录当前是否是展现
var isPresent = false  
extension ViewController: UIViewControllerTransitioningDelegate{  
    /**
     用于返回一个控制弹出控制器的对象
 
     - parameter presented:  被展现的控制器
     - parameter presenting: 发起弹出控制器, Xcode6是nil, Xcode7野指针
 
     - returns: 用于控制弹出视图的对象
     */
    func presentationControllerForPresentedViewController(presented: UIViewController, presentingViewController presenting: UIViewController, sourceViewController source: UIViewController) -> UIPresentationController?{
 
        //此处返回一个UIPresentationController,所以我们创建一个继承于UIPresentationController的文件,因为我们需要重写UIPresentationController中的方法来修改视图的大小
        return HBPresentationController(presentedViewController: presented, presentingViewController: presenting)
    }
 
    //用于控制控制器如何弹出
    func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning?{
        isPresent = true
        return self
    }
 
    //用于控制控制器如何销毁
    func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning?{
        isPresent = false
        return self
    }
}
 
// MARK: - 自定义转场控制如何显示和显示代理方法
extension HomeTableViewController: UIViewControllerAnimatedTransitioning{  
    /**
     用于返回动画时长
     */
    func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval{
        return 0.5
    }
 
    /**
     用于控制弹出的控制器如何显示和显示
     无论是展现还是消失都会调用该方法
     注意点: 只要实现了该方法, 那么系统就不会再做任何操作, 也就是说被modal的控制器视图如何添加, 如何动画等等, 系统都不会帮我们做了
     - parameter transitionContext: 上下文(上下文中包含了我们需要的所有参数)
     */
    func animateTransition(transitionContext: UIViewControllerContextTransitioning){
        // 1.判断当前是展现还是消失
        if isPresent {
            // 展现
            willPresentedViewController(transitionContext)
        }else{
           // 消失
            willDismissedController(transitionContext)
        }
    }
 
    /**
     负责如何展现
     */
    private func willPresentedViewController(transitionContext: UIViewControllerContextTransitioning){
        // 1.拿到展现的视图
        // 结论: 如果是展现, 那么toVc就是菜单
        guard let toView = transitionContext.viewForKey(UITransitionContextToViewKey) else{
            return
        }
 
        // 2.手动添加菜单到容器视图上
        transitionContext.containerView()?.addSubview(toView)
 
        // 3.执行动画
        toView.layer.anchorPoint = CGPoint(x: 0.5, y: 0.0)
        toView.transform = CGAffineTransformMakeScale(1.0, 0.0)
        UIView.animateWithDuration(transitionDuration(transitionContext), animations: { () -> Void in
            toView.transform = CGAffineTransformIdentity
            }) { (_) -> Void in
                // 注意: 自定义转场, 当动画完成之后一定要告诉系统动画已经完成, 否则会出现奇葩BUG
                transitionContext.completeTransition(true)
        }
    }
 
    /**
     负责如何消失
     */
    private func willDismissedController(transitionContext: UIViewControllerContextTransitioning){
        // 1.拿到消失视图
        guard let fromView = transitionContext.viewForKey(UITransitionContextFromViewKey) else{
            return
        }
        // 2.执行动画
        UIView.animateWithDuration(transitionDuration(transitionContext), animations: { () -> Void in
            // 当前出现直接消失的BUG是因为CGFloat不准确
            fromView.transform = CGAffineTransformMakeScale(1.0, 0.000001)
            }) { (_) -> Void in
                transitionContext.completeTransition(true)
        }
    }
}


HBPresentationController中的代码如下

import UIKit  
// 一个控制弹出控制器的对象
class HBPresentationController: UIPresentationController {
 
     /**
     该方法专门用于布局弹出控制器的视图
     所有弹出控制器的视图都是放在容器视图上的
     */
    override func containerViewWillLayoutSubviews(){
//        containerView属性, 容器视图, 所以界面都放在容器视图上
//        presentedView()方法, 可以获取被展现的视图, 可以获取弹出控制器的view
 
        // 1.修改弹出控制器view的尺寸
        let bounds = UIScreen.mainScreen().bounds
        presentedView()?.frame = CGRectMake(0, bounds.size.height / 2, bounds.size.width, bounds.size.height / 2)
 
        // 2.添加子控件
        containerView?.insertSubview(coverButton, atIndex: 0)
    }
 
    // MARK: - 内部控制方法
    @objc private func coverBtnClick(){
        presentedViewController.dismissViewControllerAnimated(true, completion: nil)
    }
 
    // MARK: - 懒加载
    private lazy var coverButton: UIButton = {
       let btn = UIButton()
        btn.frame = UIScreen.mainScreen().bounds
        btn.backgroundColor = UIColor(white: 0.5, alpha: 0.5)
        btn.addTarget(self, action: Selector("coverBtnClick"), forControlEvents: UIControlEvents.TouchUpInside)
        return btn
    }()
}

至此我们已经完成一个简单的自定义转场动画.

封装

虽然实现了转场动画,但是我们的所有代码还是放在了控制器里.这里进行一下抽取,新建一个类HBPresentationManager继承NSObject, UIViewControllerTransitioningDelegate,把自定义的一些信息放到HBPresentationManager,代码如下

import UIKit
 
class HBPresentationManager: NSObject, UIViewControllerTransitioningDelegate{  
    /// 记录当前是否是展现
    var isPresent = false
 
 
    /**
     用于返回一个控制弹出控制器的对象
 
     - parameter presented:  被展现的控制器
     - parameter presenting: 发起弹出控制器, Xcode6是nil, Xcode7野指针
 
     - returns: 用于控制弹出视图的对象
     */
    func presentationControllerForPresentedViewController(presented: UIViewController, presentingViewController presenting: UIViewController, sourceViewController source: UIViewController) -> UIPresentationController?{
        let pc = HBPresentationController(presentedViewController: presented, presentingViewController: presenting)
        return pc
    }
 
    /**
     用于控制控制器如何弹出
     */
    func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning?{
        isPresent = true
        return self
    }
 
    /**
     用于控制控制器如何消失
     */
    func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning?{
        isPresent = false
        return self
    }
}
 
 
// MARK: - 自定义转场控制如何显示和显示代理方法
extension HBPresentationManager: UIViewControllerAnimatedTransitioning{  
    /**
     用于返回动画时长
     */
    func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval{
        return 1.0
    }
 
    /**
     用于控制弹出的控制器如何显示和显示
     无论是展现还是消失都会调用该方法
     注意点: 只要实现了该方法, 那么系统就不会再做任何操作, 也就是说被modal的控制器视图如何添加, 如何动画等等, 系统都不会帮我们做了
     - parameter transitionContext: 上下文(上下文中包含了我们需要的所有参数)
     */
    func animateTransition(transitionContext: UIViewControllerContextTransitioning){
        // 1.判断当前是展现还是消失
        if isPresent{
            // 展现
            willPresentedViewController(transitionContext)
        }else{
            // 消失
            willDismissedController(transitionContext)
        }
    }
 
    /**
     负责如何展现
     */
    private func willPresentedViewController(transitionContext: UIViewControllerContextTransitioning) {
        // 1.拿到展现的视图
        // 结论: 如果是展现, 那么toVc就是菜单
        guard let toView = transitionContext.viewForKey(UITransitionContextToViewKey) else{
            return
        }
 
        // 2.手动添加菜单到容器视图上
        transitionContext.containerView()?.addSubview(toView)
 
        // 3.执行动画
        toView.layer.anchorPoint = CGPoint(x: 0.5, y: 0.0)
        toView.transform = CGAffineTransformMakeScale(1.0, 0.0)
 
        UIView.animateWithDuration(transitionDuration(transitionContext), animations: { () -> Void in
            toView.transform = CGAffineTransformIdentity
            }) { (_) -> Void in
                // 注意: 自定义转场, 当动画完成之后一定要告诉系统动画已经完成, 否则会出现奇葩BUG
                transitionContext.completeTransition(true)
        }
    }
 
    /**
     负责如何消失
     */
    private func willDismissedController(transitionContext: UIViewControllerContextTransitioning){
        // 1.拿到消失视图
        guard let fromView = transitionContext.viewForKey(UITransitionContextFromViewKey) else{
            return
        }
        // 2.执行动画
        UIView.animateWithDuration(transitionDuration(transitionContext), animations: { () -> Void in
            // 当前出现直接消失的BUG是因为CGFloat不准确
            fromView.transform = CGAffineTransformMakeScale(1.0, 0.000001)
            }) { (_) -> Void in
                transitionContext.completeTransition(true)
        }
    }
}
这个时候控制器里不需要的代码删掉后就非常简洁了
class ViewController: UIViewController {
 
    override func viewDidLoad() {
        super.viewDidLoad()
 
    }
 
    @IBAction func test(sender: AnyObject) {
        let sb = UIStoryboard(name: "Test", bundle: nil)
        let vc = sb.instantiateInitialViewController()!
        vc.transitioningDelegate = manager
        vc.modalPresentationStyle = UIModalPresentationStyle.Custom
        presentViewController(vc, animated: true, completion: nil)
 
    }
 
    private lazy var manager = HBPresentationManager()
}

搞定收工

示例Demo:https://github.com/liuhongbao/TransitioningDemo


回复 使用道具
举报
周牧

发表于 2017-02-17 00:20 显示全部楼层

回复 支持 反对 使用道具
举报
森之树

发表于 2017-02-17 00:17 显示全部楼层

观后纵想法再多,也不如一句回复实在!

回复 支持 反对 使用道具
举报
paschen

发表于 2017-02-17 00:16 显示全部楼层

作为新人,在吧里不敢大声说话,也不敢得罪人,只能默默地顶完贴转身就走,不求深藏功与名,只求前排混脸熟

回复 支持 反对 使用道具
举报
liquanhai

发表于 2017-02-16 20:04 显示全部楼层

观后纵想法再多,也不如一句回复实在!

回复 支持 反对 使用道具
举报
沟里郭嘉身似椅

发表于 2017-02-16 19:29 显示全部楼层

回复 支持 反对 使用道具
举报
Viper

发表于 2017-02-16 19:07 显示全部楼层

路过,支持一下

回复 支持 反对 使用道具
举报
炒菜不加盐

发表于 2017-02-16 12:57 显示全部楼层

回复 支持 反对 使用道具
举报
NeoCheung

发表于 2017-02-16 09:54 显示全部楼层

回复 支持 反对 使用道具
举报
sup

发表于 2017-02-15 19:55 显示全部楼层

回复 支持 反对 使用道具
举报
12下一页

发表新文章
刘洪宝老师

叩丁狼教育

0

学分

136

学币

146

积分

叩丁狼教育

Rank: 5Rank: 5

积分
146

叩丁狼一周年勋章前100注册用户勋章

Ta的主页 发消息
精华帖排行榜

精彩推荐

  • 关注叩丁狼教育