UIViewControllerAnimatedTransitioning
在UINavigationController中如何使用UIViewControllerAnimatedTransitioning?
参考:How to use UIViewControllerAnimatedTransitioning with UINavigationController?
1.动画的FromViewController要遵循UINavigationControllerDelegate协议。
2.FromViewController中,在如下的代理方法中返回自定义的转场动画对象:
- (id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC {
TransitionAnimator *animator = [TransitionAnimator new];
animator.presenting = (operation == UINavigationControllerOperationPush);
return animator;
}
3.创建自定义的动画类,需遵循UIViewControllerContextTransitioning协议,并实现如下的代理方法:
- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext {
//返回动画执行的时间
return 0.5f;
}
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
// Grab the from and to view controllers from the context
UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
// Set our ending frame. We'll modify this later if we have to
CGRect endFrame = CGRectMake(80, 280, 160, 100);
if (self.presenting) {
fromViewController.view.userInteractionEnabled = NO;
[transitionContext.containerView addSubview:fromViewController.view];
[transitionContext.containerView addSubview:toViewController.view];
CGRect startFrame = endFrame;
startFrame.origin.x += 320;
toViewController.view.frame = startFrame;
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
fromViewController.view.tintAdjustmentMode = UIViewTintAdjustmentModeDimmed;
toViewController.view.frame = endFrame;
} completion:^(BOOL finished) {
[transitionContext completeTransition:YES];
}];
}
else {
toViewController.view.userInteractionEnabled = YES;
[transitionContext.containerView addSubview:toViewController.view];
[transitionContext.containerView addSubview:fromViewController.view];
endFrame.origin.x += 320;
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
toViewController.view.tintAdjustmentMode = UIViewTintAdjustmentModeAutomatic;
fromViewController.view.frame = endFrame;
} completion:^(BOOL finished) {
[transitionContext completeTransition:YES];
}];
}
}
关于UIViewControllerAnimatedTransitioning的资料可参考:
- 轻松学习之二——iOS利用Runtime自定义控制器POP手势动画
- iOS 7 新特性:视图控制器切换API
- ViewController的高级转场动画
- View Controller 转场
- iOS 7学习:定制View Controllers之间的切换动画
以下内容来自
Introduction to Custom View Controller Transitions and Animations
创建自定义的transition,按如下的3步:
- Create a class that implements the UIViewControllerAnimatedTransitioning protocol. Here you will write code that performs the animation. This class is referred to as the animation controller.
- Before presenting a view controller, set a class as its transitioning delegate. The delegate will get a callback for the animation controller to be used when presenting the view controller.
- Implement the callback method to return an instance of the animation controller from the first step.
自定义Present过渡(Custom Present Transition)
首先创建一个animation controller。如下,创建一个CustomPresentAnimationController 类继承自NSObject
class CustomPresentAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
UIViewControllerAnimatedTransitioning协议有两个必须实现的方法。
func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {
return 2.5
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!
let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!
let finalFrameForVC = transitionContext.finalFrameForViewController(toViewController)
let containerView = transitionContext.containerView()
let bounds = UIScreen.mainScreen().bounds
toViewController.view.frame = CGRectOffset(finalFrameForVC, 0, bounds.size.height)
containerView.addSubview(toViewController.view)
UIView.animateWithDuration(transitionDuration(transitionContext), delay: 0.0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.0, options: .CurveLinear, animations: {
fromViewController.view.alpha = 0.5
toViewController.view.frame = finalFrameForVC
}, completion: {
finished in
transitionContext.completeTransition(true)
fromViewController.view.alpha = 1.0
})
}
当前的控制器要遵循UIViewControllerTransitioningDelegate协议
class ItemsTableViewController: UITableViewController, UIViewControllerTransitioningDelegate {
UIViewController有一个transitionDelegate属性,支持自定义的transitions。当一个View controller 过渡到另一个View controller的时候,系统会检查这个属性来决定是否使用自定义的transition。由UIViewControllerTransitioningDelegate来提供这个自定义的transition。
let customPresentAnimationController = CustomPresentAnimationController()
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "showAction" {
let toViewController = segue.destinationViewController as UIViewController
toViewController.transitioningDelegate = self
}
}
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return customPresentAnimationController
}
如果想要一个不同的效果,可以把CustomPresentAnimationController.Swift中的代码:
toViewController.view.frame = CGRectOffset(finalFrameForVC, 0, bounds.size.height)
改为如下的形式,把View controller的frame的原点位置改在屏幕的上面:
toViewController.view.frame = CGRectOffset(finalFrameForVC, 0, -bounds.size.height)
自定义Dismiss过渡(Custom Dismiss Transition)
UIViewControllerTransitioningDelegate 也允许指定一个animation controller来dismiss一个view controller。
创建一个CustomDismissAnimationController类,继承自NSObject。如下:
class CustomDismissAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
在这个类中添加如下的代码:
func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {
return 2
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!
let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!
let finalFrameForVC = transitionContext.finalFrameForViewController(toViewController)
let containerView = transitionContext.containerView()
toViewController.view.frame = finalFrameForVC
toViewController.view.alpha = 0.5
containerView.addSubview(toViewController.view)
containerView.sendSubviewToBack(toViewController.view)
UIView.animateWithDuration(transitionDuration(transitionContext), animations: {
fromViewController.view.frame = CGRectInset(fromViewController.view.frame, fromViewController.view.frame.size.width / 2, fromViewController.view.frame.size.height / 2)
toViewController.view.alpha = 1.0
}, completion: {
finished in
transitionContext.completeTransition(true)
})
}
在ItemsTableViewController中加入如下的属性:
let customDismissAnimationController = CustomDismissAnimationController()
并在类中添加如下的方法:
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return customDismissAnimationController
}
UIViewControllerTransitioningDelegate 协议提供了如上的方法,这个方法用来获取dismiss view controller的animation controller。
运行的效果如下:
动画效果并不是我们想要的。原因是改变view的frame并不会对其子view有影响。可以使用UIView的snapshotting来修改它。
UIView的snapshotting是UIView的截屏,使其成为轻量级的UIView。我们将会在动画过程中使用这个截屏,而不是真正的view。
把animateTransition()函数替换成如下形式:
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!
let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!
let finalFrameForVC = transitionContext.finalFrameForViewController(toViewController)
let containerView = transitionContext.containerView()
toViewController.view.frame = finalFrameForVC
toViewController.view.alpha = 0.5
containerView.addSubview(toViewController.view)
containerView.sendSubviewToBack(toViewController.view)
let snapshotView = fromViewController.view.snapshotViewAfterScreenUpdates(false)
snapshotView.frame = fromViewController.view.frame
containerView.addSubview(snapshotView)
fromViewController.view.removeFromSuperview()
UIView.animateWithDuration(transitionDuration(transitionContext), animations: {
snapshotView.frame = CGRectInset(fromViewController.view.frame, fromViewController.view.frame.size.width / 2, fromViewController.view.frame.size.height / 2)
toViewController.view.alpha = 1.0
}, completion: {
finished in
snapshotView.removeFromSuperview()
transitionContext.completeTransition(true)
})
}
导航控制器过渡(Navigation controller transitions)
自定义CustomNavigationAnimationController类继承自NSObject,如下:
class CustomNavigationAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
把这个类中添加如下的内容。给这个animation controller使用一个简单的立方体动画。使用了一个reverse变量,用来决定动画的方向。
var reverse: Bool = false
func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {
return 1.5
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView()
let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!
let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!
let toView = toViewController.view
let fromView = fromViewController.view
let direction: CGFloat = reverse ? -1 : 1
let const: CGFloat = -0.005
toView.layer.anchorPoint = CGPointMake(direction == 1 ? 0 : 1, 0.5)
fromView.layer.anchorPoint = CGPointMake(direction == 1 ? 1 : 0, 0.5)
var viewFromTransform: CATransform3D = CATransform3DMakeRotation(direction * CGFloat(M_PI_2), 0.0, 1.0, 0.0)
var viewToTransform: CATransform3D = CATransform3DMakeRotation(-direction * CGFloat(M_PI_2), 0.0, 1.0, 0.0)
viewFromTransform.m34 = const
viewToTransform.m34 = const
containerView.transform = CGAffineTransformMakeTranslation(direction * containerView.frame.size.width / 2.0, 0)
toView.layer.transform = viewToTransform
containerView.addSubview(toView)
UIView.animateWithDuration(transitionDuration(transitionContext), animations: {
containerView.transform = CGAffineTransformMakeTranslation(-direction * containerView.frame.size.width / 2.0, 0)
fromView.layer.transform = viewFromTransform
toView.layer.transform = CATransform3DIdentity
}, completion: {
finished in
containerView.transform = CGAffineTransformIdentity
fromView.layer.transform = CATransform3DIdentity
toView.layer.transform = CATransform3DIdentity
fromView.layer.anchorPoint = CGPointMake(0.5, 0.5)
toView.layer.anchorPoint = CGPointMake(0.5, 0.5)
if (transitionContext.transitionWasCancelled()) {
toView.removeFromSuperview()
} else {
fromView.removeFromSuperview()
}
transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
})
}
打开ItemsTableViewController.swift,修改类的声明如下:
class ItemsTableViewController: UITableViewController, UIViewControllerTransitioningDelegate, UINavigationControllerDelegate {
由UINavigationControllerDelegate 来提供animation controller。
添加如下的属性到类中。
let customNavigationAnimationController = CustomNavigationAnimationController()
在viewDidLoad()中添加如下的代码:
navigationController?.delegate = self
在类中添加如下的方法:
func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
customNavigationAnimationController.reverse = operation == .Pop
return customNavigationAnimationController
}
Making it Interactive
iOS内置的app有这个特征
我们需要一个interaction controller。interaction controller使用UIViewControllerInteractiveTransitioning协议。navigation controller delegate或者transitioning delegate,在requesting一个animation controller之后还会requests 一个可选的 interaction controller。
来创建interaction controller。创建一个新的类叫做CustomInteractionController,继承自UIPercentDrivenInteractiveTransition。
UIPercentDrivenInteractiveTransition实现了UIViewControllerInteractiveTransitioning协议。
要想使用UIPercentDrivenInteractiveTransition,你的animation controller必须有一个UIView动画,这样这个动画可以被停止、恢复和播放。
在这个类中加入如下的代码:
var navigationController: UINavigationController!
var shouldCompleteTransition = false
var transitionInProgress = false
var completionSeed: CGFloat {
return 1 - percentComplete
}
func attachToViewController(viewController: UIViewController) {
navigationController = viewController.navigationController
setupGestureRecognizer(viewController.view)
}
private func setupGestureRecognizer(view: UIView) {
view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: "handlePanGesture:"))
}
func handlePanGesture(gestureRecognizer: UIPanGestureRecognizer) {
let viewTranslation = gestureRecognizer.translationInView(gestureRecognizer.view!.superview!)
switch gestureRecognizer.state {
case .Began:
transitionInProgress = true
navigationController.popViewControllerAnimated(true)
case .Changed:
var const = CGFloat(fminf(fmaxf(Float(viewTranslation.x / 200.0), 0.0), 1.0))
shouldCompleteTransition = const > 0.5
updateInteractiveTransition(const)
case .Cancelled, .Ended:
transitionInProgress = false
if !shouldCompleteTransition || gestureRecognizer.state == .Cancelled {
cancelInteractiveTransition()
} else {
finishInteractiveTransition()
}
default:
println("Swift switch must be exhaustive, thus the default")
}
}
使用interaction controller,在ItemsTableViewController.swift,加入如下的属性:
let customInteractionController = CustomInteractionController()
在navigationController(_:animationControllerForOperation:
fromViewController:toViewController:)方法的开始位置,加入如下的代码:
if operation == .Push {
customInteractionController.attachToViewController(toVC)
}
然后再加入如下的方法:
func navigationController(navigationController: UINavigationController, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return customInteractionController.transitionInProgress ? customInteractionController : nil
}
download the completed project here
其它参考文章:
- How To Make A View Controller Transition Animation Like in the Ping App
- CREATING A CUSTOM FLIP VIEW CONTROLLER TRANSITION
- INTERACTIVE TRANSITIONS
- iOS7 interactive transitions
- Custom UIViewController Transitions
另推荐一个开源工具,可以很好的在项目中实现滑动返回:
FDFullscreenPopGesture
根据以上的例子,我自己写了个交互式的滑动返回的例子。主要的界面如下:
文章介绍如下:[一个丝滑的全屏滑动返回手势)(http://blog.sunnyxx.com/2015/06/07/fullscreen-pop-gesture/)
定义两个动画类,PushAnimation用来实现Push动画,PopAnimation用来实现PopAnimation,都继承自NSObject,并遵守UIViewControllerAnimatedTransitioning协议。代码如下:
PushAnimation:
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext
{
return 0.3;
}
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIView *containerView = [transitionContext containerView];
[containerView addSubview:toVC.view];
[containerView addSubview:fromVC.view];
toVC.view.alpha = 0.5f;
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
fromVC.view.transform = CGAffineTransformMakeTranslation(-fromVC.view.bounds.size.width, 0);
toVC.view.alpha = 1.0;
} completion:^(BOOL finished) {
fromVC.view.transform = CGAffineTransformIdentity;
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
}
PopAnimation:
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext
{
return 0.3;
}
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
[[transitionContext containerView] addSubview:toVC.view];
[[transitionContext containerView] addSubview:fromVC.view];
toVC.view.transform = CGAffineTransformMakeTranslation(-100, 0);
toVC.view.alpha = 0.5;
[UIView animateWithDuration:[self transitionDuration:transitionContext]
animations:^{
fromVC.view.transform = CGAffineTransformMakeTranslation(CGRectGetWidth(fromVC.view.frame), 0);
toVC.view.alpha = 1.0;
toVC.view.transform = CGAffineTransformIdentity;
}
completion:^(BOOL finished) {
fromVC.view.transform = CGAffineTransformIdentity;
toVC.view.transform = CGAffineTransformIdentity;
if ([transitionContext transitionWasCancelled]) {
}
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
}
要在UINavgationController中实现交互式的滑动返回,需要给UINavgationController的delegate指定对象。自定义个NavigationPerformer类,继承自NSObject,并实现UINavigationControllerDelegate协议。完成代码如下:
@class PushAnimation;
@class PopAnimation;
@interface NavigationPerformer : NSObject<UINavigationControllerDelegate>
@property (weak, nonatomic) IBOutlet UINavigationController *navigationController;
@property (strong, nonatomic) PushAnimation *pushAnimation;
@property (strong, nonatomic) PopAnimation *popAnimation;
@property (strong, nonatomic) UIPercentDrivenInteractiveTransition *interactionController;
@end
@interface NavigationPerformer ()
@property (nonatomic, assign) BOOL shouldCompleteTransition;
@end
@implementation NavigationPerformer
- (void)awakeFromNib
{
UIPanGestureRecognizer* panRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)];
[self.navigationController.view addGestureRecognizer:panRecognizer];
self.pushAnimation = [[PushAnimation alloc] init];
self.popAnimation = [[PopAnimation alloc] init];
}
- (void)pan:(UIPanGestureRecognizer *)pan
{
UIView *view = self.navigationController.view;
if (pan.state == UIGestureRecognizerStateBegan) {
if (self.navigationController.viewControllers.count > 1) {
self.interactionController = [[UIPercentDrivenInteractiveTransition alloc] init];
[self.navigationController popViewControllerAnimated:YES];
}
}else if(pan.state == UIGestureRecognizerStateChanged){
CGPoint translation = [pan translationInView:view];
CGFloat progress = translation.x / CGRectGetWidth(view.bounds);
progress = MIN(MAX(progress, 0.0), 1.0);
_shouldCompleteTransition = progress > 0.3;
[self.interactionController updateInteractiveTransition:progress];
}else if(pan.state == UIGestureRecognizerStateEnded || pan.state == UIGestureRecognizerStateCancelled){
if (!_shouldCompleteTransition || pan.state == UIGestureRecognizerStateCancelled) {
[self.interactionController cancelInteractiveTransition];
}else{
[self.interactionController finishInteractiveTransition];
}
self.interactionController = nil;
}
}
- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC
{
if (operation == UINavigationControllerOperationPush) {
return self.pushAnimation;
}else if(operation == UINavigationControllerOperationPop){
return self.popAnimation;
}else{
return nil;
}
}
- (id<UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransitioning>)animationController
{
return self.interactionController;
}
@end
在代码中声明了一个navigationController,用来对当前的UINavigationController保持引用。在Storyboard中给UINavigationController的delegate指定对象。
在UINavigationController中加入一个object对象,并在其Indentity Inspector中指定Class为NavigationPerformer,如下:
指定object对象为UINavigationController的delegate。
大概的效果如下: