轉場動畫
轉場動畫就是從一個場景以動畫的形式過渡到另一個場景。自定義轉場動畫的意義是脫離系統固定的轉場,實現UI交互設計師設計的視覺效果強的轉場動畫。
下圖是整個案例的Demo菜單截圖,為了方便大家一步一步掌握自定義轉場動畫,每個效果我都寫了非常詳細的Demo(包括導航push的轉場和模態modal的轉場),建議大家先下載下來跟著文章一個案例一個案例自己去實現一下,會對理解十分有幫助。github地址:https://github.com/yangli-dev/LYCustomTransition
目錄
0、CATransition(系統轉場動畫)
一、基礎轉場-非交互式(初步認識自定義轉場動畫)
二、仿酷狗轉場-非交互式(鞏固對轉場的認識)
三、仿微信轉場-非交互式(加強對轉場的認識)
四、基礎轉場-交互式
五、實戰 - 網友提問 - Question One
六、圖片瀏覽器-PictureBrowse
說明:本文章目前只講解了demo中的部分案例,但其它的跟這幾個類似,擴展一下就好,如有問題請留言
0、CATransition(系統轉場動畫)
首先來個最簡單的改變轉場效果的方法(不是自定義,是通過官方提供的動畫效果實現),提供動畫效果的這個類就是CATransition
CATransition
CATransition 是CAAnimation的子類,用于頁面之間的過度動畫,官方提供了四個公有的API動畫效果,但是私有API的效果更加炫酷(謹慎使用私有的API)
(1)Nav導航轉場:要改變轉場動畫,其實方法只有一個,非常容易理解:
[self.navigationController.view.layer addAnimation:[self pushAnimation] forKey:nil]
,
(2)modal模態轉場:和nav類似,
[self.view.window.layer addAnimation:[self presentAnimation] forKey:nil];
意思是在視圖的圖層上添加一個CAAnimation類動畫,然后圖層執行這個類提供的動畫效果,故轉場動畫也就改變了。
通過CAAnimation 的子類CATransition可以快速創建動畫效果,以下代碼是改變系統轉場動畫的具體實現
- (void)pushSecond{
LYCATransitionSecondVC *second = [[LYCATransitionSecondVC alloc] init];
[self.navigationController.view.layer addAnimation:[self pushAnimation] forKey:nil];//添加Animation
[self.navigationController pushViewController:second animated:NO]; //記得這里的animated要設為NO,不然會重復
/* modal模態
LYModalCATransitionSecondVC *second = [[LYModalCATransitionSecondVC alloc] init];
[self.view.window.layer addAnimation:[self presentAnimation] forKey:nil];//添加Animation
[self presentViewController:second animated:NO completion:nil]; //記得這里的animated要設為NO,不然會重復
*/
}
- (CATransition *)pushAnimation{
CATransition* transition = [CATransition animation];
transition.duration = 0.8;
transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault];
/*私有API
cube 立方體效果
pageCurl 向上翻一頁
pageUnCurl 向下翻一頁
rippleEffect 水滴波動效果
suckEffect 變成小布塊飛走的感覺
oglFlip 上下翻轉
cameraIrisHollowClose 相機鏡頭關閉效果
cameraIrisHollowOpen 相機鏡頭打開效果
*/
transition.type = @"cube";
//transition.type = kCATransitionMoveIn;
//下面四個是系統公有的API
//kCATransitionMoveIn, kCATransitionPush, kCATransitionReveal, kCATransitionFade
transition.subtype = kCATransitionFromRight;
//kCATransitionFromLeft, kCATransitionFromRight, kCATransitionFromTop, kCATransitionFromBottom
return transition;
}
看完是不是很簡單,趕緊自己嘗試一下吧。接下來就是真正的自定義轉場動畫的學習了。
真正的自定義轉場從這里開始
從iOS7開始,蘋果提供了真正能自定義轉場動畫的API,這才使得我們可以為APP定義自己特有的轉場效果。轉場有非交互式和交互式轉場,這里當然是從基本的非交互式的轉場開始說起。
其實導航push和模態modal自定義轉場的實現,只是一個協議的區別,
實現push的類去遵循UINavigationControllerDelegate
協議;
實現modal的類去遵循UIViewControllerTransitioningDelegate
協議。
兩個協議里面的方法都大同小異,所以此系列文章就講push轉場中的案例實現
。
具體看Demo就知道了_(可能在這里你并不知道這兩個協議是干嘛的,不要擔心,下面馬上就一一道來)
一、基礎轉場-非交互式(初步認識自定義轉場動畫)
完成這個案例只需要簡單的兩步就可實現,耐心并仔細看下去,你會發現自定義轉場其實也很簡單!
1. 遵循UINavigationControllerDelegate
協議,設置代理。
比如在Demo中的Nav-BaseTransition
案例,在LYNavBaseVC
本個類中自己遵循UINavigationControllerDelegate
此協議,在push轉場之前設置代理self.navigationController.delegate = self
,然后再實現其協議特有方法,當push操作執行時,就會回調實現的代理方法,代理方法會要求返回遵循了UIViewControllerAnimatedTransitioning
協議的代理對象,從而去執行所對應的動畫。(代碼中的LYNavBaseCustomAnimator
是遵循了UIViewControllerAnimatedTransitioning
協議的類,這個協議是專門在轉場中提供并執行轉場動畫,稍后會在第2小節詳細介紹)
- (nullable id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC;
具體代碼實現
- (nullable id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC{
if (operation == UINavigationControllerOperationPush) {
return self.customAnimator;
}else if (operation == UINavigationControllerOperationPop){
return self.customAnimator;
}
return nil;
}
- (LYNavBaseCustomAnimator *)customAnimator
{
if (_customAnimator == nil) {
_customAnimator = [[LYNavBaseCustomAnimator alloc]init];
}
return _customAnimator;
}
2.創建提供動畫效果的執行者
上述代碼中的
LYNavBaseCustomAnimator
就是動畫效果的執行者。它是遵循了UIViewControllerAnimatedTransitioning
協議的類。
協議 UIViewControllerAnimatedTransitioning
,這個協議是轉場動畫中,動畫效果的執行者,實現這個協議的類具有負責給轉場提供各種復雜動畫效果的能力。協議里有兩個必須要實現的方法
//這個方法控制轉場動畫的時間長度
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext;
//這個是轉場上下文,提供轉場過程中兩個控制器的具體信息。
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;`
具體代碼實現
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext{
return 0.5;
}
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext{
//轉場過渡的容器view
UIView *containerView = [transitionContext containerView];
//FromVC
UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIView *fromView = fromViewController.view;
fromView.frame = CGRectMake(0, 0, kScreenWidth, kScreenHeight);
//ToVC
UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *toView = toViewController.view;
toView.frame = CGRectMake(0, 0, kScreenWidth, kScreenHeight);
//此處判斷是push,還是pop 操作
BOOL isPush = ([toViewController.navigationController.viewControllers indexOfObject:toViewController] > [fromViewController.navigationController.viewControllers indexOfObject:fromViewController]);
if (isPush) {
[containerView addSubview:fromView];
[containerView addSubview:toView];//push,這里的toView 相當于secondVC的view
toView.frame = CGRectMake(kScreenWidth, kScreenHeight, kScreenWidth, kScreenHeight);
}else{
[containerView addSubview:toView];
[containerView addSubview:fromView];//pop,這里的fromView 也是相當于secondVC的view
fromView.frame = CGRectMake(0, 0, kScreenWidth, kScreenHeight);
}
//因為secondVC的view在firstVC的view之上,所以要后添加到containerView中
//動畫
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
if (isPush) {
toView.frame = CGRectMake(0, 0, kScreenWidth, kScreenHeight);
}else{
fromView.frame = CGRectMake(kScreenWidth, kScreenHeight, kScreenWidth, kScreenHeight);
}
} completion:^(BOOL finished) {
BOOL wasCancelled = [transitionContext transitionWasCancelled];
//設置transitionContext通知系統動畫執行完畢
[transitionContext completeTransition:!wasCancelled];
}];
}
這里簡單介紹下fromView和toView,不然大家可能會有點繞
A push ---> B
B pop ---> A
|| ||
fromView toView
誰在轉場中主動發起轉場,誰就是fromVC、fromView
A主動push到B,A就是fromVC
B主動pop到A,B就是fromVC
從代碼中可以看出,轉場動畫的自定義,就是對fromView和toView的操作,而這兩個view都是可以在這個協議的上下文中獲取,所以,
我們不難實現一些簡單的自定義轉場。
案例一小結
對于非交互式的轉場來說,其實就只需要實現兩個協議的相關方法:
第一個是UINavigationControllerDelegate
,作用好比是告訴系統我有自己的轉場動畫了,我要去調我自定義的。
第二個是UIViewControllerAnimatedTransitioning
,作用好比是我制作好了動畫了,需要的你直接調用就好了。
二、仿酷狗轉場-非交互式(鞏固對轉場的認識)
動畫解析:
首先可以了解到這個動畫其實也是一個線性動畫,只不過是弧線形的,那么給定起始和終止狀態的位置就可以了。跟案例一類似,只不過這里多了一個旋轉,這個動畫可以用組動畫CAAnimationGroup
實現,但是鑒于效果不是太流暢,這里我采用的是仿射變換CGAffineTransform
實現的。代碼如下
@implementation LYNavKuGouPushAnimator
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext{
return 0.4;
}
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext{
//轉場過渡的容器view
UIView *containerView = [transitionContext containerView];
//ToVC
UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *toView = toViewController.view;
[containerView addSubview:toView];
//動畫 仿射變換動畫
float centerX = toView.bounds.size.width * 0.5;
float centerY = toView.bounds.size.height * 0.5;
float x = toView.bounds.size.width * 0.5;
float y = toView.bounds.size.height * 1.8;
//起始狀態: 原始狀態繞x,y旋轉45o后的狀態
CGAffineTransform trans = [self GetCGAffineTransformRotateAroundCenterX:centerX centerY:centerY x:x y:y angle:45.0/180.0*M_PI];
toView.transform = trans;
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
//終止狀態: 原始狀態
toView.transform = CGAffineTransformIdentity;
} completion:^(BOOL finished) {
BOOL wasCancelled = [transitionContext transitionWasCancelled];
//設置transitionContext通知系統動畫執行完畢
[transitionContext completeTransition:!wasCancelled];
}];
}
/**
仿射變換
@param centerX view的中心點X坐標
@param centerY view的中心點Y坐標
@param x 旋轉中心x坐標
@param y 旋轉中心y坐標
@param angle 旋轉的角度
@return CGAffineTransform對象
*/
- (CGAffineTransform)GetCGAffineTransformRotateAroundCenterX:(float)centerX centerY:(float)centerY x:(float)x y:(float)y angle:(float)angle{
CGFloat l = y - centerY;
CGFloat h = l * sin(angle);
CGFloat b = l * cos(angle);
CGFloat a = l - b;
CGFloat x1 = h;
CGFloat y1 = a;
CGAffineTransform trans = CGAffineTransformMakeTranslation(x1, y1);
trans = CGAffineTransformRotate(trans,angle);
return trans;
}
@end
看到這個類的代碼是不是更清爽了,那是因為從這個案例開始起我就把push和pop的Animator分別用一個類實現(LYNavKuGouPushAnimator
和 LYNavKuGouPopAnimator
),這樣大家理解起來思路也會更清晰啦~
(還有從這個案例開始UINavigationControllerDelegate
協議也用一個單獨的類實現,比如這個案例中的LYNavKuGouAnimationTransition
就是遵循并實現了該協議方法的類,在控制器中設置這個類的對象為代理即可)
案例二小結
該轉場動畫的精髓也就是
GetCGAffineTransformRotateAroundCenterX: centerY: x: y: angle:
方法,這個方法使得可以根據傳入的參數計算出view的變換后的位置狀態。(關于CGAffineTransform更多知識,請自行Google,這里就不贅述了)
有了變換前后的狀態,動畫效果用一個簡單的UIView動畫也就可以實現了。
三、仿微信轉場-非交互式(加強對轉場的認識)
動畫分析:首先看下示例圖
這個動畫和前兩個動畫就有點不同了,前兩個動畫是對整個界面進行的動畫操作,而這個動畫只是對縮放的圖片進行動畫操作,背景顏色僅做了漸變效果。
既然知道了只是對圖片進行動畫操作,那就不難想到,在containerView上加上一個UIImageView,然后對此做動畫操作,即可完成需求。
看下代碼:
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext{
//轉場過渡的容器view
UIView *containerView = [transitionContext containerView];
//FromVC
UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIView *fromView = fromViewController.view;
[containerView addSubview:fromView];
//ToVC
UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *toView = toViewController.view;
[containerView addSubview:toView];
toView.hidden = YES;
//圖片背景的空白view (設置和控制器的背景顏色一樣,給人一種圖片被調走的假象 [可以換種顏色看看效果])
UIView *imgBgWhiteView = [[UIView alloc] initWithFrame:self.transitionBeforeImgFrame];
imgBgWhiteView.backgroundColor = bgColor;
[containerView addSubview:imgBgWhiteView];
//有漸變的黑色背景
UIView *bgView = [[UIView alloc] initWithFrame:containerView.bounds];
bgView.backgroundColor = [UIColor blackColor];
bgView.alpha = 0;
[containerView addSubview:bgView];
//過渡的圖片
UIImageView *transitionImgView = [[UIImageView alloc] initWithImage:self.transitionImgView.image];
transitionImgView.frame = self.transitionBeforeImgFrame;
[transitionContext.containerView addSubview:transitionImgView];
[UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0.0 usingSpringWithDamping:0.7 initialSpringVelocity:0.3 options:UIViewAnimationOptionCurveLinear animations:^{
transitionImgView.frame = self.transitionAfterImgFrame;
bgView.alpha = 1;
} completion:^(BOOL finished) {
toView.hidden = NO;
[imgBgWhiteView removeFromSuperview];
[bgView removeFromSuperview];
[transitionImgView removeFromSuperview];
BOOL wasCancelled = [transitionContext transitionWasCancelled];
//設置transitionContext通知系統動畫執行完畢
[transitionContext completeTransition:!wasCancelled];
}];
}
代碼中,空白view和漸變的黑色背景都是扮演輔助角色的,而過渡圖片才是核心。要實現動畫效果,必須得有三個數據:image圖像、轉場前imageView的frame和轉場后imageView的frame。這三個數據都是從第一個VC里面計算得來的,只需按邏輯步驟一步步傳過來即可。
其中要注意的點:
(1) toView加到containerView上時,需要先隱藏,等到動畫結束時再顯示,不然toView 會蓋住整個fromView。
(2) 其中除了系統的fromView和toView,其它所有的view在動畫結束時必須移除,不然會一直在containerView上存在。
(3) popAnimator 中的fromView 不用加到containerView中了,因為此轉場在pop時不需要fromView的參與了,加上會出現整個界面沒有變化的bug。
案例三小結:
1.在VC中計算好Animator中必要的三個參數,然后依次傳遞到Animator中。
2.獲取得到傳入的數據,對過渡圖片根據做動畫處理
四、基礎轉場-交互式
交互式轉場:人為控制轉場過渡,最常見的交互轉場動畫就是系統自帶的側滑返回。
此案例請對照demo
1.實現代理方法
在LYNavBaseInteractiveAnimatedTransition
類里,相較于案例一中的UINavigationControllerDelegate
協議,要多實現一個代理方法, 即:
- (nullable id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>) animationController;
此方法會返回一個遵循了UIViewControllerInteractiveTransitioning
協議的代理對象,實現這個方法,系統轉場時,就會知道當前是否有交互式的轉場,有便執行交互轉場,無則執行普通自定義的轉場動畫。
具體代碼實現:
- (nullable id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC{
if (operation == UINavigationControllerOperationPush) {
return self.customAnimator;
}else if (operation == UINavigationControllerOperationPop){
return self.customAnimator;
}
return nil;
}
- (LYNavBaseCustomAnimator *)customAnimator
{
if (_customAnimator == nil) {
_customAnimator = [[LYNavBaseCustomAnimator alloc]init];
}
return _customAnimator;
}
- (nullable id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>) animationController{
if (self.gestureRecognizer)
return self.percentIntractive;
else
return nil;
}
- (void)setGestureRecognizer:(UIPanGestureRecognizer *)gestureRecognizer{
_gestureRecognizer = gestureRecognizer;
}
- (LYNavBasePercentDerivenInteractive *)percentIntractive{
if (!_percentIntractive) {
_percentIntractive = [[LYNavBasePercentDerivenInteractive alloc] initWithGestureRecognizer:self.gestureRecognizer];
}
return _percentIntractive;
}
其中,
(1)gestureRecognizer 是在secondVC加入的一個交互手勢,在pop時是需要傳遞過來的,后面會講到。
(2)percentIntractive 是LYNavBasePercentDerivenInteractive
類對象,這個類繼承于 UIPercentDrivenInteractiveTransition
類,UIPercentDrivenInteractiveTransition
類是交互轉場中的核心類,后面會講到。
2.新建一個繼承于 UIPercentDrivenInteractiveTransition
類的類 LYNavBasePercentDerivenInteractive
UIPercentDrivenInteractiveTransition
類是系統定義的,它遵循了 UIViewControllerInteractiveTransitioning
協議,故可做為第一節中的代理對象。
此類又定義了三個方法供交互轉場時調用:
//更新轉場過程的百分比
- (void)updateInteractiveTransition:(CGFloat)percentComplete;
//取消轉場
- (void)cancelInteractiveTransition;
//完成轉場
- (void)finishInteractiveTransition;
具體代碼實現:
- (void)gestureRecognizeDidUpdate:(UIPanGestureRecognizer *)gestureRecognizer
{
CGFloat scale = 1 - [self percentForGesture:gestureRecognizer];
switch (gestureRecognizer.state)
{
case UIGestureRecognizerStateBegan:
//沒用
break;
case UIGestureRecognizerStateChanged:
//更新百分比
[self updateInteractiveTransition:scale];
break;
case UIGestureRecognizerStateEnded:
if (scale < 0.3){
//取消轉場
[self cancelInteractiveTransition];
}
else{
//完成轉場
[self finishInteractiveTransition];
}
break;
default:
//取消轉場
[self cancelInteractiveTransition];
break;
}
}
在此類中,根據pop時傳遞過來的手勢信息,計算獲得滑動距離所占屏幕的百分比,從而根據百分比來處理轉場的取消與完成。
3.傳值
此處傳值跟之前都有不同的地方,我們這里的交互是在pop時做交互動畫,故傳值是在SecondVC中傳入的。
具體代碼:
- (void)interactiveTransitionRecognizerAction:(UIPanGestureRecognizer *)gestureRecognizer
{
CGPoint translation = [gestureRecognizer translationInView:gestureRecognizer.view];
CGFloat scale = 1 - fabs(translation.x / kScreenWidth);
scale = scale < 0 ? 0 : scale;
NSLog(@"second = %f", scale);
switch (gestureRecognizer.state) {
case UIGestureRecognizerStatePossible:
break;
case UIGestureRecognizerStateBegan:{
//1. 設置代理
self.animatedTransition = nil;
self.navigationController.delegate = self.animatedTransition;
//2. 傳值
self.animatedTransition.gestureRecognizer = gestureRecognizer;
//3. push跳轉
[self.navigationController popViewControllerAnimated:YES];
}
break;
case UIGestureRecognizerStateChanged: {
break;
}
case UIGestureRecognizerStateFailed:
case UIGestureRecognizerStateCancelled:
case UIGestureRecognizerStateEnded: {
self.animatedTransition.gestureRecognizer = nil;
}
}
}
此案例注意的點:
(1)LYNavBaseInteractiveAnimatedTransition
類中的customAnimator
是直接用的案例一中的
五、實戰 - 網友提問 - Question One
此案例為本文章2樓網友的提問解答
此案例其實和案例三的實現方式基本一致,單從轉場角度來說,不同點可以有兩個:(1)轉場前ImageView的frame不定,案例三中ImageView的frame就一個(2)轉場后的位置跟案例三不同
而這兩個不同點都是我們可以計算得到的,所以要實現這個動畫不難。
先看代碼
// 獲取指定視圖在window中的位置
- (CGRect)getFrameInWindow:(UIView *)view
{
return [view.superview convertRect:view.frame toView:nil];
}
此方法即可解決(1)中的問題,點擊cell時,傳入cell上的UIImageView對象,即可返回此View在window上的frame,這樣在轉場中的過渡ImageView就可根據此frame設置轉場前的位置了
- (CGRect)backScreenImageViewRectWithImage:(UIImage *)image{
CGSize size = image.size;
CGSize newSize;
newSize.height = kScreenWidth * 0.6;
newSize.width = newSize.height / size.height * size.width;
CGFloat imageY = 0;
CGFloat imageX = (kScreenWidth - newSize.width) * 0.5;
CGRect rect = CGRectMake(imageX, imageY, newSize.width, newSize.height);
return rect;
}
此方法可解決(2)中的問題,傳入image,即可根據自己的需求,計算得出轉場后圖片的位置。
六、圖片瀏覽器-PictureBrowse
封裝了圖片瀏覽器,demo中是封裝的模態方式跳轉,如有需求導航方式push的,請將
LYPictureBrowseInteractiveAnimatedTransition
類中遵循的協議修改為UINavigationControllerDelegate
,并修改相應的代理方法(請仿照上面幾個案例),別忘了跳轉中的present、dismiss修改為push、pop方法。
使用方法:
(1)在你的工程中導入LYPictureBrowse 文件夾,并引入LYPictureBrowse.h 頭文件
(2)構造四個必須的參數transitionImage、firstVCImgFrames、transitionImgIndex、dataSouceArray。具體構造方法請對照demo編寫
-------------------- 完結 --------------------
更新日志:
2017.07.05
(1) 更新案例0 - CATransition
(2) 更新案例一 - 基礎轉場-非交互
(3) 新增案例二 - 仿酷狗轉場-非交互
(4) 新增案例三 - 仿微信轉場-非交互
(5) 新增案例四 - 基礎轉場-交互式
(6) 新增案例五 - 實戰 - 網友提問 - Question One
更新日志:
2017.12.11
(1)新增圖片瀏覽器框架
如果看了本篇文章能對你有所幫助的小伙伴們,就來個贊給個鼓勵吧!畢竟碼字不易 O(∩_∩)O哈哈~