“Push” animation in Objective-C

2019-03-20 03:39发布

问题:

I have two views: A and B. A is positioned at the top of the screen, B is positioned at the bottom of the screen.

When the user presses a button, view B animates upwards with a EaseInEaseOut bezier curve until it reaches y = 0. While B is on its way to its destination, it should push A up when it hits A. In other words, when B has passed a certain y coordinate (A's y origin + height) during its transition from bottom to top, A should stick to B so it seems B pushes A upwards.

What I have tried so far:

  • Register a target + selector to a CADisplayLink immediately after the user pressed the button. Inside this selector, request view B's y coordinate by accessing its presentationLayer and adjust A's y coordinate accordingly. However, this method turns out to be not accurate enough: the presentationLayer's frame is behind on B's current position on the screen (this is probably because -presentationLayer recalculates the position of the animating view on the current time, which takes longer than 1 frame). When I increase B's animation duration, this method works fine.
  • Register a target + selector to a CADisplayLink immediately after the user pressed the button. Inside this selector, calculate B's current y coordinate by solving the bezier equation for x = elapsed time / animation duration (which should return the quotient distance traveled / total distance). I used Apple's open source UnitBezier.h for this (http://opensource.apple.com/source/WebCore/WebCore-955.66/platform/graphics/UnitBezier.h). However, the results are not correct.

Any suggestions on what I can try next?

回答1:

You said that you tried the "animate B and use display link to update A" technique, and that it resulted in "A" lagging behind "B". You could theoretically animate a new view, "C", and then adjust B and A's frames accordingly in the display link, which should eliminate any lag (relative to each other).



回答2:

Two simple solutions:

  1. Use animationWithDuration only: You can break your animation into two nested animations, using "ease in" to animate the moving of "B" up to "A", and then using "ease out" to animate the moving of "B" and "A" the rest of the way. The only trick here is to make sure the two duration values make sense so that the speed of the animation doesn't appear to change.

    CGFloat animationDuration = 0.5;
    CGFloat firstPortionDistance = self.b.frame.origin.y - (self.a.frame.origin.y + self.a.frame.size.height);
    CGFloat secondPortionDistance = self.a.frame.size.height;
    CGFloat firstPortionDuration = animationDuration * firstPortionDistance / (firstPortionDistance + secondPortionDistance);
    CGFloat secondPortionDuration = animationDuration * secondPortionDistance / (firstPortionDistance + secondPortionDistance);
    
    [UIView animateWithDuration:firstPortionDuration
                          delay:0.0
                        options:UIViewAnimationOptionCurveEaseIn
                     animations:^{
                         CGRect frame = self.b.frame;
                         frame.origin.y -= firstPortionDistance;
                         self.b.frame = frame;
                     }
                     completion:^(BOOL finished) {
                         [UIView animateWithDuration:secondPortionDuration
                                               delay:0.0
                                             options:UIViewAnimationOptionCurveEaseOut
                                          animations:^{
                                              CGRect frame = self.b.frame;
                                              frame.origin.y -= secondPortionDistance;
                                              self.b.frame = frame;
    
                                              frame = self.a.frame;
                                              frame.origin.y -= secondPortionDistance;
                                              self.a.frame = frame;
                                          }
                                          completion:nil];
                     }];
    
  2. You can let animateWithDuration handle the full animation of "B", but then use CADisplayLink and use presentationLayer to retrieve B's current frame and to adjust A's frame accordingly:

    [self startDisplayLink];
    [UIView animateWithDuration:0.5
                          delay:0.0
                        options:UIViewAnimationOptionCurveEaseInOut
                     animations:^{
                         CGRect frame = self.b.frame;
                         frame.origin.y = self.a.frame.origin.y;
                         self.b.frame = frame;
                     }
                     completion:^(BOOL finished) {
                         [self stopDisplayLink];
                     }];
    

    where the methods to start, stop, and handle the display link are defined as follows:

    - (void)startDisplayLink
    {
        self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDisplayLink:)];
        [self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    }
    
    - (void)stopDisplayLink
    {
        [self.displayLink invalidate];
        self.displayLink = nil;
    }
    
    - (void)handleDisplayLink:(CADisplayLink *)displayLink
    {
        CALayer *presentationLayer = self.b.layer.presentationLayer;
    
        if (presentationLayer.frame.origin.y < (self.a.frame.origin.y + self.a.frame.size.height))
        {
            CGRect frame = self.a.frame;
            frame.origin.y = presentationLayer.frame.origin.y - self.a.frame.size.height;
            self.a.frame = frame;
        }
    }
    


回答3:

You can try followin algorythm:

1)Put A and B in UIView(i.e UIview *gropedView)

2)Change B.y till it will be equal A.y+A.height(so B will be right under the A)

3)Animate groupedView