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

在iOS7之前,开发者为了寻求自定义Navigation Controller的Push/Pop动画,只能受限于子类化一个UINavigationController,或是用自定义的动画去覆盖它。但是随着iOS7的到来,Apple针对开发者推出了新的工具,以更灵活地方式管理UIViewController切换。

我把最终的Demo稍做修改,算是找了一个合适的应用场景,另外配上几张美图,拉拉人气

虽然是Swift的Demo,但是转成Objective-C相当容易。

最终效果预览:

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

为了在基于UINavigationController下做自定义的动画切换,先建立一个简单的工程,这个工程的rootViewController是一个UINavigationController,UINavigationController的rootViewController是一个简单的UIViewController(称之为主页面),通过这个UIViewController上的一个Button能进入到下一个UIViewController中(称之为详情页面),我们先在主页面的ViewController上实现两个协议:UINavigationControllerDelegate和UIViewControllerAnimatedTransitioning,然后在ViewDidLoad里面把navigationController的delegate设为self,这样在导航栏Push和Pop的时候我们就知道了,然后用一个属性记下是Push还是Pop,就像这样:

func navigationController(navigationController: UINavigationController!, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController!, toViewController toVC: UIViewController!) -> UIViewControllerAnimatedTransitioning! {

    navigationOperation = operation

    return self

}

这是iOS7的新方法,这个方法需要你提供一个UIViewControllerAnimatedTransitioning,那UIViewControllerAnimatedTransitioning到底是什么呢?

UIViewControllerAnimatedTransitioning是苹果新增加的一个协议,其目的是在需要使用自定义动画的同时,又不影响视图的其他属性,让你把焦点集中在动画实现的本身上,然后通过在这个协议的回调里编写自定义的动画代码,即“切换中应该会发生什么”,负责切换的具体内容,任何实现了这一协议的对象被称之为动画控制器。你可以借助协议能被任何对象实现的这一特性,从而把各种动画效果封装到不同的类中,只要方便使用和管理,你可以发挥一切手段。我在这里让主页面实现动画控制器也是可以的,因为它是导航栏的rootViewController,会一直存在,我只要在里面编写自定义的Push和Pop动画代码就可以了:

//UIViewControllerTransitioningDelegate

func transitionDuration(transitionContext: UIViewControllerContextTransitioning!) -> NSTimeInterval {

    return 0.4

}

func animateTransition(transitionContext: UIViewControllerContextTransitioning!) {

    let containerView = transitionContext.containerView()

    let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)

    let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)

    

    var destView: UIView!

    var destTransform: CGAffineTransform!

    if navigationOperation == UINavigationControllerOperation.Push {

        containerView.insertSubview(toViewController.view, aboveSubview: fromViewController.view)

        destView = toViewController.view

        destView.transform = CGAffineTransformMakeScale(0.1, 0.1)

        destTransform = CGAffineTransformMakeScale(1, 1)

    } else if navigationOperation == UINavigationControllerOperation.Pop {

        containerView.insertSubview(toViewController.view, belowSubview: fromViewController.view)

        destView = fromViewController.view

        // 如果IDE是Xcode6 Beta4+iOS8SDK,那么在此处设置为0,动画将不会被执行(不确定是哪里的Bug)

        destTransform = CGAffineTransformMakeScale(0.1, 0.1)

    }

    UIView.animateWithDuration(transitionDuration(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会自动清理,所以我们无须在额外处理。

注意一点,这样一来会发现原来导航栏的交互式返回效果没有了,如果你想用原来的交互式返回效果的话,在返回动画控制器的delegate方法里返回nil,如:

if operation == UINavigationControllerOperation.Push {

    navigationOperation = operation

    return self

}

return nil

然后在viewDidLoad里,Objective-C直接self.navigationController.interactivePopGestureRecognizer.delegat = self就行了,Swift除了要navigationController.interactivePopGestureRecognizer.delegate = self之外,还要在self上声明实现了UIGestureRecognizerDelegate这个协议,虽然实际上你并没有实现。

一个简单的自定义导航栏Push/Pop动画就完成了。

自定义Modal的Present/Dismiss动画

自定义Modal的Present与Dismiss动画与之前类似,都需要提供一个动画管理器,我们用详情页面来展示一个Modal页面,详情页面就作为动画管理器:

func transitionDuration(transitionContext: UIViewControllerContextTransitioning!) -> NSTimeInterval {

    return 0.6

}

func animateTransition(transitionContext: UIViewControllerContextTransitioning!) {

    let containerView = transitionContext.containerView()

    

    let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)

    let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)

    

    var destView: UIView!

    var destTransfrom = CGAffineTransformIdentity

    let screenHeight = UIScreen.mainScreen().bounds.size.height

    

    if modalPresentingType == ModalPresentingType.Present {

        destView = toViewController.view

        destView.transform = CGAffineTransformMakeTranslation(0, screenHeight)

        containerView.addSubview(toViewController.view)

    } else if modalPresentingType == ModalPresentingType.Dismiss {

        destView = fromViewController.view

        destTransfrom = CGAffineTransformMakeTranslation(0, screenHeight)

        containerView.insertSubview(toViewController.view, belowSubview: fromViewController.view)

    }

    

    UIView.animateWithDuration(transitionDuration(transitionContext), delay: 0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0,

        options: UIViewAnimationOptions.CurveLinear, animations: {

            destView.transform = destTransfrom

        }, completion: {completed in

            transitionContext.completeTransition(true)

    })

}

