CGAffineTransform scale and translation - jump before animation CGAffineTransform scale and translation - jump before animation ios ios

CGAffineTransform scale and translation - jump before animation

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 = CGAffineTransformIdentitytransform = CGAffineTransformScale(transform, 1.1, 1.1)transform = CGAffineTransformTranslate(transform, 10, 10)view.layer.setAffineTransform(transform)

New code:

var transform = CATransform3DIdentitytransform = 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.


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.

I dont know why, but this code can work


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 = .blueself.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    }