專案概述
- iOS中最常見的影片無疑是Push和Pop的轉場影片了,其次是Present和Dismiss的轉場影片,
如果我們想自定義這些轉場影片,蘋果其實提供了相關的API,在自定義轉場之前,我們需要了解轉場原理和處理邏輯,下面是自定義轉場的效果:
- 專案地址:CustomPushAndPresent
如果文章和專案對你有幫助,還請給個Star??,你的Star??是我持續輸出的動力,謝謝啦😘
Push/Pop轉場
Push/Pop轉場原理
- 在呼叫導航控制器的pushViewController:animated:之前,如果設定了導航控制器的delegate物件,就會呼叫delegate物件的回呼方法
navigationController:animationControllerForOperation:fromViewController:toViewController:,可在該回呼方法中自定義轉場,該回呼方法需要回傳一個遵守UIViewControllerAnimatedTransitioning協議的物件,定義一個類實作UIViewControllerAnimatedTransitioning協議的兩個方法以便自定義Push/Pop轉場,這兩個必須實作的方法如下:
- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext;
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;
- 用runtime給UIViewController提供一個屬性hr_addTransitionFlag,用于標記是否添加自定義轉場,代碼如下:
@interface UIViewController (TransitionProperty)
@property (nonatomic, assign) BOOL hr_addTransitionFlag;//是否添加自定義轉場
@end
#import "UIViewController+TransitionProperty.h"
#import <objc/runtime.h>
static NSString *hr_addTransitionFlagKey = @"hr_addTransitionFlagKey";
@implementation UIViewController (TransitionProperty)
- (void)setHr_addTransitionFlag:(BOOL)hr_addTransitionFlag {
objc_setAssociatedObject(self, &hr_addTransitionFlagKey, @(hr_addTransitionFlag), OBJC_ASSOCIATION_ASSIGN);
}
- (BOOL)hr_addTransitionFlag {
return [objc_getAssociatedObject(self, &hr_addTransitionFlagKey) integerValue] == 0 ? NO : YES;
}
@end
上面說過只要給導航控制器設定delegate,則呼叫pushViewController:animated:后,就會執行navigationController:animationControllerForOperation:fromViewController:toViewController:方法,從而展示自定義的Push/Pop轉場,呼叫popViewControllerAnimated:后同理,導航控制器的代碼如下:
-(void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated{
/*給導航控制器設定了delegate,呼叫pushViewController:animated:后,
會去執行navigationController:animationControllerForOperation:fromViewController:toViewController:
*/
self.delegate = (id)viewController;
[super pushViewController:viewController animated:animated];
}
-(UIViewController *)popViewControllerAnimated:(BOOL)animated{
/*給導航控制器設定了delegate,呼叫popViewControllerAnimated:后,
會去執行navigationController:animationControllerForOperation:fromViewController:toViewController:
*/
self.delegate = self.viewControllers.lastObject;
return [super popViewControllerAnimated:animated];
}
自定義轉場
- 這里自定義一種Push時toView從螢屏頂部往下移動到螢屏中央的轉場,Pop時toView從螢屏中央往下移出螢屏的轉場,實作代碼如下:
#import <UIKit/UIKit.h>
@interface HRPushAnimatedTransitioning : NSObject <UIViewControllerAnimatedTransitioning,CAAnimationDelegate>
@property(nonatomic, assign)UINavigationControllerOperation operation;
@end
@implementation HRPushAnimatedTransitioning
-(NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext{
return 0.4;
}
-(void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext{
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
UIView *containerView = transitionContext.containerView;
//containerView本來有fromView,只需添加toView
[containerView addSubview:toView];
CGRect fromViewStartFrame = [transitionContext initialFrameForViewController:fromVC];
CGRect toViewStartFrame = [transitionContext finalFrameForViewController:toVC];
CGRect fromViewEndFrame = fromViewStartFrame;
CGRect toViewEndFrame = toViewStartFrame;
if (_operation == UINavigationControllerOperationPush) {
toViewStartFrame.origin.y -= toViewEndFrame.size.height;
}else if (_operation == UINavigationControllerOperationPop) {
fromViewEndFrame.origin.y += fromViewStartFrame.size.height;
[containerView sendSubviewToBack:toView];
}
fromView.frame = fromViewStartFrame;
toView.frame = toViewStartFrame;
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
fromView.frame = fromViewEndFrame;
toView.frame = toViewEndFrame;
} completion:^(BOOL finished) {
[transitionContext completeTransition:!transitionContext.transitionWasCancelled];
}];
}
處理系統的右滑回傳手勢
- iOS7開始蘋果提供了一個滑動回傳上一界面的手勢,由于我在pushViewController:animated:方法中設定了導航控制器的delegate,導致右滑回傳手勢失效,解決方式是重新設定右滑回傳手勢的delegate物件:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
__weak typeof(self) weakself = self;
if ([self respondsToSelector:@selector(interactivePopGestureRecognizer)]) {
/*只要自定義navigationItem的leftBarButtonItem或navigationController,滑動手勢會失效,
因此要重新設定系統自帶的右滑回傳手勢的代理為self
*/
self.interactivePopGestureRecognizer.delegate = weakself;
}
}
以上設定后,rootViewController也會回應右滑回傳,可能導致一些問題,因此需要禁止rootViewController的右滑回傳功能,即導航控制器中的代碼如下:
#pragma mark - UIGestureRecognizerDelegate
-(BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer{
if (gestureRecognizer == self.interactivePopGestureRecognizer) {
//屏蔽rootViewController的滑動回傳手勢,避免右滑回傳手勢引起死機問題
if (self.viewControllers.count <= 1 || self.visibleViewController == [self.viewControllers objectAtIndex:0]) {
return NO;
}
}
return YES;
}
注意右滑回傳手勢默認是啟用的,即self.interactivePopGestureRecognizer的enable默認是YES
處理右滑回傳手勢的轉場
- 上面雖然實作了自定義Push/Pop轉場,但是用系統自帶滑動手勢pop時并沒有展示我們自定義的Push/Pop轉場效果,展示的依然是系統默認的轉場效果,
原因是當自定義了Push or Pop的轉場,系統呼叫navigationController:animationControllerForOperation:fromViewController:toViewController:方法,該方法如果回傳的是非nil物件后,就會執行以下代理方法:
-(id<UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransitioning>)animationController
這是蘋果提供給開發者自定義滑動手勢互動轉場的代理方法,回傳一個遵守UIViewControllerInteractiveTransitioning協議的物件,該物件需要實作startInteractiveTransition:方法,為此蘋果提供了一個實作該協議的UIPercentDrivenInteractiveTransition類,我們只需定義一個繼承UIPercentDrivenInteractiveTransition類的類,就能滿足回傳物件的條件,而不需要是實作startInteractiveTransition:方法,
由于當navigationController:animationControllerForOperation:fromViewController:toViewController回傳的物件非nil時,Push和Pop都會回呼navigationController:interactionControllerForAnimationController:代理方法,而我們重寫該代理方法只是針對右滑回傳手勢的轉場,其他情況回傳nil,因此需要區分push還是pop,解決方式是在navigationController:animationControllerForOperation:fromViewController:toViewController中保存當前是push還是pop,代碼如下:
//用于自定義Push or Pop的轉場
//回傳值非nil表示使用自定義的Push or Pop轉場,nil表示使用系統默認的轉場
-(id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC{
if (!self.hr_addTransitionFlag) {
return nil;
}
HRPushAnimatedTransitioning *obj = [[HRPushAnimatedTransitioning alloc] init];
obj.operation = operation;
_operation = operation;
if (operation == UINavigationControllerOperationPush) {
// NSLog(@"_interactive:%@--%@", _interactive, self);
if (_interactive == nil) {
_interactive = [[HRPercentDrivenInteractiveTransition alloc] init];
}
[_interactive addGestureToViewController:self];
}
return obj;
}
//使用自定義的Push or Pop轉場才會回呼該方法,用于自定義滑動手勢的轉場互動方式
//回傳值非nil表示可互動處理轉場進度,nil表示無法互動處理轉場進度,直接完成轉場
-(id<UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransitioning>)animationController{
if (_operation == UINavigationControllerOperationPush) {
return nil;
}else{
if (_interactive.canInteractive) {
return _interactive;
}else{
return nil;
}
}
}
實作自定義右滑回傳手勢的轉場
- HRPercentDrivenInteractiveTransition類的邏輯是:給控制器view添加Pan手勢,當右滑時,計算右滑占螢屏寬度的百分比percent(可認為是轉場進度引數),然后在右滑開始時,呼叫導航控制器的popViewControllerAnimated:,滑動程序中呼叫updateInteractiveTransition:,傳入轉場進度引數percent,轉場結束時根據轉場進度,判斷是呼叫finishInteractiveTransition(轉場完成,即成功pop到上一界面)還是cancelInteractiveTransition(轉場恢復到起點),最終代碼如下:
#import <UIKit/UIKit.h>
//UIPercentDrivenInteractiveTransition實作UIViewControllerInteractiveTransitioning協議
@interface HRPercentDrivenInteractiveTransition : UIPercentDrivenInteractiveTransition
@property (readonly, assign, nonatomic) BOOL canInteractive;
-(void)addGestureToViewController:(UIViewController *)vc;
@end
@interface HRPercentDrivenInteractiveTransition ()
@property (nonatomic, weak) UINavigationController *nav;
@property (nonatomic, strong) CADisplayLink *displayLink;
@property (nonatomic, assign) CGFloat percent;
@end
@implementation HRPercentDrivenInteractiveTransition
-(void)addGestureToViewController:(UIViewController *)vc{
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panAction:)];
[vc.view addGestureRecognizer:pan];
self.nav = vc.navigationController;
}
-(void)panAction:(UIPanGestureRecognizer *)pan{
_percent = 0.0;
CGFloat totalWidth = pan.view.bounds.size.width;
CGFloat x = [pan translationInView:pan.view].x;
_percent = x/totalWidth;
switch (pan.state) {
case UIGestureRecognizerStateBegan:{
_canInteractive = YES;
[_nav popViewControllerAnimated:YES];
}
break;
case UIGestureRecognizerStateChanged:{
[self updateInteractiveTransition:_percent];
}
break;
case UIGestureRecognizerStateEnded:{
_canInteractive = NO;
[self continueAction];
}
break;
default:
break;
}
}
-(BOOL)isCanInteractive{
return _canInteractive;
}
- (void)continueAction{
if (_displayLink) {
return;
}
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(UIChange)];
[_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)UIChange {
CGFloat timeDistance = 1.5/60;
if (_percent > 0.4) {
_percent += timeDistance;
}else {
_percent -= timeDistance;
}
[self updateInteractiveTransition:_percent];
if (_percent >= 1.0) {
//轉場完成
[self finishInteractiveTransition];
[_displayLink invalidate];
_displayLink = nil;
}
if (_percent <= 0.0) {
//轉場取消
[self cancelInteractiveTransition];
[_displayLink invalidate];
_displayLink = nil;
}
}
Present/Dismiss轉場
Present/Dismiss轉場原理
- 控制器設定transitioningDelegate為自身,遵守UIViewControllerTransitioningDelegate協議,實作協議的present影片方法和dismiss影片方法,即如下兩個方法:
-(id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source;
-(id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed;
這兩個方法需要回傳一個遵守UIViewControllerAnimatedTransitioning協議的物件,定義一個類實作UIViewControllerAnimatedTransitioning協議的兩個方法以便自定義Present/Dismiss轉場,
控制器關鍵代碼如下:
- (instancetype)init
{
self = [super init];
if (self) {
self.transitioningDelegate = self;
}
return self;
}
//present過渡影片(非互動)
-(id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source{
HRPresentAnimatedTransitioning *obj = [[HRPresentAnimatedTransitioning alloc] initType:PictureTransitionPresent];
return obj;
}
//dismiss過渡影片(非互動)
-(id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed{
HRPresentAnimatedTransitioning *obj = [[HRPresentAnimatedTransitioning alloc] initType:PictureTransitionDismiss];
return obj;
}
自定義轉場
- 這里自定義一種Present時toView從螢屏左邊往右移動到螢屏中央的轉場,dismiss時toView從螢屏中央往右移出螢屏的轉場,實作代碼如下:
typedef NS_ENUM(NSInteger,PictureTransitionType) {
PictureTransitionPresent = 0,//顯示
PictureTransitionDismiss //消失
};
@interface HRPresentAnimatedTransitioning : NSObject <UIViewControllerAnimatedTransitioning>
- (instancetype)initType:(PictureTransitionType)type;
@end
#import "HRPresentAnimatedTransitioning.h"
@interface HRPresentAnimatedTransitioning ()
@property(nonatomic, assign)PictureTransitionType type;
@end
@implementation HRPresentAnimatedTransitioning
- (instancetype)initType:(PictureTransitionType)type{
self = [super init];
if (self) {
_type = type;
}
return self;
}
-(NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext{
return 0.4;
}
-(void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext{
//present時,fromVC是導航控制器,toVC是HRDetailViewController,dismiss時,fromVC是HRDetailViewController,toVC是導航控制器
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
UIView *containerView = transitionContext.containerView;
//containerView本來有fromView,只需添加toView
[containerView addSubview:toView];
CGRect fromViewStartFrame = [transitionContext initialFrameForViewController:fromVC];
CGRect toViewStartFrame = [transitionContext finalFrameForViewController:toVC];
CGRect fromViewEndFrame = fromViewStartFrame;
CGRect toViewEndFrame = toViewStartFrame;
if (_type == PictureTransitionPresent) {
toViewStartFrame.origin.x -= toViewEndFrame.size.width;
}else if (_type == PictureTransitionDismiss) {
fromViewEndFrame.origin.x += fromViewStartFrame.size.width;
[containerView sendSubviewToBack:toView];
}
fromView.frame = fromViewStartFrame;
toView.frame = toViewStartFrame;
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
fromView.frame = fromViewEndFrame;
toView.frame = toViewEndFrame;
} completion:^(BOOL finished) {
[transitionContext completeTransition:!transitionContext.transitionWasCancelled];
}];
}
@end
參考資料
- 一行代碼呼叫支持手勢回傳的iOS自定義轉場影片
- 向 UINavigationController 的傳統影片說”再見” — 自定義過場影片
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/305718.html
標籤:其他
下一篇:使用Python控制手機
