iOS swift 自定义页面的切换动画与交互动画

参照:iOS 自定义页面的切换动画与交互动画 By Swift
oc 版切换动画

一、自定义导航栏的Push/Pop动画

为了在基于UINavigationController下做自定义的动画切换,
1、先建立一个简单的工程,建一个UINavigationController的子类LSNavigationController,另外两个VC viewControllersecondViewController,注意: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时返回已经实现了动画方法的类LSPushAnimationLSPopAnimation
viewControllerviewDidLoadself.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,这是一个特殊的容器,切换时的动画将在这个容器中进行;UITransitionContextFromViewControllerKeyUITransitionContextToViewControllerKey就是从哪个VC切换到哪个VC,容易理解;除此之外,还有直接获取view的UITransitionContextFromViewKeyUITransitionContextToViewKey等。
我按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

然后在LSNavigationControllerviewDidLoad里,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()
    }

其中LSPresentAnimationLSDismissAnimation也是实现了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、在 LSViewControllerself.transitioningDelegate = self(之所以让viewControllersecondViewcontroller继承LSViewController是因为懒,都写在一起了,不然需要在两个VC中实现)

这样present就会有动画,想要dismiss也实现我们自己的动画需要在viewControllerpresent按钮中将 secondViewController定位动画管理器。

以上简单demo可以点击下载

三、自定义导航栏的交互式动画

与动画控制器类似,我们把实现了*** 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 Controllerdelegate同一时间只能有一个。

首先我们需要创建一个交互控制器。新建一个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
            }
        }
  1. 手势开始后,我们初始化交互控制器self.interactivePopTransition,调整interactionInProgress的值并触发关闭视图控制器的操作。
  1. 手势进行时,我们不断调用update方法更新进度。它是UIPercentDrivenInteractiveTransition的一个方法,根据你传入的百分比值更新过渡动画。
  2. 如果手势被取消,更新interactionInProgress的值,并回滚过渡动画。
  3. 手势完成后,根据当前进度判断是取消还是完成过渡动画。

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
        }
    }

这样就完成了自定义交互控制器的全部内容。

自定义控制器的demo

注意,视图控制器的这些同样适用于model视图的动画(连接中包括三个工程,其中一个是图片浏览,swift代码还不太熟悉,需要改善)。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,277评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,777评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,946评论 0 245
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,271评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,636评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,767评论 1 221
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,989评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,733评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,457评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,674评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,155评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,518评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,160评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,114评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,898评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,822评论 2 280
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,705评论 2 273

推荐阅读更多精彩内容