web-dev-qa-db-fra.com

Comment puis-je implémenter une animation de balayage / glissement entre les vues?

J'ai quelques vues entre lesquelles je veux glisser dans un programme iOS. En ce moment, je balaie entre eux en utilisant un style modal, avec une animation de dissolution croisée. Cependant, je veux avoir une animation de glissement/glissement comme vous voyez sur l'écran d'accueil et autres. Je ne sais pas comment coder une telle transition, et le style d'animation n'est pas un style de transition modale disponible. Quelqu'un peut-il me donner un exemple du code? Il n'a pas besoin d'être un modèle modal ou quoi que ce soit, je viens de trouver cela plus facile.

27
user1368136

Depuis iOS 7, si vous souhaitez animer la transition entre deux contrôleurs de vue, vous utiliseriez des transitions personnalisées, comme indiqué dans la vidéo WWDC 2013 Transitions personnalisées à l'aide de contrôleurs de vue . Par exemple, pour personnaliser la présentation d'un nouveau contrôleur de vue, vous devez:

  1. Le contrôleur de vue de destination spécifierait le self.modalPresentationStyle et transitioningDelegate pour l'animation de présentation:

    - (instancetype)initWithCoder:(NSCoder *)coder {
        self = [super initWithCoder:coder];
        if (self) {
            self.modalPresentationStyle = UIModalPresentationCustom;
            self.transitioningDelegate = self;
        }
        return self;
    }
    
  2. Ce délégué (dans cet exemple, le contrôleur de vue lui-même) serait conforme à UIViewControllerTransitioningDelegate et implémenterait:

    - (id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented
                                                                       presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
        return [[PresentAnimator alloc] init];
    }
    
    // in iOS 8 and later, you'd also specify a presentation controller
    
    - (UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)presented presentingViewController:(UIViewController *)presenting sourceViewController:(UIViewController *)source {
        return [[PresentationController alloc] initWithPresentedViewController:presented presentingViewController:presenting];
    }
    
  3. Vous implémenteriez un animateur qui effectuerait l'animation souhaitée:

    @interface PresentAnimator : NSObject <UIViewControllerAnimatedTransitioning>
    
    @end
    
    @implementation PresentAnimator
    
    - (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext {
        return 0.5;
    }
    
    // do whatever animation you want below
    
    - (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. Vous devez également implémenter un contrôleur de présentation qui s'occupe de nettoyer la hiérarchie des vues pour vous. Dans ce cas, puisque nous superposons complètement la vue de présentation, nous pouvons la supprimer de la hiérarchie lorsque la transition est terminée:

    @interface PresentationController: UIPresentationController
    @end
    
    @implementation PresentationController
    
    - (BOOL)shouldRemovePresentersView {
        return true;
    }
    
    @end
    
  5. Facultativement, si vous vouliez que ce geste soit interactif, vous devriez également:

    • Créez un contrôleur d'interaction (généralement un UIPercentDrivenInteractiveTransition);

    • Demandez à votre UIViewControllerAnimatedTransitioning d'implémenter également interactionControllerForPresentation, ce qui retournerait évidemment le contrôleur d'interaction susmentionné;

    • Avoir un geste (ou ce que vous avez) qui met à jour le interactionController

Tout cela est décrit dans ce qui précède Transitions personnalisées à l'aide de contrôleurs de vue .

Par exemple, pour personnaliser le Push/Pop du contrôleur de navigation, voir Animation de transition personnalisée du contrôleur de navigation


Ci-dessous, veuillez trouver une copie de ma réponse d'origine, antérieure aux transitions personnalisées.


La réponse de @ sooper est correcte, à savoir que CATransition peut produire l'effet que vous recherchez.

Mais, au fait, si votre arrière-plan n'est pas blanc, le kCATransitionPush de CATransition a un fondu étrange à la fin de la transition qui peut être gênant (lors de la navigation entre images, surtout, cela lui donne un effet légèrement scintillant). Si vous en souffrez, j'ai trouvé cette transition simple très gracieuse: vous pouvez préparer votre "vue suivante" pour qu'elle soit juste hors écran vers la droite, puis animer le déplacement de la vue actuelle hors écran vers la gauche pendant que vous simultanément animer la vue suivante pour aller à l'endroit où se trouvait la vue actuelle. Notez que dans mes exemples, j'anime des sous-vues dans et hors de la vue principale dans un contrôleur de vue unique, mais vous avez probablement l'idée:

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")
                 }];

De toute évidence, vous devriez modifier cela pour savoir comment vous avez configuré tous vos contrôles, mais cela donne une transition très simple, sans décoloration ou quelque chose comme ça, juste une transition douce et agréable.

Une mise en garde: tout d'abord, ni cet exemple, ni l'exemple CATransition, ne sont tout à fait comme l'animation de l'écran d'accueil SpringBoard (dont vous avez parlé), qui est continue (c'est-à-dire si vous êtes à mi-chemin d'un balayage, vous peut s'arrêter et revenir en arrière ou autre). Ces transitions sont celles qui une fois pour les initier, elles se produisent simplement. Si vous avez besoin de cette interaction en temps réel, cela peut aussi être fait, mais c'est différent.

Mise à jour:

Si vous souhaitez utiliser un geste continu qui suit le doigt de l'utilisateur, vous pouvez utiliser UIPanGestureRecognizer plutôt que UISwipeGestureRecognizer, et je pense que animateWithDuration est mieux que CATransition dans ce cas. J'ai modifié mes handlePanGesture pour changer les coordonnées frame pour les coordonner avec le geste de l'utilisateur, puis j'ai modifié le code ci-dessus pour terminer l'animation lorsque l'utilisateur lâche prise. Fonctionne plutôt bien. Je ne pense pas que vous puissiez le faire avec CATransition très facilement.

Par exemple, vous pouvez créer un gestionnaire de mouvements sur la vue principale du contrôleur:

[self.view addGestureRecognizer:[[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)]];

Et le gestionnaire pourrait ressembler à:

- (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];
        }
    }
}

Cela utilise ces simples méthodes frame que vous définiriez probablement pour votre UX souhaitée:

- (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);
}

Votre mise en œuvre particulière variera sans aucun doute, mais j'espère que cela illustre l'idée.

Ayant illustré tout cela (complétant et clarifiant cette ancienne réponse), je dois souligner que je n'utilise plus cette technique. De nos jours, j'utilise généralement un UIScrollView (avec "paging" activé) ou (dans iOS 6) un UIPageViewController. Cela vous évite d'écrire ce type de gestionnaire de gestes (et de profiter de fonctionnalités supplémentaires telles que les barres de défilement, les rebonds, etc.). Dans l'implémentation UIScrollView, je répond simplement à l'événement scrollViewDidScroll pour m'assurer que je charge paresseusement la sous-vue nécessaire.

36
Rob

Vous pouvez créer une animation CATransition. Voici un exemple de la façon dont vous pouvez faire glisser une deuxième vue (de gauche) dans l'écran tout en poussant la vue actuelle:

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"];
9
sooper

Si vous voulez le même effet de défilement/glissement de page que le tremplin lors du changement de page, alors pourquoi ne pas simplement utiliser un 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;

Et puis utilisez UIPageControl pour obtenir ces points. :)

8
Leonard Pauli