自定義轉場動畫(一)
兩個Controller之間的互動,相比正常的Push和Present,轉場動畫是iOS裡比較酷炫的一種效果,能夠以各種效果平滑的切換兩個不同的檢視控制器。適當的運用轉場動畫,會讓你的APP變得更加生動有趣。
這篇文章會以Present為例,講述如何自定義一個轉場動畫。後續會有Push,以及更復雜的轉場介紹。
準備:首先要兩個UIViewController和一個繼承與UIPercentDrivenInteractiveTransition的類。
ViewController1
ViewController2
PresentTransitionAnimator
場景:ViewController1 從右邊present 到 ViewController2,並支援手勢拖動返回。
PresentTransitionAnimator.h
#import <UIKit/UIKit.h>
@interface PresentTransitionAnimator : UIPercentDrivenInteractiveTransition<UIViewControllerAnimatedTransitioning,UIViewControllerTransitioningDelegate,UIGestureRecognizerDelegate>
@property (nonatomic, assign) BOOL isInteractive;//是否在拖動
@property (nonatomic, assign) BOOL isDismiss;//是present還是dismiss
@property (nonatomic, assign) float panRatio;//拖動比率
- (id)initWithModalViewController:(UIViewController *)modalViewController;//初始化
@end
PresentTransitionAnimator.m
@property (nonatomic, weak) UIViewController *modalController;//目標Controller
@property (nonatomic, strong) id<UIViewControllerContextTransitioning> transitionContext;//轉場上下文,用來獲取這兩個互動的UIViewController。
@property (nonatomic, strong) UIPanGestureRecognizer *gesture;//拖動手勢
- (instancetype)initWithModalViewController:(UIViewController *)modalViewController{
self = [super init];
if (self) {
_modalController = modalViewController;
//建立手勢
self.gesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)];
self.gesture.delegate = self;
[self.modalController.view addGestureRecognizer:self.gesture];
}
return self;
}
轉場動畫的協議
#pragma mark - UIViewControllerTransitioningDelegate Methods
//Present轉場的開始
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source{
self.isDismiss = NO;
return self;
}
//Dismiss轉場的開始
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed{
self.isDismiss = YES;
return self;
}
//手勢Present轉場的開始(由於這個demo不支援手勢拖動Present,所以返回nil)
- (id<UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id<UIViewControllerAnimatedTransitioning>)animator{
return nil;
}
//手勢Dismiss轉場的開始,同理,假如不支援手勢Dismiss,則返回nil。
- (id<UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id<UIViewControllerAnimatedTransitioning>)animator{
// Return nil if we are not interactive
if ( self.isInteractive) {
self.isDismiss = YES;
return self;
}
return nil;
}
轉場動畫需要的時間
#pragma mark - UIViewControllerAnimatedTransitioning
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext{
return 0.25;
}
轉場核心程式碼,我們轉場動畫的細節處理,適用於點選按鈕的自動跳轉或者返回。手勢拖動不會呼叫該方法,需要我們單獨處理。
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext{
-
UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *containerView = [transitionContext containerView];//獲取容器View
//*這裡需要注意!如果是present,fromViewController為ViewCOntroller1,toViewController為ViewController2.
//若是dismiss,則正好相反。
if (!self.isDismiss) {//Present
//*把目標的view放到容器View中
[containerView addSubview:toViewController.view];
toViewController.view.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
//*轉場開始前,toViewController的View位於螢幕的右側看不見的地方
CGRect startRect = CGRectMake(CGRectGetWidth(containerView.bounds), 0, CGRectGetWidth(containerView.bounds), CGRectGetHeight(containerView.bounds));
toViewController.view.frame = startRect;
[fromViewController beginAppearanceTransition:NO animated:YES];
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
fromViewController.view.alpha = self.behindViewAlpha;
//*動畫的執行過程是從右側看不見的地方,往左移動到到螢幕的位置
toViewController.view.frame = CGRectMake( 0, 0,
CGRectGetWidth(toViewController.view.frame),
CGRectGetHeight(toViewController.view.frame) );
} completion:^(BOOL finished) {
//*轉場結束,把操作權給系統。
[fromViewController endAppearanceTransition];
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
}
//*dismiss
else{
[containerView bringSubviewToFront:fromViewController.view];
toViewController.view.alpha = self.behindViewAlpha;
CGRect endRect = CGRectMake(CGRectGetWidth(fromViewController.view.frame), 0, CGRectGetWidth(fromViewController.view.frame), CGRectGetHeight(fromViewController.view.frame));
[toViewController beginAppearanceTransition:YES animated:YES];
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
toViewController.view.alpha = 1.0f;
fromViewController.view.frame = endRect;
} completion:^(BOOL finished) {
[toViewController endAppearanceTransition];
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
}
}
備註:可以看到轉場動畫的present和dismiss過程,都是在一個函式內完成的,主要區別就是處理fromViewController.view和toViewController.view的frame的方式不同罷了。
處理手勢:
- (void)handlePan:(UIPanGestureRecognizer *)recognizer{
CGPoint location = [recognizer locationInView:self.modalController.view.window];
CGPoint point = [recognizer translationInView:self.modalController.view];
//禁止左滑
if (point.x<0) {
return;
}
//*開始滑動
if (recognizer.state == UIGestureRecognizerStateBegan) {
self.isInteractive = YES;
self.panLocationStart = location.x;
[self.modalController dismissViewControllerAnimated:YES completion:nil];
}
//*滑動中
else if (recognizer.state == UIGestureRecognizerStateChanged){
CGFloat animationRatio = 0;
animationRatio = (location.x - self.panLocationStart) / ( CGRectGetWidth([self.modalController view].bounds) );
self.panRatio = animationRatio;
[self updateInteractiveTransition:animationRatio];
}
//*滑動
else if (recognizer.state == UIGestureRecognizerStateEnded){
//*控制滑動到某程度,鬆手後是dismiss還是恢復到滑動前的狀態。
if ( self.panRatio > 0.12 ) {
[self finishInteractiveTransition];
}else{
[self cancelInteractiveTransition];
}
self.isInteractive = NO;
}
}
隨著拖動的位置更新view的frame以及透明度
- (void)updateInteractiveTransition:(CGFloat)percentComplete{
if (!self.bounces && percentComplete < 0) {
percentComplete = 0;
}
id<UIViewControllerContextTransitioning> transitionContext = self.transitionContext;
UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
toViewController.view.alpha = self.behindViewAlpha + ( (1 - self.behindViewAlpha) * ABS(percentComplete) );
CGRect updateRect = CGRectMake((CGRectGetWidth(fromViewController.view.bounds) * percentComplete),
0,
CGRectGetWidth(fromViewController.view.frame),
CGRectGetHeight(fromViewController.view.frame));
if ( isnan(updateRect.origin.x) || isinf(updateRect.origin.x) ) {
updateRect.origin.x = 0;
}
if ( isnan(updateRect.origin.y) || isinf(updateRect.origin.y) ) {
updateRect.origin.y = 0;
}
fromViewController.view.frame = updateRect;
}
完成拖動返回
- (void)finishInteractiveTransition{
id<UIViewControllerContextTransitioning> transitionContext = self.transitionContext;
UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
CGRect endRect;
endRect = CGRectMake( CGRectGetWidth(fromViewController.view.bounds),
0,
CGRectGetWidth(fromViewController.view.frame),
CGRectGetHeight(fromViewController.view.frame) );
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
toViewController.view.alpha = 1.0f;
fromViewController.view.frame = endRect;
}completion:^(BOOL finished) {
[toViewController endAppearanceTransition];
[transitionContext completeTransition:YES];
}];
}
取消拖動
- (void)cancelInteractiveTransition{
id<UIViewControllerContextTransitioning> transitionContext = self.transitionContext;
UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
[toViewController beginAppearanceTransition:NO animated:YES];
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
toViewController.view.alpha = self.behindViewAlpha;
fromViewController.view.frame = CGRectMake( 0, 0,
CGRectGetWidth(fromViewController.view.frame),
CGRectGetHeight(fromViewController.view.frame) );
} completion:^(BOOL finished) {
[toViewController endAppearanceTransition];
[transitionContext completeTransition:NO];
}];
}
完成手勢拖動的協議 UIViewControllerInteractiveTransitioning
#pragma mark - UIViewControllerInteractiveTransitioning
- (void)startInteractiveTransition:(id<UIViewControllerContextTransitioning>)transitionContext{
self.transitionContext = transitionContext;
UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
[toViewController beginAppearanceTransition:YES animated:YES];
toViewController.view.alpha = self.behindViewAlpha;
UIView *containerView = [transitionContext containerView];
[containerView bringSubviewToFront:fromViewController.view];
}
完成轉場
#pragma mark - 完成轉場
- (void)animationEnded:(BOOL)transitionCompleted{
self.isInteractive = NO;
self.transitionContext = nil;
}
如何使用:
ViewController1.m
ViewController2 *vc = [[ViewController2 alloc] init];
[self presentViewController:vc animated:YES completion:nil];
ViewController2.m
- (instancetype)init
{
self = [super init];
if ( self ) {
_animator = [[PXPresentTransitionAnimator alloc] initWithModalViewController:self];
self.transitioningDelegate = self.animator;
self.modalPresentationStyle = UIModalPresentationCustom;
}
return self;
}
注:Present轉場和Push轉場大致上是差不多的,至少在原理上基本相似。但還是有差異的,如果處理不好這些差異,會導致黑屏,甚至崩潰的問題。
Present轉場是從fromView轉換到toView,根檢視 fromView 也參與了轉場。在轉場結束後,fromView 可能依然可見。
而Push轉場其根檢視並未參與轉場,轉場結束後fromView 會被主動移出檢視結構。
下一節會針對Push的轉場動畫進行程式碼詮釋,這樣對比著會更加清晰。
相關文章
- TransitionAnimation自定義轉場動畫NaN動畫
- 自定義TabBar動畫效果 - 頁面轉場(Swift)tabBar動畫Swift
- [譯]Workcation App – 第一部分 . 自定義 Fragment 轉場動畫APPFragment動畫
- Android SeekBar 自定義thumb,thumb旋轉動畫效果Android動畫
- react-navigation自定義StackNavigator頁面跳轉動畫ReactNavigation動畫
- Flutter動畫之自定義動畫元件-FlutterLayoutFlutter動畫元件
- Android轉場動畫一說Android動畫
- 搞定動畫之 JQuery 中的自定義動畫動畫jQuery
- Java 給PPT新增動畫效果(預設動畫/自定義動畫)Java動畫
- Android自定義View播放Gif動畫AndroidView動畫
- Android 自定義View之下雨動畫AndroidView動畫
- Flutter自定義CupertinoPageRoute進入動畫Flutter動畫
- XamarinAndroid元件教程設定自定義子元素動畫(一)NaNAndroid元件動畫
- Android 轉場動畫Android動畫
- 萬彩動畫大師教程 | 自定義動畫函式動畫函式
- Qt自定義動畫插值函式QT動畫函式
- Flutter 建立自定義路由過渡動畫Flutter路由動畫
- Android 自定義View:屬性動畫(六)AndroidView動畫
- 動畫函式的繪製及自定義動畫函式動畫函式
- 【動畫消消樂】HTML+CSS 自定義載入動畫 065動畫HTMLCSS
- 【動畫消消樂】HTML+CSS 自定義載入動畫 062動畫HTMLCSS
- 【動畫消消樂】HTML+CSS 自定義載入動畫 061動畫HTMLCSS
- 自定義Push/Pop和Present/Dismiss轉場
- 萬彩動畫大師教程 | 移動動畫自定義加速度動畫
- android 自定義酷炫進度條動畫Android動畫
- 【Android】自定義ProgressView-進度條動畫AndroidView動畫
- Android 自定義帶動畫的柱狀圖Android動畫
- 「HTML+CSS」--自定義載入動畫【005】HTMLCSS動畫
- 「HTML+CSS」--自定義載入動畫【006】HTMLCSS動畫
- 「HTML+CSS」--自定義載入動畫【016】HTMLCSS動畫
- 「HTML+CSS」--自定義載入動畫【015】HTMLCSS動畫
- 「HTML+CSS」--自定義載入動畫【026】HTMLCSS動畫
- 「HTML+CSS」--自定義載入動畫【011】HTMLCSS動畫
- 「HTML+CSS」--自定義載入動畫【010】HTMLCSS動畫
- 「HTML+CSS」--自定義載入動畫【008】HTMLCSS動畫
- 「HTML+CSS」--自定義載入動畫【007】HTMLCSS動畫
- 「HTML+CSS」--自定義載入動畫【009】HTMLCSS動畫
- flutter 用 CustomPaint 畫一個自定義的 CircleProgressBar (一)FlutterAI
- 【動畫消消樂】HTML+CSS 自定義載入動畫:怦然心跳 066動畫HTMLCSS