UIView animation jumps at beginning
Asked Answered
D

9

33

I'm running this code...

[UIView animateWithDuration:0.2
                      delay:0.0
                    options:UIViewAnimationOptionCurveEaseOut
                 animations:^{

                     CGAffineTransform settingsTransform = CGAffineTransformMakeTranslation(self.settingsView.frame.size.width, 0);
                     CGAffineTransform speedTransform = CGAffineTransformMakeTranslation(-self.speedView.frame.size.width, 0);

                     self.settingsView.transform = settingsTransform;
                     self.speedView.transform = speedTransform;

                 } completion:nil];

But when it runs the views jump half the transform in the opposite direction before sliding to half a position in the correct direction.

I've slowed down the animation duration to 5 seconds but the initial jump is instantaneous and half the transformation in the wrong direction.

When I animate back using this code...

[UIView animateWithDuration:0.2
                      delay:0.0
                    options:UIViewAnimationOptionCurveEaseOut
                 animations:^{
                     self.settingsView.transform = CGAffineTransformIdentity;
                     self.speedView.transform = CGAffineTransformIdentity;
                 } completion:nil];

It does exactly the same thing.

The result is that the final movement is half the desired transform as it jumps half the transform in the wrong direction first.

I really can't work out why this is happening?

Any ideas.

::EDIT::

Clearing up some possible ambiguity.

I'm not trying to have these views "bounce" back to where they are. The views I'm animating are like control panel at the edge of the screen. When the user presses "go" the view then slide out of the way. When the user presses "stop" the panels slide back into the view.

At least they used to. Since enabling auto layout (which I need for other parts of the app) I can't just change the frame of the views so I went the the transform route.

You can see this effect by sticking a view into a view controller and a button to run the animation.

Thanks

Diazonium answered 21/9, 2012 at 17:53 Comment(9)
I've just set up a brand new project with a single VC (in a storyboard) and in the view of the VC is a single button and a single view (red background). The button the animates (using the same code as above) the red view from left to right using transforms. But it jumps in exactly the same way.Diazonium
When and where the animation begins?Surreptitious
It's in its own function that gets triggered by pressing a button. I used to do the animation by changing the frames of the views but auto-layout doesn't seem to like me doing that. You can see the effect yourself by creating a project with a button and a label (or something else to animate). In the trigger function for the UIButton use the code above to animate the label. You can see the jump before the animation starts. About 10 lines of code maximum (plus the IB stuff).Diazonium
which sdk are you using? and xcode version?Surreptitious
Do you want a view that animate itself from left to right repeatedly? if yes there is a simpler way to achieve thisDialyser
Hi, no, these animations are completely separate from one another. See edit in OP. ThanksDiazonium
Did you consider using the UIViewAnimationOptionBeginFromCurrentState option for your second animation? I have tried your first code it will animate your views to the right and stop as expected. If you want your second animation to start at any time from where the first animation is use this flag I mentioned.Dialyser
Hi, I tried the begin from current state option but it didn't have any effect. Are you using auto-layout in your view when you tried it? I can only think that that is the cause of this? Thanks againDiazonium
it works in my case(with or without the auto-layout) i have a view that translates to the right and when i click on a button it will set the view back to its original position: it is independent of the status of the first animation (either it has finished or not).Dialyser
T
71

I had the exact same problem so here is the solution I came up with.

    CGAffineTransform transform = CGAffineTransformMake(1, 0, 0, 1, translation.x, translation.y);
    [UIView animateWithDuration:0.25 animations:^{
        _assetImageView.transform = transform;
        [self.view layoutIfNeeded];
    } completion:^(BOOL finished) {
    }];

So I call [self.view layoutIfNeeded]; inside of the animate block. Without this the it has the same problem as you and jumps the distance negative translation first then animates to the correct position from there. I am calling this from a view controller so self is a sub class of UIViewController. In your case "self.view" may not exist but I hope you get the idea.

