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