CABasicAnimation - transform scale keep in center

35,783

Solution 1

I seem to have fixed this with a second animation on the position key. Is this correct or is there a better way of doing this?

CABasicAnimation *animation2 = [CABasicAnimation animationWithKeyPath:@"position"];

animation2.duration = 1.0f;

animation2.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];

animation2.fromValue = [NSValue valueWithCGPoint:CGPointMake((self.view.frame.size.width/2), (self.view.frame.size.height/2))];
animation2.toValue = [NSValue valueWithCGPoint:CGPointMake((self.view.frame.size.width/2), (self.view.frame.size.height/2))];

[toBeMask.layer.mask addAnimation:animation2 forKey:@"animateMask2"];

Solution 2

You can create a scale around the center of the object instead of (0, 0) like this:

CABasicAnimation *scale = [CABasicAnimation animationWithKeyPath:@"transform"];
CATransform3D tr = CATransform3DIdentity;
tr = CATransform3DTranslate(tr, self.bounds.size.width/2, self.bounds.size.height/2, 0);
tr = CATransform3DScale(tr, 3, 3, 1);
tr = CATransform3DTranslate(tr, -self.bounds.size.width/2, -self.bounds.size.height/2, 0);
scale.toValue = [NSValue valueWithCATransform3D:tr];

So we start out with the "identity" transform (which means 'no transform'). Then we translate (move) to the center of the object. Then we scale. Then we move back to origo so we're back where we started (we only wanted to affect the scale operation).

Solution 3

I am not sure why, but CAShapeLayer does not set the bounds property to the layer. In my case, that's was the problem. Try setting the bounds. That should work.

I did something like this for building the initial circle

self.circle = [CAShapeLayer layer];
CGRect roundedRect = CGRectMake(0, 0, width, width);
self.circle.bounds = roundedRect;
UIBezierPath *start = [UIBezierPath bezierPathWithRoundedRect:roundedRect cornerRadius:width/2];
[self.circle setPath:start.CGPath];
self.circle.position = CGPointMake(whatever suits you);
self.circleView.layer.mask = self.circle;
[self.circleView.layer.mask setValue: @(1) forKeyPath: @"transform.scale"];

And something like this for scaling it

CABasicAnimation *scale = [CABasicAnimation animationWithKeyPath:@"transform.scale"];
scale.fromValue = [self.circleView.layer.mask valueForKeyPath:@"transform.scale"];
scale.toValue = @(100);
scale.duration = 1.0;
scale.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[self.circleView.layer.mask setValue:scale.toValue forKeyPath:scale.keyPath];
[self.circleView.layer.mask addAnimation:scale forKey:scale.keyPath];

PD: AnchorPoint is by default (.5,.5) according to apple documentations...but we all know that does not mean anything right?

Hope it helps!!

Solution 4

I have written following function with many added tweaks and options, could be useful for many others.

func layerScaleAnimation(layer: CALayer, duration: CFTimeInterval, fromValue: CGFloat, toValue: CGFloat) {
    let timing = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
    let scaleAnimation: CABasicAnimation = CABasicAnimation(keyPath: "transform.scale")

    CATransaction.begin()
    CATransaction.setAnimationTimingFunction(timing)
    scaleAnimation.duration = duration
    scaleAnimation.fromValue = fromValue
    scaleAnimation.toValue = toValue
    layer.add(scaleAnimation, forKey: "scale")
    CATransaction.commit()
}

Note: Don't forget to update size after animation completion, because CATransaction reverts back to actual size.

Share:
35,783
Leon Storey
Author by

Leon Storey

Software Developer in the UK. Primarily mobile app and server based development with experience in Swift, Objective C, Java, Kotlin, C#, NodeJS, MSSQL, MySQL and C/C++.

Updated on September 10, 2020

Comments

  • Leon Storey
    Leon Storey almost 4 years

    Trying to animatie an ellipse masked on a UIView to be scale transformed remaining in the center position.

    I have found CALayer - CABasicAnimation not scaling around center/anchorPoint, and followed by adding a bounds property to the maskLayer CAShapeLayer however, this ends with the mask being positioned in the left corner with only 1/4 of it showing. I would like the mask to remain within the center of the screen.

    @synthesize maskedView;
    - (void)viewDidLoad
    {
        [super viewDidLoad];
        UIViewController *vc = [self.storyboard instantiateViewControllerWithIdentifier:@"Next"];
    
        vc.view.frame = CGRectMake(0,0, self.view.frame.size.width, self.view.frame.size.height);
    vc.view.layer.bounds = self.view.layer.bounds;
    
        CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
    
        CGRect maskRect = CGRectMake((self.view.frame.size.width/2)-50, (self.view.frame.size.height/2)-50, 100, 100);
        CGMutablePathRef path = CGPathCreateMutable();
        CGPathAddEllipseInRect(path, nil, maskRect);
        [maskLayer setPath:path];
    
        CGPathRelease(path);
    
        vc.view.layer.mask = maskLayer;
        [self.view addSubview:vc.view];
    
        maskedView = vc.view;
        [self startAnimation];
    }
    

    Animation...

    -(void)startAnimation
    {
        maskedView.layer.mask.bounds = CGRectMake((self.view.frame.size.width/2)-50, (self.view.frame.size.height/2)-50, 100, 100);
        maskedView.layer.mask.anchorPoint = CGPointMake(.5,.5);
        maskedView.layer.mask.contentsGravity = @"center";
    
        CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform.scale"];
    
        animation.duration = 1.0f;
        animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
        animation.fromValue = [NSNumber numberWithFloat:1.0f];
        animation.toValue = [NSNumber numberWithFloat:5.0f];
    
        [maskedView.layer.mask addAnimation:animation forKey:@"animateMask"];
    }
    

    Update

    I seem to have fixed this with a second animation on the position key. Is this correct or is there a better way of doing this?

    CABasicAnimation *animation2 = [CABasicAnimation animationWithKeyPath:@"position"];
    
    animation2.duration = 1.0f;
    
    animation2.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    
    animation2.fromValue = [NSValue valueWithCGPoint:CGPointMake((self.view.frame.size.width/2), (self.view.frame.size.height/2))];
    animation2.toValue = [NSValue valueWithCGPoint:CGPointMake((self.view.frame.size.width/2), (self.view.frame.size.height/2))];
    
    [toBeMask.layer.mask addAnimation:animation2 forKey:@"animateMask2"];
    
  • User
    User almost 8 years
    Doesn't the scaling affect the translation you do after it? I'm testing this (as well as with normal affine transform) and the view always ends top left far away from it's initial position.
  • Sk Borhan Uddin
    Sk Borhan Uddin about 7 years
    This one was worked in my case CABasicAnimation *scale = [CABasicAnimation animationWithKeyPath:@"transform"]; CATransform3D tr = CATransform3DIdentity; tr = CATransform3DScale(tr, 3, 3, 1); scale.toValue = [NSValue valueWithCATransform3D:tr];
  • David Seek
    David Seek almost 7 years
    scrubber? ,.,./,.
  • David Seek
    David Seek almost 7 years
    guess scrubber is supposed to be layer? anywas. helped me pretty well. thank you >)
  • Aamir
    Aamir almost 7 years
    @DavidSeek yes its layer not scrubber, I have updated my answer thanks.
  • nevyn
    nevyn over 5 years
    It's better to fix the scale animation to scale about the correct point, instead of doing two animations to hide the incorrectness of the first animation. A lot less code, for one thing.