Timmons answered 31/10, 2012 at 1:51 Comment(8)
Thanks, this worked. Anyone have a good explanation for why this works?Capillary
This should be marked as the correct answer as it is required due to the nature of animating constraints. This worked well for me!Cosby
Wow that is such a random solution that works perfectly! Any idea why it works?Homogenesis
Oddly, I had this problem and this this worked for me even though I'm not using Autolayout. (I don't have a nib, and I'm not adding any constraints to my view.) My views were jumping around at the start of a scaling transform, and with this fix they don't. Does anyone have an explanation for why this would work in a non-autolayout project?Generalize
Holy cow, @davidnesbitt is my hero! I have been looking for a solution to this for at least a year and struggling with crappy work arounds in the mean time. You ROCK!Predecessor
@David Nesbitt - this works, thanks a lot! Could you explain why it does, though? I'm really curious!Bary
layoutIfNeed will update everytime after first load, layoutSubviews will update everytime include the first loadBramble
I still have the same problem. If I have an affine rotation and scale in place before the animation, it skews very badly. If there is a delay, it stays in this skewed position and animates from the skewed position to the final desired transform. I can animate from a scaled to a scaled+transform without problem. It's jsut animating from a scaled+transform to another scaled+transform. Used to work in older iOS versions.Watermelon
T
16

None of the solutions here worked for me... My animation would always skip. (except when it was right at the end of another animation, hint).

Some details:

The view that was doing this had a series of scale and translates already in the stack.

Solution:

Doing a keyframe animation, with a super short first key that would basically re apply the transform that that the last animation should have set.

    UIView.animateKeyframesWithDuration(0.4, delay: 0.0, options: [.CalculationModeCubic], animations: { () -> Void in
        //reset start point
        UIView.addKeyframeWithRelativeStartTime(0.0, relativeDuration: 0.01, animations: { () -> Void in
            self.element.transform = initialTransform
        })

        UIView.addKeyframeWithRelativeStartTime(0.01, relativeDuration: 0.99, animations: { () -> Void in
            self.element.transform = finalTransform
        })
    }, completion: nil)
Trimly answered 19/3, 2016 at 0:59 Comment(7)
The bug is still present in iOS 9.3. This is the only working solution.Diomedes
Andres, I am in awe. You are my hero. Bravo!!! (One minor thing ... I think you have a typo. The last relativeDuration should be 0.99 not 0.39, right?)Disquieting
@CharlieHitchcock, thanks! Have you tested it out? Did it make any difference? Off the top of my head, since duration is relative the total here is 0.4, so it would be about the same as keys of ~0.025 & ~0.975. I'm not sure if I tested different ranges and this worked durations and this worked, or if I had another keyframe in there before.Trimly
@AndresCanella I did test it out using "relativeDuration: 0.99" and it worked as expected that way (brilliantly!). And 0.99 is correct as it is a relative duration. From the addKeyFrame documentation: // start time and duration are values between 0.0 and 1.0 specifying time and duration relative to the overall time of the keyframe animationDisquieting
Right you are! Also tested in playground. Updated answer to 0.99.Trimly
Glad to help! iOS bugs seem to like me.Trimly
Still present in iOS 10.1. Worked like a charm. Thanks. You can also do "self.element.transform = self.element.transform" - same thing, fewer instantialized variables, just as clear.Knap
D
8

OK, having watched the WWDC videos again they state that one of the first things you have to do when using AutoLayout is to remove any calls for setFrame.

Due to the complexity of the screen I have removed the Auto-Layout completely and I'm now using the frame location to move the view around the screen.

Thanks

Diazonium answered 2/10, 2012 at 11:9 Comment(1)
Disable Auto-Layout fixed my problem. Thanks.Farlee
M
8

I've asked Apple Technical Support about this issue and got this response, so it's a bug.

iOS 8 is currently exhibiting a known bug in UIKit animations where transform animations get the wrong fromValue (primarily affecting the position of animated objects, making them appear to “jump” unexpectedly at the start on an animation).

[...]

The workaround until a potential fix is delivered is to drop down to Core Animation APIs to code your animations.

Mildred answered 4/3, 2015 at 8:42 Comment(2)
This question was written 3 years ago. It wasn't a bug then. It was auto layout and me not animating it properly.Diazonium
I appreciate this answer, since it is exactly the bug I am running into.Brandie
S
4

Here's improved version of Andreas answer The key difference is that you can specify just one keyframe and get less code