动画部分用了一个iOS7的弹簧动画,usingSpringWithDamping的值设置得越小,弹的就越明显,动画的其他地方与之前类似,不一样的是之前主页面除了做动画管理器之外,还实现了UINavigationControllerDelegate协议,因为我们是自定义导航栏的动画,而在这里需要自定义Modal动画就要实现另一个协议:UIViewControllerTransitioningDelegate,这个协议与之前的UINavigationControllerDelegate协议具有相似性,都是返回一个动画管理器,iOS7的方法总共有四个,有两个交互式的先不管,我们只需要实现另两个即可:

func animationControllerForPresentedController(presented: UIViewController!, presentingController presenting: UIViewController!, sourceController source: UIViewController!) -> UIViewControllerAnimatedTransitioning! {

    modalPresentingType = ModalPresentingType.Present

    return self

}

func animationControllerForDismissedController(dismissed: UIViewController!) -> UIViewControllerAnimatedTransitioning! {

    modalPresentingType = ModalPresentingType.Dismiss

    return self

}

我同样的用一个属性记下是Present还是Dismiss,然后返回self。因为我是用的Storyboard,所以需要在prepareForSegue方法里设置一下transitionDelegate:

override func prepareForSegue(segue: UIStoryboardSegue!, sender: AnyObject!) {

    let modal = segue.destinationViewController as UIViewController

    modal.transitioningDelegate = self

}

对需要执行自定义动画的VC设置transitionDelegate属性即可。

如此一来,一个针对模态VC的自定义动画也完成了。

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

与动画控制器类似,我们把实现了UIViewControllerInteractiveTransitioning协议的对象称之为交互控制器,最常用的就是把交互控制器应用到导航栏的Back手势返回上,而如果要实现一个自定义的交互式动画,我们有两种方式来完成:实现一个交互控制器,或者使用iOS提供的UIPercentDrivenInteractiveTransition类作交互控制器。

使用UIPercentDrivenInteractiveTransition

我们这里就用UIPercentDrivenInteractiveTransition来完成导航栏的交互式动画。先看下UIPercentDrivenInteractiveTransition的定义:

实际上这个类就是实现了UIViewControllerInteractiveTransitioning协议的交互控制器,我们使用它就能够轻松地为动画控制器添加一个交互动画。调用updateInteractiveTransition:更新进度;调用cancelInteractiveTransition取消交互,返回到切换前的状态;调用finishInteractiveTransition通知上下文交互已完成,同completeTransition一样。我们把交互动画应用到详情页面Back回主页面的地方,由于之前的动画管理器的角色是主页面担任的,Navigation Controller的delegate同一时间只能有一个,那在这里交互控制器的角色也由主页面来担任。首先添加一个手势识别器:

let popRecognizer = UIScreenEdgePanGestureRecognizer(target: self, action: Selector("handlePopRecognizer:"))

popRecognizer.edges = UIRectEdge.Left

self.navigationController.view.addGestureRecognizer(popRecognizer)

UIScreenEdgePanGestureRecognizer继承于UIPanGestureRecognizer,能检测从屏幕边缘滑动的手势,设置edges为left检测左边即可。然后实现handlePopRecognizer:

