前言
(呃呃呃,其實本文不算是動畫實戰(zhàn)懦铺,只是用到了一點動畫捉貌,算了沒差~)
在平時使用的app中,部分app的部分轉(zhuǎn)場動畫與傳統(tǒng)的動畫不一樣冬念,其實他們使用的是自定義轉(zhuǎn)場動畫趁窃。本文記錄的是自定義轉(zhuǎn)場動畫的實現(xiàn)。
效果圖
主要思路
最重要的是需要創(chuàng)建一個繼承NSObject的類急前,并且遵守UIViewControllerAnimatedTransitioning協(xié)議醒陆。我暫時給這個類命名為YQAnimatedTransition。這個協(xié)議就是用來自定義轉(zhuǎn)場動畫的裆针。點進去看看:
@protocol UIViewControllerAnimatedTransitioning <NSObject>
// This is used for percent driven interactive transitions, as well as for
// container controllers that have companion animations that might need to
// synchronize with the main animation.
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext;
// This method can only be a nop if the transition is interactive and not a percentDriven interactive transition.
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;
@end
發(fā)現(xiàn)這個協(xié)議有兩個必須實現(xiàn)的方法统求。第一個方法是設(shè)置動畫的時間。第二個方法是設(shè)置動畫据块。
好了码邻,當這個YQAnimatedTransition類設(shè)置好后,在控制器需要用它的時候調(diào)用它另假。這個下面會具體說像屋。
開始吃鍵盤
1.YQAnimatedTransition創(chuàng)建
首先創(chuàng)建一個繼承NSObject,并且遵守UIViewControllerAnimatedTransitioning協(xié)議的類YQAnimatedTransition边篮。
其次考慮到轉(zhuǎn)場一共有四種方式:push己莺,pop,present戈轿,dismiss凌受。所以我加了一個枚舉,用來設(shè)置轉(zhuǎn)場的類型思杯。
typedef enum {
YQAnimatedTransitionTypePush,
YQAnimatedTransitionTypePop,
YQAnimatedTransitionTypePresent,
YQAnimatedTransitionTypeDismiss
}YQAnimatedTransitionType;
為了方便這個類的使用胜蛉,我加了一個類方法挠进,在類方法中進行初始化且設(shè)置轉(zhuǎn)場類型:
//.h
+ (YQAnimatedTransition *)animatedTransitionWithType:(YQAnimatedTransitionType)type;
//.m
+ (YQAnimatedTransition *)animatedTransitionWithType:(YQAnimatedTransitionType)type
{
YQAnimatedTransition *animatedTransition = [[YQAnimatedTransition alloc] init];
animatedTransition.type = type;
return animatedTransition;
}
2.協(xié)議方法實現(xiàn)
下面是重點了!既然這個類遵循UIViewControllerAnimatedTransitioning協(xié)議誊册,就需要實現(xiàn)協(xié)議方法领突。
直接上代碼了。
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext
{
return 0.5;
}
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
self.transitionContext = transitionContext;
if (self.type == YQAnimatedTransitionTypePush) {
} else if (self.type == YQAnimatedTransitionTypePresent) {
} else if (self.type == YQAnimatedTransitionTypeDismiss) {
} else {
}
}
解釋一下案怯。第一個方法的意思是我設(shè)置轉(zhuǎn)場動畫為0.5秒君旦。第二個方法是在設(shè)置動畫過程。由于篇幅過長嘲碱,我暫時先省略啦~
重點說說上面的第二方法:動畫設(shè)置金砍。
不管是pop或者dismiss等等,只要控制器轉(zhuǎn)場都會執(zhí)行這第二個方法麦锯。所以首先在這個方法中進行判斷恕稠,是屬于哪種轉(zhuǎn)場方式。然后再自定義動畫离咐。
以push為例子:
if (self.type == YQAnimatedTransitionTypePush) {
// 獲得即將消失的vc的v
UIView *fromeView = [transitionContext viewForKey:UITransitionContextFromViewKey];
// 獲得即將出現(xiàn)的vc的v
UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
// 獲得容器view
UIView *containerView = [transitionContext containerView];
[containerView addSubview:fromeView];
[containerView addSubview:toView];
UIBezierPath *startBP = [UIBezierPath bezierPathWithOvalInRect:CGRectMake((containerView.frame.size.width-100)/2, 100, 100, 100)];
CGFloat radius = 1000;
UIBezierPath *finalBP = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(150 - radius, 150 -radius, radius*2, radius*2)];
CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.path = finalBP.CGPath;
toView.layer.mask = maskLayer;
//執(zhí)行動畫
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"path"];
animation.fromValue = (__bridge id _Nullable)(startBP.CGPath);
animation.toValue = (__bridge id _Nullable)(finalBP.CGPath);
animation.duration = [self transitionDuration:transitionContext];
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[maskLayer addAnimation:animation forKey:@"path"];
}
這里用到了動畫的知識谱俭,改變的是layer的path屬性奉件,讓layer從小圓變成了大圓宵蛀。
一直看代碼和文字也累了吧,先看看現(xiàn)在push的效果好了县貌。(注意哈术陶,這里為了看效果,我已經(jīng)在控制器寫了調(diào)用該類的代碼了煤痕,至于怎么調(diào)用梧宫,下面會說,先看效果吧~)
首先可以發(fā)現(xiàn)一個問題摆碉,就是返回不了了塘匣。解決辦法是:在動畫完成后加一行代碼
[transitionContext completeTransition:YES];
。但是巷帝,問題又來了忌卤,這行代碼加在哪里呢。直接加在動畫設(shè)置后面效果:
好像沒問題楞泼,但是仔細觀察發(fā)現(xiàn)navBar存在push的太早問題驰徊。如果你和我一樣覺得這個很丑,那就換一個方法堕阔。
給animation設(shè)置代理棍厂,然后該類監(jiān)聽動畫,當動畫結(jié)束的時候再調(diào)用這行代碼超陆,這樣就沒問題啦牺弹。當然別忘了遵循動畫協(xié)議CAAnimationDelegate。
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
//告訴系統(tǒng)轉(zhuǎn)場動畫完成
[self.transitionContext completeTransition:YES];
}
這樣動畫就寫好了,至于present例驹,dismiss等捐韩,也類似,就不再說啦鹃锈。
上面動畫實現(xiàn)中有一個layer.mask屬性荤胁,我在本文最后會解釋。
3.控制器調(diào)用
最后一步就是控制器調(diào)用剛寫的類了屎债。
a.push/pop方式如下:
在控制器中遵循UINavigationControllerDelegate協(xié)議仅政,并實現(xiàn)協(xié)議方法:
- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC{
if (operation == UINavigationControllerOperationPush) {
YQAnimatedTransition *animatedTransition = [YQAnimatedTransition animatedTransitionWithType:YQAnimatedTransitionTypePush];
return animatedTransition;
}
return nil;
}
b.present/dismiss方式如下:
在控制器中遵循UIViewControllerTransitioningDelegate協(xié)議,并實現(xiàn)方法:
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
{
return [YQAnimatedTransition animatedTransitionWithType:YQAnimatedTransitionTypePresent];
}
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
{
return [YQAnimatedTransition animatedTransitionWithType:YQAnimatedTransitionTypeDismiss];
}
到這里盆驹,轉(zhuǎn)場動畫就實現(xiàn)了圆丹。
4.細節(jié)補充
上圖和效果圖比較還是有差別的,少了一個過渡動畫躯喇。當用戶點擊cell的時候辫封,頭像會移動且放大到詳細頁面那個頭像那個位置。實現(xiàn)代碼:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
// 獲得點擊的cell
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
CGRect rectInTableView = [tableView rectForRowAtIndexPath:indexPath];
// 獲得點擊cell的frame
CGRect rect = [tableView convertRect:rectInTableView toView:[tableView superview]];
// 設(shè)置selectImageView的位置和圖片
self.selectImageView.image = cell.imageView.image;
self.selectImageView.frame = CGRectMake(cell.imageView.frame.origin.x, rect.origin.y, cell.imageView.frame.size.width, cell.imageView.frame.size.height);
// 動畫
[UIView animateWithDuration:0.5 animations:^{
self.selectImageView.frame = CGRectMake(0, 64, self.view.bounds.size.width, self.view.bounds.size.width);
} completion:^(BOOL finished) {
[self.navigationController pushViewController:detail animated:YES];
}];
}
獲取當前cell方法以及cell相對屏幕的位置兩個方法每次都忘記廉丽,所以加粗倦微,方便以后找。
上面代碼的效果圖:
現(xiàn)在的問題是返回的時候 self.selectImageView還在那里正压,所以需要在轉(zhuǎn)場結(jié)束后使 self.selectImageView消失欣福。
解決方法:
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated
{
if (viewController != self) {
self.selectImageView.frame = CGRectNull;
}
}
轉(zhuǎn)場后,設(shè)置frame為CGRectNull焦履,這樣就消失啦~
layer.mask屬性
其實這個mask屬性用到的地方還是蠻多的拓劝。比如新手引導(雖然現(xiàn)在都是圖片),還有微信的照片紅包嘉裤。下面說說這個屬性郑临。
mask是一個layer層,并且作為背景層和組成層之間的一個遮罩層通道屑宠,默認是nil厢洞。
還是在這個項目中,在列表控制器的- (void)viewDidLoad
方法中加如下代碼
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(100, 100, 200, 200)].CGPath;
self.view.layer.mask = shapeLayer;
效果圖:
發(fā)現(xiàn)就只有l(wèi)ayer那一塊顯示出來侨把,其余全部白色了犀变。至于其余部分的顏色 是由 window.backgroundColor控制。
改成黑色:
當我代碼改成這樣:(一條線的時候)
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(100, 100)];
[path addLineToPoint:CGPointMake(100, 500)];
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.path = path.CGPath;
shapeLayer.lineWidth = 20;
self.view.layer.mask = shapeLayer;
發(fā)現(xiàn)不起作用秋柄,即使線寬為20获枝。
當代碼為三角形:
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(100, 100)];
[path addLineToPoint:CGPointMake(100, 500)];
[path addLineToPoint:CGPointMake(200, 500)];
[path closePath];
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.path = path.CGPath;
shapeLayer.lineWidth = 20;
self.view.layer.mask = shapeLayer;
綜上可以說明:layer的路徑必須要封閉才能起作用。
最后
本文github地址:https://github.com/JabberYQ/animatedTransitionDemo