UIView.animateKeyframes(withDuration: animationDuration, delay: 0.0, options: [], animations: {
  UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 1.0, animations: {
    animatedView.transform = newTransform
    })
  }, completion: nil)
Sharenshargel answered 23/8, 2017 at 7:35 Comment(2)
This works, but I'll be darned if I know why.Libertarian
(To add some context, I encountered this bug in iOS 12)Libertarian
D
1

Does your settingsView or speedView already have a transformation applied before this animation takes place?

If the transformations for these views is not the identity transformation (aka CGAffineTransformIdentity, aka no transformation), you cannot access their .frame properties.

UIViews' frame properties are invalid when they have a transformation applied to them. Use "bounds" instead.

Delouse answered 21/9, 2012 at 18:23 Comment(2)
Thanks for the answer. The transform isn't touched before trying the animation and when the second animation (going back to CGAffineTransformIdentity) it goes back to where it started from. ThanksDiazonium
If you put together a project to try this you can see it happening. All it takes is a button to press and a view to animate. When you animate it jumps the view backwards before moving forwards.Diazonium
D
1

Since you want your second animation to occurs from the current state of your first animation (whether it is finished or not) I recommend to use the UIViewAnimationOptionLayoutSubviews option when setting your second animation.

[UIView animateWithDuration:0.2
                      delay:0.0
                    options:UIViewAnimationOptionCurveEaseOut|UIViewAnimationOptionLayoutSubviews
                 animations:^{
                     self.settingsView.transform = CGAffineTransformIdentity;
                     self.speedView.transform = CGAffineTransformIdentity;
                 } completion:nil];

Here is a sample of the code i used for testing by using the simple view controller template:

myviewcontroller.m:

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.animatedView = [[UIView alloc] initWithFrame:CGRectMake(20, 20, 160, 80)];
    self.animatedView.backgroundColor = [UIColor yellowColor];

    [UIView animateWithDuration:0.2
                          delay:0.0
                        options:UIViewAnimationOptionCurveEaseOut|UIViewAnimationOptionLayoutSubviews
                     animations:^{
                         CGAffineTransform settingsTransform = CGAffineTransformMakeTranslation(self.animatedView.frame.size.width, 0);

                         self.animatedView.transform = settingsTransform;

                     }
                     completion:nil];
    self.buttonToTest = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    self.buttonToTest.frame = CGRectMake(90, 20, 80, 40);
    [self.buttonToTest setTitle:@"Click Me!" forState:UIControlStateNormal];
    [self.buttonToTest addTarget:self action:@selector(buttonClicked) forControlEvents:UIControlEventTouchUpInside];

    // set-up view hierarchy
    [self.view addSubview:self.buttonToTest];
    [self.view addSubview: self.animatedView];

}

- (void) buttonClicked
{
    [UIView animateWithDuration:.2
                          delay:0.0
                        options:UIViewAnimationOptionCurveEaseOut|UIViewAnimationOptionBeginFromCurrentState|UIViewAnimationOptionLayoutSubviews
                     animations:^{
                         self.animatedView.transform = CGAffineTransformIdentity;

                     }
                     completion:nil];
}
Dialyser answered 21/9, 2012 at 18:47 Comment(2)
Hi, these two animations are completely separate from each other. I'm not trying to chain them together. See edit of OP. ThanksDiazonium
The first time the user press the button it moves right. When the user presses it again the view goes back to where it was. Each animation only has a single movement to slide one way. If you set up a project with the views shown in the OP you can see this happening.Diazonium
S
0

I had this problem and solved it by:

  1. Hide the original view
  2. Add a temp view that copies the view you want to animate
  3. Carry out animation which will now be as intended
  4. Remove the temp view and unhide the original view with the final state

Hope this helps.

Siderite answered 7/3, 2016 at 22:3 Comment(0)
P
0

I tried the answers above, about the layoutIfNeeded, but it didn't work. I added the layoutIfNeeded outside (before) the animation block and it solved my problem.

view.layoutIfNeeded()
    
UIView.animate(withDuration: duration, delay: 0, options: .beginFromCurrentState, animations: {
    // Animation here
})
Pinochle answered 17/3, 2021 at 8:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.