func handlePopRecognizer(popRecognizer: UIScreenEdgePanGestureRecognizer) {

    var progress = popRecognizer.translationInView(navigationController.view).x / navigationController.view.bounds.size.width

    progress = min(1.0, max(0.0, progress))

    

    println("\(progress)")

    if popRecognizer.state == UIGestureRecognizerState.Began {

        println("Began")

        self.interactivePopTransition = UIPercentDrivenInteractiveTransition()

        self.navigationController.popViewControllerAnimated(true)

    } else if popRecognizer.state == UIGestureRecognizerState.Changed {

        self.interactivePopTransition?.updateInteractiveTransition(progress)

        println("Changed")

    } else if popRecognizer.state == UIGestureRecognizerState.Ended || popRecognizer.state == UIGestureRecognizerState.Cancelled {

        if progress > 0.5 {

            self.interactivePopTransition?.finishInteractiveTransition()

        } else {

            self.interactivePopTransition?.cancelInteractiveTransition()

        }

        println("Ended || Cancelled")

        self.interactivePopTransition = nil

    }

}

我用了一个实例变量引用UIPercentDrivenInteractiveTransition,这个类只在需要用时才创建,否则在正常Push/Pop的时候,即使只是点击操作并没有识别手势的情况下,也会进入交互(你也可以在要求你返回交互控制器时,进行一些判断,通过返回nil来屏蔽,但这显然就太麻烦了)。当手势识别的时候我们调用pop,用户手势发生变化时,调用update去更新,不管是end还是cancel,都判断下是进入下一个页面还是返回之前的页面,完成这一切后把交互控制器清理掉。

现在我们已经有了交互控制器对象,只需要把它给告知给Navigation Controller就行了,我们实现UINavigationControllerDelegate的另一个方法:

func navigationController(navigationController: UINavigationController!, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning!) -> UIViewControllerInteractiveTransitioning! {

    return self.interactivePopTransition

}

我们从详情页面通过自定义的交互动画返回到上一个页面的工作就完成了。

Demo效果预览:

使用UIPercentDrivenInteractiveTransition的Demo

自定义交互控制器

我在之前提过,UIPercentDrivenInteractiveTransition实际上就是实现了UIViewControllerInteractiveTransitioning协议,只要是实现了这个协议的对象就可以称之为交互控制器,我们如果想更加精确的管理动画以及深入理解处理上的细节,就需要自己实现UIViewControllerInteractiveTransitioning协议。

UIViewControllerInteractiveTransitioning协议总共有三个方法,其中startInteractiveTransition:是必须实现的方法,我们在里面初始化动画的状态:

func startInteractiveTransition(transitionContext: UIViewControllerContextTransitioning!) {

    self.transitionContext = transitionContext

    

    let containerView = transitionContext.containerView()

    let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)

    let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)

    

    containerView.insertSubview(toViewController.view, belowSubview: fromViewController.view)

    

    self.transitingView = fromViewController.view

}

这里不涉及动画,只是把需要切换的view添加到上下文环境中即可。动画部分我们还是和之前使用UIPercentDrivenInteractiveTransition的接口保持一致,添加几个方法:

func updateWithPercent(percent: CGFloat) {

    let scale = CGFloat(fabsf(Float(percent - CGFloat(1.0))))

    transitingView?.transform = CGAffineTransformMakeScale(scale, scale)

    transitionContext?.updateInteractiveTransition(percent)

}

func finishBy(cancelled: Bool) {

    if cancelled {

        UIView.animateWithDuration(0.4, animations: {

            self.transitingView!.transform = CGAffineTransformIdentity

            }, completion: {completed in

                self.transitionContext!.cancelInteractiveTransition()

                self.transitionContext!.completeTransition(false)

        })

    } else {

        UIView.animateWithDuration(0.4, animations: {

            print(self.transitingView)

            self.transitingView!.transform = CGAffineTransformMakeScale(0, 0)

            print(self.transitingView)

            }, completion: {completed in

                self.transitionContext!.finishInteractiveTransition()

                self.transitionContext!.completeTransition(true)

        })

    }

}

updateWithPercent:方法用来更新view的transform属性,finishBy:方法主要用来判断是进入下一个页面还是返回到之前的页面,并告知transitionContext目前的状态,以及对当前正在scale的view做最后的动画。这里的transitionContext和transitingView可以在前面的处理手势识别代码中取得,我将里面的代码更新了一下,变成下面这样:

