3d翻转 ios_iOS自定义转场详解04——实现3D翻转效果
這是自定義轉場系列的第四篇。由于具有一定的連續性,我會忽略一些基礎,所以如果你是第一次看這個系列,可以先過目之前的幾篇 ——— UIViewControllerTransitioning的用法 、實現Keynote中的神奇移動效果、實現通過圓圈放大縮小的轉場動畫。
老規矩,先端上GIF。
How to work
首先在StoryBoard上拖兩個UIViewController。并且在第一個VC上放一個button,使用Action Segue連接到第二個VC。
然后回到代碼界面。和以往一樣,我們需要創建兩個文件:一個用于從第一個VC過渡到第二個VC的動畫(如push),另一個這是第二個過渡到第一個VC的動畫(如pop)。這里不得不說iOS7中引入的這種解耦合的方式,它的意義在于無論在哪兒需要用到轉場動畫的地方,直接把這兩個文件扔過去就行了。
我們創建兩個文件:KYPushTransition 和 KYPopTransition 。從名字可以看出,后一個是前一個的反轉動畫。其實,我們完全可以把兩個文件寫在一起:KYTransition 。因為兩個文件的代碼結構幾乎別無二致,不同的地方也只要用布爾值區分一下就行了。但這里為了讓介紹思路清晰,我們把兩個動畫分開來實現。
首先是KYPushTransition。
先繼承 UIViewControllerAnimatedTransitioning 協議。實現下面兩個方法:
- (NSTimeInterval)transitionDuration:(id )transitionContext{
//動畫的時間
return 0.6f;
}
- (void)animateTransition:(id )transitionContext{
//動畫的邏輯
...
}
下面具體介紹動畫的邏輯。
- (void)animateTransition:(id )transitionContext{
//1
FirstViewController *fromVC = (FirstViewController *)[transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
SecondViewController *toVC = (SecondViewController *)[transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *fromView = fromVC.view;
UIView *toView = toVC.view;
UIView *containerView = [transitionContext containerView];
[containerView addSubview:toView];
[containerView sendSubviewToBack:toView];
//2
CATransform3D transform = CATransform3DIdentity;
transform.m34 = -0.002;
containerView.layer.sublayerTransform = transform;
//3
CGRect initialFrame = [transitionContext initialFrameForViewController:fromVC];
fromView.frame = initialFrame;
toView.frame = initialFrame;
//4
[self updateAnchorPointAndOffset:CGPointMake(0.0, 0.5) view:fromView];
//5
CAGradientLayer *gradient = [CAGradientLayer layer];
gradient.frame = fromView.bounds;
gradient.colors = @[(id)[UIColor colorWithWhite:0.0 alpha:0.5].CGColor,
(id)[UIColor colorWithWhite:0.0 alpha:0.0].CGColor];
gradient.startPoint = CGPointMake(0.0, 0.5);
gradient.endPoint = CGPointMake(0.8, 0.5);
UIView *shadow = [[UIView alloc]initWithFrame:fromView.bounds];
shadow.backgroundColor = [UIColor clearColor];
[shadow.layer insertSublayer:gradient atIndex:1];
shadow.alpha = 0.0;
[fromView addSubview:shadow];
//6
[UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0.0 options:UIViewAnimationOptionCurveEaseOut animations:^{
//旋轉fromView 90度
fromView.layer.transform = CATransform3DMakeRotation(-M_PI_2, 0, 1.0, 0);
shadow.alpha = 1.0;
} completion:^(BOOL finished) {
//7
fromView.layer.anchorPoint = CGPointMake(0.5, 0.5);
fromView.layer.position = CGPointMake(CGRectGetMidX([UIScreen mainScreen].bounds), CGRectGetMidY([UIScreen mainScreen].bounds));
fromView.layer.transform = CATransform3DIdentity;
[shadow removeFromSuperview];
[transitionContext completeTransition:YES];
}];
}
解釋一下。
1)通過上下文transitionContext獲得前后兩個UIView,這也是發生動畫的具體對象。同時還需要獲得containerView,這也是動畫發生的地方。我們需要把后一個視圖添加上去。為了保證后一個視圖加上去之后不遮住前一個視圖的動畫,我們還要把后一個視圖放到最后:[containerView sendSubviewToBack:toView];
2)為了保證視圖產生3D的效果,我們需要設置layer的仿射變換。關于仿射變化和m34的概念,推薦一篇博客:iOS的三維透視投影。
3)為fromView、toView設置初始frame。
4)重置錨點。錨點就是視圖旋轉時候的中心,就是那個不動的點。關于錨點以及position的關系,你可以參考這一篇解釋:這將是你最后一次糾結position與anchorPoint!。所以我們在設置了錨點的之后,還需要把layer的position也設置到相應位置:
-(void)updateAnchorPointAndOffset:(CGPoint)anchorPoint view:(UIView *)view{
view.layer.anchorPoint = anchorPoint;
view.layer.position = CGPointMake(0, CGRectGetMidY([UIScreen mainScreen].bounds));
}
方便記憶,你可以理解錨點會吸附到position上。所以光改變錨點不改變position,那么結果就是錨點確實改了,但是position還是在默認的(0.5,0.5),也就是視圖中心。就像這樣:
5)給fromView增加左深右淺的陰影。并且一開始的透明度為0,隨著翻轉的角度變大過渡到1。
6)開始動畫。這這里,我們讓fromView翻轉90度 fromView.layer.transform = CATransform3DMakeRotation(-M_PI_2, 0, 1.0, 0);。這里要注意向外90度是-M_PI_2,y為1.0表示繞著y軸旋轉。
7)動畫結束,我們需要還原錨點的位置、恢復position的位置、恢復layer的transform為CATransform3DIdentity,并且把陰影層移除。由于一開始我沒有沒有恢復錨點和position的位置,而且一直沒找到原因。知道我查看了視圖的層級結構才恍然大悟:
可見,使用控制臺的”Debug View Hierarchy“
是多么有用!
當然,還有最重要最關鍵的一步:[transitionContext completeTransition:YES];。告訴上下文,動畫已經完成。如果你這里不這么做,你將無法從后一個視圖返回前一個視圖。
好了,至此已經完成所有push動畫邏輯。實現pop的邏輯基本無二。
KYPopTransition
1、首先就是刪除[containerView sendSubviewToBack:toView];,這時我們可不想讓動畫躲在后面。
2、第二個不同點,需要加上
//讓toView的截圖旋轉90度
toView.layer.transform = CATransform3DMakeRotation(-M_PI_2, 0.0, 1.0, 0.0);
因為這時動畫的起始應該先保持在90度的位置,然后慢慢過渡到0度。
3、陰影層的需要加在toView上,并且起始透明度應該為1,終止時為0。
How to Use
1、如果你是一個VC present 到另一個VC,那么FirstViewController和SecondViewController都需要繼承協議是UIViewControllerTransitioningDelegate。
然后在FirstViewController中設置代理。
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
SecondViewController *secVC = (SecondViewController *)segue.destinationViewController;
secVC.transitioningDelegate = self;
[super prepareForSegue:segue sender:sender];
}
并實現協議的兩個方法:分別對應present和dismiss。
- (id )animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source{
KYPushTransition *flip = [KYPushTransition new];
return flip;
}
- (id )animationControllerForDismissedController:(UIViewController *)dismissed{
KYPopTransition *flip = [KYPopTransition new];
return flip;
}
2、如果你用UINavigationController去控制兩個VC,此時如果你什么都不做,segue會自動變成標準的導航欄推進的push、pop。要使用我們自定義的動畫,需要讓FirstViewController繼承協議UINavigationControllerDelegate,然后設置代理為自己self.navigationController.delegate = self;。
實現UINavigationControllerDelegate中的相關協議方法,只需要一個:
- (id )navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC{
if (operation == UINavigationControllerOperationPush) {
KYPushTransition *flip = [KYPushTransition new];
return flip;
}else if (operation == UINavigationControllerOperationPop){
KYPopTransition *flip = [KYPopTransition new];
return flip;
}else{
return nil;
}
}
搞定,現在就運行了。
Where to go —— 如何為轉場增加手勢交互
如果是一個VC present 到另一個VC,那么就要實現UIViewControllerTransitioningDelegate中的兩個方法,和UIViewControllerAnimatedTransitioning一樣,分別對應 present 和 dismiss的動畫。
- (id )interactionControllerForPresentation:(id )animator;
- (id )interactionControllerForDismissal:(id )animator;
這時,你可能已經被各種協議代理名字搞暈了,是的,我一開始也暈了,但只要多練幾次還是能就熟悉的。
既然我們之前是創建了兩個文件繼承UIViewControllerAnimatedTransitioning來實現過渡動畫,那么是不是也應該繼承UIViewControllerInteractiveTransitioning來實現百分比交互動畫呢?因為在圖中,兩者的關系是并列的。是的,你可以這么做。但是蘋果提供了一個更好的類 ———— UIPercentDrivenInteractiveTransition。
顧名思義,我們就可以猜到這個類就是專門用來實現手勢百分比交互的。怎么使用呢?
為了遵循解耦合,我們新建一個UIPercentDrivenInteractiveTransition的子類 —— KYPopInteractiveTransition。
創建一個方法-(void)addPopGesture:(UIViewController *)viewController;,用來給目標視圖控制器添加一個邊緣滑動手勢:
-(void)addPopGesture:(UIViewController *)viewController{
presentedVC = viewController;
UIScreenEdgePanGestureRecognizer *edgeGes = [[UIScreenEdgePanGestureRecognizer alloc]initWithTarget:self action:@selector(edgeGesPan:)];
edgeGes.edges = UIRectEdgeLeft;
[viewController.view addGestureRecognizer:edgeGes];
}
實現相應的手勢方法:
-(void)edgeGesPan:(UIScreenEdgePanGestureRecognizer *)edgeGes{
//1
CGFloat translation =[edgeGes translationInView:presentedVC.view].x;
CGFloat percent = translation / (presentedVC.view.bounds.size.width);
percent = MIN(1.0, MAX(0.0, percent));
NSLog(@"%f",percent);
switch (edgeGes.state) {
case UIGestureRecognizerStateBegan:{
//2
self.interacting = YES;
[presentedVC dismissViewControllerAnimated:YES completion:nil];
//如果是navigationController控制,這里應該是[presentedVC.navigationController popViewControllerAnimated:YES];
break;
}
case UIGestureRecognizerStateChanged:{
//3
[self updateInteractiveTransition:percent];
break;
}
case UIGestureRecognizerStateEnded:{
//4
self.interacting = NO;
if (percent > 0.5) {
[self finishInteractiveTransition];
}else{
[self cancelInteractiveTransition];
}
break;
}
default:
break;
}
}
1)計算手指在X軸方向上的偏移距離,與屏幕的寬度的之比保存為一個百分比。也就是說,當手指劃過屏幕的距離超過屏幕寬度的1/2,那么剩下的動畫就自動完成;否則,取消動畫。這里用了MIN和MAX把百分比始終控制在了0~1之間。
2)滑動開始,指定要執行的操作。這里因為沒有使用UINavigation控制兩個VC,所以是dismissViewControllerAnimated:。如果是用UINavigation去控制的,那么這里相應的應該是navigationController popViewControllerAnimated:。 self.interacting的作用稍后揭曉。
3)在UIGestureRecognizerStateChanged 調用 [self updateInteractiveTransition:percent]。這里我們把剛才的百分比傳了過去,系統就可以通過這個0~1的豎數值實時改變動畫的進度。
4)當UIGestureRecognizerStateEnded的時候,我們需要判斷此時手指是否劃過屏幕大于一半的距離。如果大于一半,告訴系統完成:[self finishInteractiveTransition]; 反之,告訴系統取消操作:[self cancelInteractiveTransition],這時動畫也將返回初始位置。
特別注意,當我們使用了手勢百分比交互,在相應的動畫邏輯KYPopTransition中,把原來的[transitionContext completeTransition:YES] 改成 [transitionContext completeTransition:![transitionContext transitionWasCancelled]]。如果一直是YES的話,當我們手指劃過小于屏幕一半,即使系統知道是取消動畫,但在上下文中依然是寫死的YES。
使用手勢百分比交互
在FirstViewController中的-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender里面,創建一個KYPopInteractiveTransition的實例并把SecondVC傳過去:
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
...
popInteractive = [KYPopInteractiveTransition new];
[popInteractive addPopGesture:secVC];
...
}
然后實現UIViewControllerTransitioningDelegate里的這個關于手勢百分比交互的方法:
- (id )interactionControllerForDismissal:(id )animator{
return popInteractive.interacting ? popInteractive : nil;
}
好了,這里你發現我們使用了popInteractive.interacting來判斷,還記得之前買的關子嗎?self.interacting的作用就在這里。因為- (id )interactionControllerForDismissal:(id )animator 這個方法在 點擊dismiss和滑動dismiss時候都會調用。然而如果只是return popInteractive;的話,當我們點擊dismiss的時候,程序將不會做出反應。所以,我們需要區分,到底是點擊dismiss還是滑動dismiss。因此,我們需要一個布爾值來判斷,就是這樣。
這個系列應該差不多到這里就結束了。本篇的源碼你可以在這里獲得。
有任何疑問,歡迎在下方評論區域留言:D
總結
以上是生活随笔為你收集整理的3d翻转 ios_iOS自定义转场详解04——实现3D翻转效果的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: onCreateOptionsMenu
- 下一篇: sqlserver55555_sqlse