我该如何实现视图之间的滑动/滑动动画?

28

我有几个视图需要在iOS程序中进行滑动切换。目前,我使用模态样式进行视图之间的切换,使用交叉淡化动画。然而,我想要像主屏幕等地方看到的滑动/滑块动画。我不知道如何编写这样的过渡效果,而且该动画样式并不是可用的模态转换样式之一。有人可以给我提供代码示例吗?它不需要是模态模型或其他什么,我只是发现这最容易。


如果您需要真正的自定义转换,请参考2018年的完整教程 https://dev59.com/oXE95IYBdhLWcg3wp_qn#48081504 - Fattie
在iOS中使用分页非常简单:https://dev59.com/eWMl5IYBdhLWcg3wcmrD#26024779 - Fattie
如果您需要将动画实际连接/同步到手指上,这里有一篇关于UIPercentDrivenInteractiveTransition的好文章:https://theswiftdev.com/ios-custom-transition-tutorial-in-swift/ - Fattie
3个回答

37

iOS 7以后,如果你想要为两个视图控制器之间的转换添加动画效果,你可以使用自定义转换功能,这在2013年WWDC视频 Custom Transitions Using View Controllers 中有所讲解。例如,如果你想要自定义一个新视图控制器的呈现方式,你可以:

  1. 目标视图控制器将指定 self.modalPresentationStyletransitioningDelegate 以进行演示动画:

    - (instancetype)initWithCoder:(NSCoder *)coder {
        self = [super initWithCoder:coder];
        if (self) {
            self.modalPresentationStyle = UIModalPresentationCustom;
            self.transitioningDelegate = self;
        }
        return self;
    }
    
  2. 该委托(在此示例中为视图控制器本身)将符合 UIViewControllerTransitioningDelegate 并实现:

    - (id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented
                                                                       presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
        return [[PresentAnimator alloc] init];
    }
    
    // 在 iOS 8 及更高版本中,您还需要指定一个表示控制器
    
    - (UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)presented presentingViewController:(UIViewController *)presenting sourceViewController:(UIViewController *)source {
        return [[PresentationController alloc] initWithPresentedViewController:presented presentingViewController:presenting];
    }
    
  3. 您将实现一个执行所需动画的动画器:

    @interface PresentAnimator : NSObject <UIViewControllerAnimatedTransitioning>
    
    @end
    
    @implementation PresentAnimator
    
    - (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext {
        return 0.5;
    }
    
    // 在下面执行任何所需的动画
    
    - (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
        UIViewController* toViewController   = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
        UIViewController* fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    
        [[transitionContext containerView] addSubview:toViewController.view];
        CGFloat width = fromViewController.view.frame.size.width;
        CGRect originalFrame = fromViewController.view.frame;
        CGRect rightFrame = originalFrame; rightFrame.origin.x += width;
        CGRect leftFrame  = originalFrame; leftFrame.origin.x  -= width / 2.0;
        toViewController.view.frame = rightFrame;
    
        toViewController.view.layer.shadowColor = [[UIColor blackColor] CGColor];
        toViewController.view.layer.shadowRadius = 10.0;
        toViewController.view.layer.shadowOpacity = 0.5;
    
        [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
            fromViewController.view.frame = leftFrame;
            toViewController.view.frame = originalFrame;
            toViewController.view.layer.shadowOpacity = 0.5;
        } completion:^(BOOL finished) {
            [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
        }];
    }
    
    @end
    
  4. 您还将实现一个表示控制器,以便为您清理视图层次结构。在这种情况下,由于我们完全覆盖了呈现视图,因此可以在转换完成时从层次结构中删除它:

    @interface PresentationController: UIPresentationController
    @end
    
    @implementation PresentationController
    
    - (BOOL)shouldRemovePresentersView {
        return true;
    }
    
    @end
    
  5. 如果您希望此手势是交互式的,则可以选择:

    • 创建一个交互式控制器(通常是 UIPercentDrivenInteractiveTransition);

    • 让您的 UIViewControllerAnimatedTransitioning 也实现 interactionControllerForPresentation,它将显然返回上述交互式控制器;

    • 有一个手势(或其他什么)来更新 interactionController

所有这些都在前面提到的使用视图控制器自定义转场中有详细描述。

关于自定义导航控制器的推送/弹出动画,可以参考导航控制器自定义转场动画


以下是我的原始答案副本,早于自定义转换。


@sooper的回答是正确的,CATransition可以产生你想要的效果。

但是,顺便说一下,如果你的背景不是白色,CATransitionkCATransitionPush在过渡结束时会有一个奇怪的淡入淡出效果,可能会分散注意力(特别是在图像之间导航时,它会产生轻微的闪烁效果)。如果你遇到了这个问题,我发现这个简单的过渡效果非常优雅:你可以将“下一个视图”准备好,使其刚好在屏幕右侧,然后同时动画移动当前视图到屏幕左侧并将下一个视图移动到当前视图的位置。请注意,在我的示例中,我在单个视图控制器中动画显示和隐藏主视图中的子视图,但你可能已经明白了:

float width = self.view.frame.size.width;
float height = self.view.frame.size.height;

// my nextView hasn't been added to the main view yet, so set the frame to be off-screen

[nextView setFrame:CGRectMake(width, 0.0, width, height)];

// then add it to the main view

[self.view addSubview:nextView];

// now animate moving the current view off to the left while the next view is moved into place

[UIView animateWithDuration:0.33f 
                      delay:0.0f 
                    options:UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionAllowUserInteraction
                 animations:^{
                     [nextView setFrame:currView.frame];
                     [currView setFrame:CGRectMake(-width, 0.0, width, height)];
                 }
                 completion:^(BOOL finished){
                     // do whatever post processing you want (such as resetting what is "current" and what is "next")
                 }];

清楚地说,你需要根据你设置的控件进行调整,但这将产生一个非常简单的过渡效果,没有任何淡入淡出之类的东西,只有一个漂亮流畅的过渡效果。
注意:首先,无论是这个例子还是CATransition的例子,都不太像你提到的SpringBoard主屏幕动画(即如果你在滑动过程中停下来或者返回等)。这些转换效果一旦启动,就会立即发生。如果你需要实时交互,也是可以做到的,但是方式不同。
更新:
如果你想使用一个持续跟踪用户手指的连续手势,可以使用UIPanGestureRecognizer而不是UISwipeGestureRecognizer,并且我认为在这种情况下,使用animateWithDuration比CATransition更好。我修改了我的handlePanGesture函数来改变frame坐标以配合用户的手势,然后我修改了上面的代码,在用户释放手指时完成动画。效果还不错。我觉得用CATransition很难做到这一点。
例如,您可以在控制器的主视图上创建手势处理程序:
[self.view addGestureRecognizer:[[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)]];

处理程序可能看起来像这样:

- (void)handlePan:(UIPanGestureRecognizer *)gesture
{
    // transform the three views by the amount of the x translation

    CGPoint translate = [gesture translationInView:gesture.view];
    translate.y = 0.0; // I'm just doing horizontal scrolling

    prevView.frame = [self frameForPreviousViewWithTranslate:translate];
    currView.frame = [self frameForCurrentViewWithTranslate:translate];
    nextView.frame = [self frameForNextViewWithTranslate:translate];

    // if we're done with gesture, animate frames to new locations

    if (gesture.state == UIGestureRecognizerStateCancelled ||
        gesture.state == UIGestureRecognizerStateEnded ||
        gesture.state == UIGestureRecognizerStateFailed)
    {
        // figure out if we've moved (or flicked) more than 50% the way across

        CGPoint velocity = [gesture velocityInView:gesture.view];
        if (translate.x > 0.0 && (translate.x + velocity.x * 0.25) > (gesture.view.bounds.size.width / 2.0) && prevView)
        {
            // moving right (and/or flicked right)

            [UIView animateWithDuration:0.25
                                  delay:0.0
                                options:UIViewAnimationOptionCurveEaseOut
                             animations:^{
                                 prevView.frame = [self frameForCurrentViewWithTranslate:CGPointZero];
                                 currView.frame = [self frameForNextViewWithTranslate:CGPointZero];
                             }
                             completion:^(BOOL finished) {
                                 // do whatever you want upon completion to reflect that everything has slid to the right

                                 // this redefines "next" to be the old "current",
                                 // "current" to be the old "previous", and recycles
                                 // the old "next" to be the new "previous" (you'd presumably.
                                 // want to update the content for the new "previous" to reflect whatever should be there

                                 UIView *tempView = nextView;
                                 nextView = currView;
                                 currView = prevView;
                                 prevView = tempView;
                                 prevView.frame = [self frameForPreviousViewWithTranslate:CGPointZero];
                             }];
        }
        else if (translate.x < 0.0 && (translate.x + velocity.x * 0.25) < -(gesture.view.frame.size.width / 2.0) && nextView)
        {
            // moving left (and/or flicked left)

            [UIView animateWithDuration:0.25
                                  delay:0.0
                                options:UIViewAnimationOptionCurveEaseOut
                             animations:^{
                                 nextView.frame = [self frameForCurrentViewWithTranslate:CGPointZero];
                                 currView.frame = [self frameForPreviousViewWithTranslate:CGPointZero];
                             }
                             completion:^(BOOL finished) {
                                 // do whatever you want upon completion to reflect that everything has slid to the left

                                 // this redefines "previous" to be the old "current",
                                 // "current" to be the old "next", and recycles
                                 // the old "previous" to be the new "next". (You'd presumably.
                                 // want to update the content for the new "next" to reflect whatever should be there

                                 UIView *tempView = prevView;
                                 prevView = currView;
                                 currView = nextView;
                                 nextView = tempView;
                                 nextView.frame = [self frameForNextViewWithTranslate:CGPointZero];
                             }];
        }
        else
        {
            // return to original location

            [UIView animateWithDuration:0.25
                                  delay:0.0
                                options:UIViewAnimationOptionCurveEaseOut
                             animations:^{
                                 prevView.frame = [self frameForPreviousViewWithTranslate:CGPointZero];
                                 currView.frame = [self frameForCurrentViewWithTranslate:CGPointZero];
                                 nextView.frame = [self frameForNextViewWithTranslate:CGPointZero];
                             }
                             completion:NULL];
        }
    }
}

这使用了一些简单的frame方法,你可以为你想要的用户体验定义它们:

- (CGRect)frameForPreviousViewWithTranslate:(CGPoint)translate
{
    return CGRectMake(-self.view.bounds.size.width + translate.x, translate.y, self.view.bounds.size.width, self.view.bounds.size.height);
}

- (CGRect)frameForCurrentViewWithTranslate:(CGPoint)translate
{
    return CGRectMake(translate.x, translate.y, self.view.bounds.size.width, self.view.bounds.size.height);
}

- (CGRect)frameForNextViewWithTranslate:(CGPoint)translate
{
    return CGRectMake(self.view.bounds.size.width + translate.x, translate.y, self.view.bounds.size.width, self.view.bounds.size.height);
}

你的具体实现肯定会有所不同,但希望这能说明问题。

在阐述了所有这些内容(补充和澄清了这个旧答案)之后,我应该指出我不再使用这种技术。现在,我通常使用一个 UIScrollView(打开“分页”)或(在iOS 6中)一个 UIPageViewController。这让你摆脱了编写此类手势处理程序的麻烦(并享受额外的功能,如滚动条、弹跳等)。在 UIScrollView 实现中,我只需响应 scrollViewDidScroll 事件,以确保我正在惰性加载必要的子视图。


在特定控制器中的子视图之间进行转换。 - StinkyCat
@StinkyCat 当我最初编写此代码时,我手动向窗口添加子视图并移动它们。随后我发现了一些更简单的方法,包括UIPageViewController(iOS 6.0及以上版本,如果需要侧滑视图)或UIScrollView(在其中打开分页功能,然后响应scrollViewWillScroll代理方法来确定要添加为子视图的视图。这些方法很好用,因为它们帮助您避免创建自己的手势识别器,而且可以使用滚动条或页面计数器等工具。 - Rob
1
@StinkyCat 我已经扩展了我的答案,告诉你如何使用平移手势识别器来拖动子视图,然后在你松开手时对它们进行动画处理。话虽如此,我通常会使用 UIScrollView 来享受弹跳效果等功能。无论如何,请参见上面的扩展答案。 - Rob
@Rob,你能否提供一个小的gif或类似的东西,以便能够理解你代码的结果吗? - Ömer Faruk Almalı
@Rob 因为我即将实现这样的动画,如果你能提供结果让我看一下,那将不胜感激。 - Ömer Faruk Almalı
显示剩余11条评论

9
您可以创建一个 CATransition 动画。下面是一个示例,演示了如何将第二个视图(从左侧)滑入屏幕,并将当前视图推出:
UIView *theParentView = [self.view superview];

CATransition *animation = [CATransition animation];
[animation setDuration:0.3];
[animation setType:kCATransitionPush];
[animation setSubtype:kCATransitionFromLeft];
[animation setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];

[theParentView addSubview:yourSecondViewController.view];
[self.view removeFromSuperview];

[[theParentView layer] addAnimation:animation forKey:@"showSecondViewController"];

7

如果你想要实现类似于Springboard在切换页面时的滚动/滑动效果,为什么不使用UIScrollView呢?

CGFloat width = 320;
CGFloat height = 400;
NSInteger pages = 3;

UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0,0,width,height)];
scrollView.contentSize = CGSizeMake(width*pages, height);
scrollView.pagingEnabled = YES;

然后使用UIPageControl来获取这些点。 :)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接