func handlePopRecognizer(popRecognizer: UIScreenEdgePanGestureRecognizer) {

    var progress = popRecognizer.translationInView(navigationController.view).x / navigationController.view.bounds.size.width

    progress = min(1.0, max(0.0, progress))

    

    println("\(progress)")

    if popRecognizer.state == UIGestureRecognizerState.Began {

        println("Began")

        isTransiting = true

        //self.interactivePopTransition = UIPercentDrivenInteractiveTransition()

        self.navigationController.popViewControllerAnimated(true)

    } else if popRecognizer.state == UIGestureRecognizerState.Changed {

        //self.interactivePopTransition?.updateInteractiveTransition(progress)

        updateWithPercent(progress)

        println("Changed")

    } else if popRecognizer.state == UIGestureRecognizerState.Ended || popRecognizer.state == UIGestureRecognizerState.Cancelled {

        //if progress > 0.5 {

        //    self.interactivePopTransition?.finishInteractiveTransition()

        //} else {

        //    self.interactivePopTransition?.cancelInteractiveTransition()

        //}

        finishBy(progress < 0.5)

        println("Ended || Cancelled")

        isTransiting = false

        //self.interactivePopTransition = nil

    }

}

另外还用一个额外布尔值变量isTransiting来标识当前是否在手势识别中,这是为了在返回交互控制器的时候,不会在不当的时候返回self:

func navigationController(navigationController: UINavigationController!, interactionControllerForAnimationController animationController:

UIViewControllerAnimatedTransitioning!) -> UIViewControllerInteractiveTransitioning! {

    if !self.isTransiting {

        return nil

    }

    return self

}

这样一来就完成了自定义交互控制器。可以发现,基本流程与使用UIPercentDrivenInteractiveTransition是一致的,UIPercentDrivenInteractiveTransition主要是帮我们封装了transitionContext的初始化以及对它的调用等,只是动画部分需要我们在额外处理一下了。

使用自定义交互控制器的Demo

最终效果:

我在主页面上多放了几个带Image的Button,在点击Button时会将Button的Image传递到详情页面,详情页面相应的也有一个UIImageView用来显示。在主页面初始化动画状态的时候,会生成一个Image的快照来进行动画,要是在以前,我们只能通过UIGraphics的APIs进行一系列的操作,涉及视图的scale、旋转、透明及渲染到context等,但现在,我们只需要用iOS7的API就行了:

@availability(iOS, introduced=7.0)

func snapshotViewAfterScreenUpdates(afterUpdates: Bool) -> UIView

这个API能帮助我们快速获取一个视图的的快照,afterUpdates参数表示是否等所有效果应用到该视图之后再获取,如果设置为false,则立即获取;为true则会受到后面对该视图的影响。

在动画之前,把主页面和详情页面对应的Button和ImageView隐藏,然后对快照生成的View进行动画,动画用简单的frame隐式动画就可以了。

最终效果的Demo(上传到我的资源页面时总是失败,所以只能上传到GitHub上了)

最后附上一张图,这个图比较容易区分那几个名称相近的协议:

UPDATED:

GitHub上已更新至Xcode 6,主要是语法上的一些小调整

时间: 2024-10-28 18:52:09

iOS 自定义页面的切换动画与交互动画 By Swift的相关文章

交互动画设计:交互动画设计法则

文章描述:用户清楚这些常见的界面元素是如何工作的,他们不愿在每次点击按钮的时候,被迫花费多余的时间去看一些不必要的动画. 随着体验经济时代的到来,人们对产品界面的期待值也越来越高,交互动画在手机领域的应用已经非常普遍,例如iPhone充分发挥了动画在交互易用性方面的优势. 而交互动画会给用户带来一种舒适.自然和流畅的感觉. 交互动画逐渐成为了产品与用户之间的沟通方式,成为产品的肢体语言.尤其是目前高端机的性能提升迅速,也为交互动画的执行效率提供了有效的硬件保证. Google地图的让工具化与拟物

iOS自定义键盘切换效果_IOS

