32

I am struggling with an issue regarding CGAffineTransform scale and translation where when I set a transform in an animation block on a view that already has a transform the view jumps a bit before animating.

Example:

// somewhere in view did load or during initialization
var view = UIView()
view.frame = CGRectMake(0,0,100,100)
var scale = CGAffineTransformMakeScale(0.8,0.8)
var translation = CGAffineTransformMakeTranslation(100,100)
var concat = CGAffineTransformConcat(translation, scale)
view.transform = transform

// called sometime later
func buttonPressed() {
    var secondScale = CGAffineTransformMakeScale(0.6,0.6)
    var secondTranslation = CGAffineTransformMakeTranslation(150,300)
    var secondConcat = CGAffineTransformConcat(secondTranslation, secondScale)
    UIView.animateWithDuration(0.5, animations: { () -> Void in 
         view.transform = secondConcat
    })

}

Now when buttonPressed() is called the view jumps to the top left about 10 pixels before starting to animate. I only witnessed this issue with a concat transform, using only a translation transform works fine.

Edit: Since I've done a lot of research regarding the matter I think I should mention that this issue appears regardless of whether or not auto layout is turned on

matteok
  • 2,189
  • 3
  • 30
  • 54
  • What if you use `CGAffineTransformTranslate(secondScale,150,300)`? – Ian MacDonald Jan 13 '15 at 21:09
  • The result is exactly the same – matteok Jan 13 '15 at 21:10
  • What does "jumps a bit" mean? Does it return to the identity transform? Does it animate to `secondConcat` from the jump or does it jump, return to concat, _then_ animate to `secondConcat`? – Logan Moseley Jan 13 '15 at 21:16
  • The view starts at 100,100 (where the first transform puts it) then when buttonPressed() is called the view jumps from (100, 100) to approximately (85,85) and then animates to the second location/scale – matteok Jan 13 '15 at 21:23
  • 2
    Did you ever solve this? Currently debugging the same problem here. – Sami Samhuri Feb 13 '15 at 15:13
  • 1
    unfortunately I didn't. I ended up wrapping one view inside another so i could translate the outside view and scale the inside view. This way i was able to work around the issue. – matteok Feb 14 '15 at 17:36

5 Answers5

54

I ran into the same issue, but couldn't find the exact source of the problem. The jump seems to appear only in very specific conditions: If the view animates from a transform t1 to a transform t2 and both transforms are a combination of a scale and a translation (that's exactly your case). Given the following workaround, which doesn't make sense to me, I assume it's a bug in Core Animation.

First, I tried using CATransform3D instead of CGAffineTransform.

Old code:

var transform = CGAffineTransformIdentity
transform = CGAffineTransformScale(transform, 1.1, 1.1)
transform = CGAffineTransformTranslate(transform, 10, 10)
view.layer.setAffineTransform(transform)

New code:

var transform = CATransform3DIdentity
transform = CATransform3DScale(transform, 1.1, 1.1, 1.0)
transform = CATransform3DTranslate(transform, 10, 10, 0)
view.layer.transform = transform

The new code should be equivalent to the old one (the fourth parameter is set to 1.0 or 0 so that there is no scaling/translation in z direction), and in fact it shows the same jumping. However, here comes the black magic: In the scale transformation, change the z parameter to anything different from 1.0, like this:

transform = CATransform3DScale(transform, 1.1, 1.1, 1.01)

This parameter should have no effect, but now the jump is gone.

Theo
  • 3,826
  • 30
  • 59
  • 4
    I think this answer did not get enough credits... I've been coding for days and couldn't fix this. – farzadshbfn Nov 16 '17 at 14:42
  • 2
    ☝️Ditto. I encountered the same issue with a simple scale and translation. The layer would "move" to the left just a little bit before animating. Applying a `1.01` scale to the `z` axis fixed the problem. – Chris Apr 05 '18 at 00:03
  • 1
    For anyone curious from my testing this issue has been fixed in iOS 13 – Tomer Shemesh Sep 06 '19 at 18:59
  • My simulator running iOS 13 is still experiencing this issue. – Krøllebølle Jun 28 '20 at 19:59
4

Looks like Apple UIView animation internal bug. When Apple interpolates CGAffineTransform changes between two values to create animation it should do following steps:

  • Extract translation, scale, and rotation
  • Interpolate extracted values form start to end
  • Assemble CGAffineTransform for each interpolation step

Assembling should be in following order:

  • Translation
  • Scaling
  • Rotation

But looks like Apple make translation after scaling and rotation. This bug should be fixed by Apple.

k06a
  • 17,755
  • 10
  • 70
  • 110
1

I dont know why, but this code can work

update:

I successfully combine scale, translate, and rotation together, from any transform state to any new transform state.

I think the transform is reinterpreted at the start of the animation.

