iOS开发-转场动画切换界面(类似系统动画)
在开发中,无论我们使用 push 还是 present 推出新的 viewcontroller 时,系统为了提高用户体验都会为我们默认加上一些过渡动画。但是开发中需要自定义过度动画效果。这里就需要用到了转场动画了。
如图所示
一、转场动画相关协议
自定义转场动画时需要使用的协议:
- UIViewControllerAnimatedTransitioning: 实现此协议的实例控制转场动画效果。
- UIViewControllerInteractiveTransitioning: 实现此协议的实例控制着利用手势过渡时的进度处理。
二、实现转场动画代码
2.1、实现UIViewControllerAnimatedTransitioning协议
实现转场动画时候,实现UIViewControllerAnimatedTransitioning协议
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;
- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext;
在区分Push与Pop操作时候,需要用到以下的
UIView *containerView = [transitionContext containerView];
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIView *fromView = fromVC.view;
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *toView = toVC.view;
注意:在转场过程中,转场过渡的容器,如果是PUSH时候,A->B是,fromVC是A,toVC是B;如果是POP的时候,fromVC是B,toVC是A。
动画实现
在转场过程中更改fromView与toView的toFrame,动画结束后调用transitionContext的completeTransition方法。
具体代码如下
INPushPopTranstioning.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "INPercentDrivenInteractiveTransition.h"
@interface INPushPopTranstioning : NSObject<UIViewControllerAnimatedTransitioning, UINavigationControllerDelegate>
// 是否是Push
@property (nonatomic, assign) NSTimeInterval transitionDuration;
@property (nonatomic, strong) INPercentDrivenInteractiveTransition *interactiveTransition;
+ (instancetype)sharedInstance;
@end
INPushPopTranstioning.m
#import "INPushPopTranstioning.h"
#define kINScreenWidth [[UIScreen mainScreen] bounds].size.width
static INPushPopTranstioning *_sharedInstance = nil;
@interface INPushPopTranstioning ()
// 是否是Push
@property (nonatomic, assign) BOOL isPush;
@end
@implementation INPushPopTranstioning
+ (instancetype)sharedInstance {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_sharedInstance = [[INPushPopTranstioning alloc] init];
_sharedInstance.transitionDuration = 0.35;
_sharedInstance.interactiveTransition = [[INPercentDrivenInteractiveTransition alloc] init];
});
return _sharedInstance;
}
- (nullable id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>) animationController {
return self.interactiveTransition.isInteractive?self.interactiveTransition:nil;
}
- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext {
return self.transitionDuration;
}
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
// 转场过渡的容器 如果是PUSH时候,A->B是,fromVC是A,toVC是B;如果是POP的时候,fromVC是B,toVC是A。
// Transition container
UIView *containerView = [transitionContext containerView];
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIView *fromView = fromVC.view;
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *toView = toVC.view;
UIImage *fromImage = [self screenShotImage:fromView];
UIImageView *backView = [[UIImageView alloc] initWithFrame:CGRectZero];
//backView.image = fromImage;
backView.backgroundColor = [UIColor blackColor];
backView.frame = containerView.bounds;
UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
UIVisualEffectView *effectView = [[UIVisualEffectView alloc] initWithEffect:blurEffect];
effectView.frame = backView.bounds;
//[backView addSubview:effectView];
UIImageView *viewMaskView = [[UIImageView alloc] initWithFrame:CGRectZero];
viewMaskView.backgroundColor = [UIColor blackColor];
viewMaskView.frame = containerView.bounds;
// 判断是 push 还是 pop 操作
// Determine if it is a push or pop operation
if (self.isPush) {
//[containerView addSubview:backView];
[containerView addSubview:fromView];
[containerView addSubview:viewMaskView];
[containerView addSubview:toView];
} else {
//[containerView addSubview:backView];
[containerView addSubview:toView];
[containerView addSubview:viewMaskView];
[containerView addSubview:fromView];
}
CGRect fromFrame;
CGRect toFrame;
CGFloat beginAlpha = 0.0;
if (self.isPush) {
fromFrame = containerView.bounds;
fromView.frame = fromFrame;
toFrame = CGRectMake(containerView.bounds.size.width, 0, containerView.bounds.size.width, containerView.bounds.size.height);
toView.frame = toFrame;
beginAlpha = 0.0;
} else {
fromFrame = containerView.bounds;
fromView.frame = fromFrame;
toFrame = CGRectMake(containerView.bounds.size.width, 0, containerView.bounds.size.width, containerView.bounds.size.height);
toView.frame = fromFrame;
beginAlpha = 0.3;
}
viewMaskView.alpha = beginAlpha;
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
if (self.isPush) {
//fromView.transform = CGAffineTransformMakeScale(0.9, 0.9);
toView.frame = fromFrame;
viewMaskView.alpha = 0.3;
} else {
fromView.frame = toFrame;
//toView.transform = CGAffineTransformMakeScale(1.0, 1.0);
viewMaskView.alpha = 0.0;
}
} completion:^(BOOL finished) {
BOOL cancelled = [transitionContext transitionWasCancelled];
fromView.transform = CGAffineTransformIdentity;
toView.transform = CGAffineTransformIdentity;
// 删除遮罩
[backView removeFromSuperview];
[viewMaskView removeFromSuperview];
// 设置 transitionContext 通知系统动画执行完毕
[transitionContext completeTransition:!cancelled];
}];
}
- (id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC {
if (operation == UINavigationControllerOperationPush) {
self.isPush = YES;
} else {
self.isPush = NO;
}
return self;
}
- (UIImage *)screenShotImage:(UIView *)view {
// 第一个参数表示区域大小。第二个参数表示是否是非透明的。如果需要显示半透明效果,需要传NO,否则传YES。第三个参数就是屏幕密度了,设置为[UIScreen mainScreen].scale可以保证转成的图片不失真。
UIGraphicsBeginImageContextWithOptions(view.bounds.size, NO,[UIScreen mainScreen].scale);
if ([view respondsToSelector:@selector(drawViewHierarchyInRect:afterScreenUpdates:)]) {
[view drawViewHierarchyInRect:view.bounds afterScreenUpdates:YES];
} else {
[view.layer renderInContext:UIGraphicsGetCurrentContext()];
}
UIImage *viewImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return viewImage;
}
// Called when the navigation controller shows a new top view controller via a push, pop or setting of the view controller stack.
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
NSLog(@"willShowViewController:%@",viewController);
}
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
NSLog(@"didShowViewController:%@",viewController);
}
- (CATransform3D)transformRotation:(CGFloat)angle {
CATransform3D rotation3DIdentity = CATransform3DIdentity;
rotation3DIdentity.m34 = 0.3/500.0;
CATransform3D rotateTransform = CATransform3DRotate(rotation3DIdentity, angle, 0, 1, 0);
CATransform3D transform = CATransform3DMakeTranslation(0, 0, kINScreenWidth/4.0);
return CATransform3DConcat(rotateTransform, transform);
}
- (void)resetAnChorPoint:(UIView *)view anchorPoint:(CGPoint)anchorPoint {
CGPoint oldAnchorPoint = view.layer.anchorPoint;
view.layer.anchorPoint = anchorPoint;
[view.layer setPosition:CGPointMake(view.layer.position.x + view.layer.bounds.size.width * (view.layer.anchorPoint.x - oldAnchorPoint.x), view.layer.position.y + view.layer.bounds.size.height * (view.layer.anchorPoint.y - oldAnchorPoint.y))];
}
@end
2.2、处理手势过渡
在Pop可能需要手势滑动返回,我这里使用继承UIPercentDrivenInteractiveTransition的子类INPercentDrivenInteractiveTransition来处理
实现拖拽手势UIPanGestureRecognizer
代码如下
INPercentDrivenInteractiveTransition.h
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface INPercentDrivenInteractiveTransition : UIPercentDrivenInteractiveTransition
@property (nonatomic, assign, readonly) BOOL isInteractive;
@end
NS_ASSUME_NONNULL_END
INPercentDrivenInteractiveTransition.m
#import "INPercentDrivenInteractiveTransition.h"
// 屏幕宽与高
#define kScreenWidth ([UIScreen mainScreen].bounds.size.width)
#define kScreenHeight ([UIScreen mainScreen].bounds.size.height)
@interface INPercentDrivenInteractiveTransition()
@property (nonatomic, assign, readwrite) BOOL isInteractive;
@end
@implementation INPercentDrivenInteractiveTransition
- (instancetype)init
{
self = [super init];
if (self) {
self.isInteractive = NO;
}
return self;
}
- (void)handleGesture:(UIPanGestureRecognizer *)panGesture {
CGFloat transitionX = [panGesture translationInView:panGesture.view].x;
CGFloat persent = transitionX / panGesture.view.frame.size.width;
switch (panGesture.state) {
case UIGestureRecognizerStateBegan:
self.isInteractive = YES;
[self start];
break;
case UIGestureRecognizerStateChanged:
[self updateInteractiveTransition:persent];
break;
case UIGestureRecognizerStateEnded:
self.isInteractive = NO;
if (persent > 0.5) {
[self finishInteractiveTransition];
}else{
[self cancelInteractiveTransition];
}
break;
default:
break;
}
}
- (void)start {
UIViewController *controller = [self getCurrentWindowController];
[controller.navigationController popViewControllerAnimated:YES];
}
/**
获取当前屏幕显示的controller
@return controller
*/
- (UIViewController *)getCurrentWindowController {
UIViewController *result = nil;
UIWindow * window = [[UIApplication sharedApplication] keyWindow];
if (window.windowLevel != UIWindowLevelNormal) {
NSArray *windows = [[UIApplication sharedApplication] windows];
for(UIWindow * tmpWin in windows) {
if (tmpWin.windowLevel == UIWindowLevelNormal) {
window = tmpWin;
break;
}
}
}
result = window.rootViewController;
while (result.presentingViewController) {
result = result.presentingViewController;
}
if ([result isKindOfClass:[UITabBarController class]]) {
result = [(UITabBarController *)result selectedViewController];
}
if ([result isKindOfClass:[UINavigationController class]]) {
result = [(UINavigationController *)result visibleViewController];
}
return result;
}
@end
2.3、UINavigationController扩展Category
在Push与Pop时候需要转场动画,那需要替换UINavigationController中的方法
需要使用method_exchangeImplementations
这里暂不详细说明方法替换的逻辑了,详细参考
https://blog.csdn.net/gloryFlow/article/details/131677505
具体代码如下
UINavigationController+Transition.h
#import <UIKit/UIKit.h>
#import "UIViewController+Transition.h"
/**
处理转场动画
*/
@interface UINavigationController (Transition)
@end
UINavigationController+Transition.m
#import "UINavigationController+Transition.h"
#import <objc/runtime.h>
#import "INPushPopTranstioning.h"
@implementation UINavigationController (Transition)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
method_exchangeImplementations(class_getInstanceMethod(class, @selector(popViewControllerAnimated:)), class_getInstanceMethod(class, @selector(in_popViewControllerAnimated:)));
method_exchangeImplementations(class_getInstanceMethod(class, @selector(pushViewController:animated:)), class_getInstanceMethod(class, @selector(in_pushViewController:animated:)));
});
}
- (void)in_pushViewController:(UIViewController *)viewController animated:(BOOL)animated {
if (animated && [INPushPopTranstioning sharedInstance].transitionDuration > 0.0) {
// 跳转到某些ViewController,需要特定的转场动画,需要设置showNavTransitionDelegate。
if (viewController.showNavTransitionDelegate) {
self.delegate = viewController.showNavTransitionDelegate;
} else {
self.delegate = [INPushPopTranstioning sharedInstance];
}
}
[self in_pushViewController:viewController animated:animated];
}
- (nullable UIViewController *)in_popViewControllerAnimated:(BOOL)animated {
if (animated && [INPushPopTranstioning sharedInstance].transitionDuration > 0.0) {
// 回到到某些ViewController,需要特定的转场动画,需要设置showNavTransitionDelegate。
if (self.visibleViewController.showNavTransitionDelegate) {
self.delegate = self.visibleViewController.showNavTransitionDelegate;
} else {
self.delegate = [INPushPopTranstioning sharedInstance];
}
}
return [self in_popViewControllerAnimated:animated];
}
@end
2.4、UIViewController扩展Category
我这里扩展UIViewController添加运行时属性,代码如下
UIViewController+Transition.h
#import <UIKit/UIKit.h>
@interface UIViewController (Transition)
@property (nonatomic, weak) id <UINavigationControllerDelegate> showNavTransitionDelegate;
@end
UIViewController+Transition.m
#import "UIViewController+Transition.h"
#import <objc/runtime.h>
static const void *showTransitionKey = &showTransitionKey;
@implementation UIViewController (Transition)
- (id<UINavigationControllerDelegate>)showNavTransitionDelegate {
return objc_getAssociatedObject(self, showTransitionKey);
}
- (void)setShowNavTransitionDelegate:(id<UINavigationControllerDelegate>)showNavTransitionDelegate {
objc_setAssociatedObject(self, showTransitionKey, showNavTransitionDelegate, OBJC_ASSOCIATION_ASSIGN);
}
@end
2.4、UIViewController扩展Category Navigation
UIViewController+Navigation.h
#import <UIKit/UIKit.h>
@interface UIViewController (Navigation)
@property (nonatomic, weak) UINavigationController *in_navigationController;
@end
UIViewController+Navigation.m
#import "UIViewController+Navigation.h"
#import <objc/runtime.h>
static const void *showNavTransitionKey = &showNavTransitionKey;
@implementation UIViewController (Navigation)
- (UINavigationController *)in_navigationController {
return objc_getAssociatedObject(self, showNavTransitionKey);
}
- (void)setIn_navigationController:(UINavigationController *)in_navigationController {
objc_setAssociatedObject(self, showNavTransitionKey, in_navigationController, OBJC_ASSOCIATION_ASSIGN);
}
@end
三、使用自定义转场动画
在BaseViewController中添加拖拽手势,用于处理手势滑动返回。
UIScreenEdgePanGestureRecognizer *pan = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:[INPushPopTranstioning sharedInstance].interactiveTransition action:@selector(handleGesture:)];
pan.edges = UIRectEdgeLeft;
[self.view addGestureRecognizer:pan];
BaseViewController完整代码如下
INBaseViewController.h
#import <UIKit/UIKit.h>
#import "UIViewController+Navigation.h"
NS_ASSUME_NONNULL_BEGIN
@interface INBaseViewController : UIViewController
@property (nonatomic, assign) BOOL isViewDidAppear;
@end
NS_ASSUME_NONNULL_END
INBaseViewController.m
#import "INBaseViewController.h"
#import "INPushPopTranstioning.h"
@interface INBaseViewController ()
@end
@implementation INBaseViewController
- (id)init {
self = [super init];
if (self) {
self.hidesBottomBarWhenPushed = YES;
[self setNeedsStatusBarAppearanceUpdate];
}
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.view.exclusiveTouch = YES;
self.view.clipsToBounds = YES;
self.automaticallyAdjustsScrollViewInsets = NO;
self.extendedLayoutIncludesOpaqueBars = YES;
UIScreenEdgePanGestureRecognizer *pan = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:[INPushPopTranstioning sharedInstance].interactiveTransition action:@selector(handleGesture:)];
pan.edges = UIRectEdgeLeft;
[self.view addGestureRecognizer:pan];
}
- (void)loadView {
[super loadView];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
self.isViewDidAppear = NO;
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
self.isViewDidAppear = NO;
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
self.isViewDidAppear = YES;
self.navigationController.interactivePopGestureRecognizer.enabled = YES;
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
self.isViewDidAppear = NO;
}
- (void)handleGesture:(UIScreenEdgePanGestureRecognizer *)gestureRecognizer {
// 手势
}
#pragma mark - StatusBar style
- (UIStatusBarStyle)preferredStatusBarStyle {
return UIStatusBarStyleDefault;
}
- (BOOL)prefersStatusBarHidden {
return NO;
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation {
return (toInterfaceOrientation == UIInterfaceOrientationPortrait);
}
- (BOOL)shouldAutorotate {
return NO;
}
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
return UIInterfaceOrientationPortrait;
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (void)dealloc {
NSLog(@"DEALLOCBaseViewController");
}
@end
我这里使用的是NavigationController嵌套自定义的TabbarController.
在AppDelegate中didFinishLaunchingWithOptions进行设置
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
[self.window makeKeyAndVisible];
INMainTabBarController *tabbar = [[INMainTabBarController alloc] init];
UINavigationController *mainNav = [[UINavigationController alloc] initWithRootViewController:tabbar];
mainNav.delegate = [INPushPopTranstioning sharedInstance];
self.window.backgroundColor = [UIColor whiteColor];
self.window.rootViewController = mainNav;
return YES;
}
三、小结
iOS开发-转场动画切换界面(类似系统动画)。UIViewControllerAnimatedTransitioning与UIViewControllerInteractiveTransitioning。
学习记录,每天不停进步。