本文实例为大家分享了iOS自定义键盘切换的相关代码,供大家参考,具体内容如下 具体代码如下 - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. self.title = @"小飞哥键盘"; self.textField = [[UITextField alloc] initWithFrame:CGRectMa

应用-Android自定义页面跳转动画

问题描述 Android自定义页面跳转动画 <br> <item name="android:activityOpenEnterAnimation">@anim/move_right_in_activity</item><br> <item name="android:activityOpenExitAnimation">@anim/move_left_out_activity</item>&l

Android编程实现ViewPager多页面滑动切换及动画效果的方法_Android

本文实例讲述了Android编程实现ViewPager多页面滑动切换及动画效果的方法.分享给大家供大家参考,具体如下: 一.首先,我们来看一下效果图,这是新浪微博的Tab滑动效果.我们可以手势滑动,也可以点击上面的头标进行切换.与此同方式, 白色横条会移动到相应的页卡头标下.这是一个动画效果,白条是缓慢滑动过去的.好了,接下来我们就来实现它. 二.在开始前,我们先要认识一个控件,ViewPager.它是google SDk中自带的一个附加包的一个类,可以用来实现屏幕间的切换. 这个附加包是and

Android编程实现ViewPager多页面滑动切换及动画效果的方法

本文实例讲述了Android编程实现ViewPager多页面滑动切换及动画效果的方法.分享给大家供大家参考,具体如下: 一.首先,我们来看一下效果图,这是新浪微博的Tab滑动效果.我们可以手势滑动,也可以点击上面的头标进行切换.与此同方式, 白色横条会移动到相应的页卡头标下.这是一个动画效果,白条是缓慢滑动过去的.好了,接下来我们就来实现它. 二.在开始前,我们先要认识一个控件,ViewPager.它是google SDk中自带的一个附加包的一个类,可以用来实现屏幕间的切换. 这个附加包是and

产品的肢体语言–交互动画

随着体验经济时代的到来,人们对产品界面的期待值也越来越高,交互动画在手机领域的应用已经非常普遍,例如iPhone充分发挥了动画在交互易用性方面的优势. 而交互动画会给用户带来一种舒适.自然和流畅的感觉. 交互动画逐渐成为了产品与用户之间的沟通方式,成为产品的肢体语言.尤其是目前高端机的性能提升迅速,也为交互动画的执行效率提供了有效的硬件保证. Google地图的让工具化与拟物化结合,不论是翻起页面的效果,还是图钉的效果,都是还原真实场景中的操作,通过动画的表现,让其更加逼真.也使界面更加的有层次

如何从3个方面判断是否做交互动画

  IOS 7出来后,交互动画立刻成为设计师的抢手货,有关动效设计的教程都趁势火了一把,但是很少有人讨论到底该不该做交互动画,如果做,为什么?今天终于有同学分享了他的观点,一旦符合文中提到的3个方面,那死心塌地动手吧! @王笑Nothing :作为设计师,相比以前死板的体验,我们越来越看重APP的交互性.这种心态的转变很大程度上可以归因于IOS 7的大换脸.IOS 7过度动画强调物理变化.从这以后,我们可以看到有很多热门APP上都有非常出色的动效. 令人激动的动效很容易让人着迷,从而让用户忘记机

iOS自定义button抖动效果并实现右上角删除按钮_IOS

遇到过这种需求要做成类似与苹果删除软件时的动态效果. 1.长按抖动; 2.抖动时出现一个X; 3.点击x,删除button; 4.抖动时,点击按钮,停止抖动; 下面是我的设计思路: 1.继承UIButton: 2.给button在右上角添加一个按钮: 3.给button添加长按手势: 4.给button添加遮盖,抖动时可以拦截点击事件: 有更好的做法,还请斧正. // .m文件 #import "DZDeleteButton.h" #import "UIView+Extens

手把手教你改善界面交互动画

  本文将探究UI设计中动画效果的过度应用,通过对比早期的视觉设计,为UI动画的有效设计提供一些建议,另外附上实战案例,手把手教你改进文中案例的交互动画哟. 遗憾的是,这并非某个做作的反面案例--而是某个近期客户处拿来的实例. 简介 自70-80年代CRT屏幕上映第一幅光栅图形以来,人们对数字视觉设计的态度便不断进化.与其他艺术领域不同,数字设计的潮流始终随可用工具的进化而变化. 我们已经见证了设备显示能力的不断进步--从有限的CGA分辨率 (320 x 200) 到VGA(640 x 480)