the anchor of start transform is considered in new transform, and then we convert it to old transform.

self.v  = UIView(frame: CGRect(x: 50, y: 50, width: 50, height: 50))
self.v?.backgroundColor = .blue
self.view.addSubview(v!)

func buttonPressed() {
    let view = self.v!

    let m1 = view.transform
    let tempScale = CGFloat(arc4random()%10)/10 + 1.0
    let tempRotae:CGFloat = 1
    let m2 = m1.translatedBy(x: CGFloat(arc4random()%30), y: CGFloat(arc4random()%30)).scaledBy(x: tempScale, y: tempScale).rotated(by:tempRotae)
    self.animationViewToNewTransform(view: view, newTranform: m2)
}    


func animationViewToNewTransform(view: UIView, newTranform: CGAffineTransform) {
    // 1. pointInView.apply(view.transform) is not correct point.
    // the real matrix is mAnchorToOrigin.inverted().concatenating(m1).concatenating(mAnchorToOrigin)
    // 2. animation begin trasform is relative to final transform in final transform coordinate

    // anchor and mAnchor
    let normalizedAnchor0 = view.layer.anchorPoint
    let anchor0 = CGPoint(x: normalizedAnchor0.x * view.bounds.width, y: normalizedAnchor0.y * view.bounds.height)
    let mAnchor0 = CGAffineTransform.identity.translatedBy(x: anchor0.x, y: anchor0.y)

    // 0->1->2
    //let origin = CGPoint(x: 0, y: 0)
    //let m0 = CGAffineTransform.identity
    let m1 = view.transform
    let m2 = newTranform

    // rotate and scale relative to anchor, not to origin
    let matrix1 = mAnchor0.inverted().concatenating(m1).concatenating(mAnchor0)
    let matrix2 = mAnchor0.inverted().concatenating(m2).concatenating(mAnchor0)
    let anchor1 = anchor0.applying(matrix1)
    let mAnchor1 = CGAffineTransform.identity.translatedBy(x: anchor1.x, y: anchor1.y)
    let anchor2 = anchor0.applying(matrix2)
    let txty2 = CGPoint(x: anchor2.x - anchor0.x, y: anchor2.y - anchor0.y)
    let txty2plusAnchor2 = CGPoint(x: txty2.x + anchor2.x, y: txty2.y + anchor2.y)
    let anchor1InM2System = anchor1.applying(matrix2.inverted()).applying(mAnchor0.inverted())
    let txty2ToM0System = txty2plusAnchor2.applying(matrix2.inverted()).applying(mAnchor0.inverted())
    let txty2ToM1System = txty2ToM0System.applying(mAnchor0).applying(matrix1).applying(mAnchor1.inverted())

    var m1New = m1
    m1New.tx = txty2ToM1System.x + anchor1InM2System.x
    m1New.ty = txty2ToM1System.y + anchor1InM2System.y

    view.transform = m1New
    UIView.animate(withDuration: 1.4) {
        view.transform = m2
    }
}

I also try the zScale solution, it seems also work if set zScale non-1 at the first transform or at every transform

    let oldTransform = view.layer.transform
    let tempScale = CGFloat(arc4random()%10)/10 + 1.0
    var newTransform = CATransform3DScale(oldTransform, tempScale, tempScale, 1.01)
    newTransform = CATransform3DTranslate(newTransform, CGFloat(arc4random()%30), CGFloat(arc4random()%30), 0)
    newTransform = CATransform3DRotate(newTransform, 1, 0, 0, 1)

    UIView.animate(withDuration: 1.4) {
        view.layer.transform = newTransform
    }
andrewchan2022
  • 4,953
  • 45
  • 48
0

Instead of CGAffineTransformMakeScale() and CGAffineTransformMakeTranslation(), which create a transform based off of CGAffineTransformIdentity (basically no transform), you want to scale and translate based on the view's current transform using CGAffineTransformScale() and CGAffineTransformTranslate(), which start with the existing transform.

Dave Batton
  • 8,795
  • 1
  • 46
  • 50
  • I actually need to set an absolute transform during animation which means I don't want to add to the existing transform but replace it animated – matteok Jan 13 '15 at 22:50
  • 1
    Also I just tried using CGAffineTransformTranslate instead of CGAffineTransformMakeTranslate and the issue remains the same. – matteok Jan 13 '15 at 23:43
0

The source of the issue is the lack of perspective information to the transform.

You can add perspective information modifying the m34 property of your 3d transform

var transform = CATransform3DIdentity
transform.m34 = 1.0 / 200 //your own perspective value here
transform = CATransform3DScale(transform, 1.1, 1.1, 1.0)
transform = CATransform3DTranslate(transform, 10, 10, 0)
view.layer.transform = transform
Snit
  • 44
  • 6