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
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.
🎩✨
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 = .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
}
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.
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
Related
Im currently developing an iOS Application where you can process an image. (rotating, zooming, translating). Im using an uiimageview where i added gestures. This works fine but i also have some masking rectangle of a fixed size. Initial State
After i processed my image i want the content which is inside my masking rectangle.
I also want the four edge points of the masking rectangle of the processed image.
I know i have to apply the imageview transform to the points somehow, but its not working.
let points = maskView.edgePoints()
let translateTransform = CGAffineTransform(translationX: translationPoint.x, y: translationPoint.y)
let rotateTransform = CGAffineTransform(rotationAngle: CGFloat(rotationAngle))
let scaleTransform = CGAffineTransform(scaleX: xScale, y: yScale)
let finalTransform = rotateTransform.concatenating(scaleTransform).concatenating(translateTransform)
let topleftPoint = points[0].applying(finalTransform)
let toprightPoint = points[1].applying(finalTransform)
let bottomleftPoint = points[2].applying(finalTransform)
let bottomrightPoint = points[3].applying(finalTransform)
Edge point results: Sample
Topleft: (50.75, -8.75)
Topright: (63.6072332330383, -365.252863911729)
Bottomleft: (-172.064289944831, -16.7857707706489)
Bottomright: (-159.207056711792, -373.288634682378)
But the Topleft should be something like (0,0)
and the Bottomleft something like (40,200)?
Maybe you can give me some hints or useful links!
Thx in advance!
The problem lies in your transformation order. Right now your transformation order is Rotate Scale Translate, it should be Scale Rotate Translate instead.
let finalTransform = scaleTransform.concatenating(rotateTransform).concatenating(translateTransform)
In my application, I am calculating BMI of a user based on its height and weight. So based on the BMI value I am animating a needle. The issue is that the needle moves off the center and rotates.
CODE:
ivPointer.layer.anchorPoint = CGPoint(x: 0, y: 0.5) // i have changed the values still its not working properly
let angle = Double((value * Double.pi)/180)
var transform = CGAffineTransform.identity.rotated(by: CGFloat(angle))
When I attempt to translate a path, to move it to an origin of {0, 0}, the resulting path bounds is in error. (Or, my assumptions are in error).
e.g. the path gives the following bounds info:
let bezier = UIBezierPath(cgPath: svgPath)
print(bezier.bounds)
// (0.0085, 0.7200, 68.5542, 41.1379)
print(bezier.cgPath.boundingBoxOfPath)
// (0.0085, 0.7200, 68.5542, 41.1379)
print(bezier.cgPath.boundingBox)
// (-1.25, -0.1070, 70.0360, 41.9650)
I (attempt to) move the path to the origin:
let origin = bezier.bounds.origin
bezier.apply(CGAffineTransform(translationX: -origin.x, y: -origin.y))
print(bezier.bounds)
// (0.0, -2.7755, 68.5542, 41.1379)
As you can see, the x origin component is correct at 0. But, the y component (-2.7755) has gone all kittywumpus. It should be 0, non?
The same thing happens when I perform the transform on the cgPath property.
Does anyone know what kind of circumstances could cause a UIBezierPath/CGPath to behave like this when translated? After reading the Apple docs, it seems that UIBezierPath/CGPath do not hold a transform state; the points are transformed immediately when the transform is called.
Thanks for any help.
Background:
The path data is from Font-Awesome SVGs, via PocketSVG. All files parse, and most draw OK. But a small subset exhibit the above translation issue. I'd like to know if I'm doing something fundamentally wrong or silly before I go ferreting through the SVG parsing, path-building code looking for defects.
BTW I am not drawing at this stage or otherwise dealing with a context; I am building paths prior to drawing.
[edit]
To check that PocketSVG was giving me properly formed data, I passed the same SVG to SwiftSVG, and got the same path data as PocketSVG, and the same result:
let svgURL = Bundle.main.url(forResource: "fa-mars-stroke-h", withExtension: "svg")!
var bezier = UIBezierPath.pathWithSVGURL(svgURL)!
print(bezier.bounds)
// (0.0085, 0.7200, 68.5542, 41.1379)
let origin = bezier.bounds.origin
let translation = CGAffineTransform(translationX: -origin.x, y: -origin.y)
bezier.apply(translation)
print(bezier.bounds)
// (0.0, -2.7755, 68.5542, 41.1379)
Once again, that y component should be 0, but is not. Very weird.
On a whim, I thought I'd try to apply a transformation again. And, it worked!
let translation2 = CGAffineTransform(translationX: -bezier.bounds.origin.x, y: -bezier.bounds.origin.y)
bezier.apply(translation2)
print(bezier.bounds)
// (0.0, 0.0, 68.5542491336633, 41.1379438254997)
Baffling! Am I overlooking something really basic here?
I have tried the same as you and is working for me in Xcode 8.3.2 / iOS 10
I struggled myself with the same problem, I managed to solve it by using the following snippet of code (Swift 5). I tested on an organic bezier shape and it works as expected:
extension CGRect {
var center: CGPoint { return CGPoint(x: midX, y: midY) }
}
extension UIBezierPath {
func center(inRect rect:CGRect) {
let rectCenter = rect.center
let bezierCenter = self.bounds.center
let translation = CGAffineTransform(translationX: rectCenter.x - bezierCenter.x, y: rectCenter.y - bezierCenter.y)
self.apply(translation)
}
}
Usage example:
override func viewDidLoad() {
super.viewDidLoad()
let bezier = UIBezierPath() // replace this with your bezier object
let shape = CAShapeLayer()
shape.strokeColor = UIColor.black.cgColor
shape.fillColor = UIColor.clear.cgColor
shape.bounds = self.view.bounds
shape.position = self.view.bounds.center
bezier.center(inRect: shape.bounds)
shape.path = bezier.cgPath
self.view.layer.addSublayer(shape)
}
It will display the shape in the center of the screen.
I have a collection view with a collection of images (which are views). The layout causes transformations to occur on every visible item. The item at the center is both zoomed and translated while the others are just translated.
The point is, the layout is setting the layer's transform property for each item.
Later, when the user touches an item, I want to animate the item using a keyframe animation.
The behavior I am experiencing is that the item seems to revert back to its untransformed state, the animation occurs on its untransformed state and then the item's layout transformation returns.
Why?
I've tried using the view's transform property, the backing layer's affine transform property, and the backing layer's 3D transform property all with this same behavior. The keyframe animation uses 3D transforms.
I clearly have a gap in my understanding of transforms and animations, though I have seen this question posed for platforms other than iOS without answers.
Layout transformation code (in a flow layout subclass):
// Calculate the distance from the center of the visible rect to the center of the attributes.
// Then normalize it so we can compare them all. This way, all items further away than the
// active get the same transform.
let distanceFromVisibleRectToItem = CGRectGetMidX(visibleRect) - attributes.center.x
let normalizedDistance = distanceFromVisibleRectToItem / ACTIVE_DISTANCE
let isLeft = distanceFromVisibleRectToItem > 0
var transform = CATransform3DIdentity
var maskAlpha: CGFloat = 0.0
if (abs(distanceFromVisibleRectToItem) < ACTIVE_DISTANCE) {
// We're close enough to apply the transform in relation to how far away from the center we are.
transform = CATransform3DTranslate(transform,
(isLeft ? -FLOW_OFFSET : FLOW_OFFSET) * abs(distanceFromVisibleRectToItem/TRANSLATE_DISTANCE),
0, (1 - abs(normalizedDistance)) * 40000 + (isLeft ? 200 : 0))
// Set the zoom factor.
let zoom = 1 + ZOOM_FACTOR * (1 - abs(normalizedDistance))
transform = CATransform3DScale(transform, zoom, zoom, 1)
attributes.zIndex = 1
let ratioToCenter = (ACTIVE_DISTANCE - abs(distanceFromVisibleRectToItem)) / ACTIVE_DISTANCE
// Interpolate between 0.0f and INACTIVE_GREY_VALUE
maskAlpha = INACTIVE_GREY_VALUE + ratioToCenter * (-INACTIVE_GREY_VALUE)
} else {
// We're too far away - just apply a standard perspective transform.
transform = CATransform3DTranslate(transform, isLeft ? -FLOW_OFFSET : FLOW_OFFSET, 0, 0)
attributes.zIndex = 0
maskAlpha = INACTIVE_GREY_VALUE
}
attributes.transform3D = transform
Animation code (note this is a Swift extension to the UIView class):
func bounceView(completion: (() -> Void)? = nil) {
var animation = bounceAnimation(frame.height)
animation.delegate = AnimationDelegate(completion: completion)
layer.addAnimation(animation, forKey: "bounce")
}
func bounceAnimation(itemHeight: CGFloat) -> CAKeyframeAnimation {
let factors: [CGFloat] = [0, 32, 60, 83, 100, 114, 124, 128, 128, 124, 114, 100, 83, 60, 32,
0, 24, 42, 54, 62, 64, 62, 54, 42, 24, 0, 18, 28, 32, 28, 18, 0]
var transforms: [AnyObject] = [NSValue(CATransform3D: self.layer.transform)]
for factor in factors {
let positionOffset = factor/256.0 * itemHeight
let transform = CATransform3DMakeTranslation(0, -positionOffset, 0)
transforms.append(NSValue(CATransform3D: transform))
}
let animation = CAKeyframeAnimation(keyPath: "transform")
animation.repeatCount = 1
animation.duration = CFTimeInterval(factors.count)/30.0
animation.fillMode = kCAFillModeForwards
animation.values = transforms
animation.removedOnCompletion = true // final stage is equal to starting stage
animation.autoreverses = false
return animation;
}
Note: I should state more simply that I want this process to begin and end in the collection view layout transformed state. I want the animation to occur on the layout transformed state as well (i.e. no reverting to the untransformed state at all).
It seems that in your animations you are not syncing your model with your presentation layer.
When you animate with Core Animation (CABasicAnimation, for instance) it only changes the presentation layer (the thing you see on the screen) but in fact the state your layers have is different from the visible one.
(You can read more about that in this objc.io issue)
So basically to fix this you need to update your model in your CAAnimation. You need to set a from and toValue, and then, after you set those, you update your transform so it can match the final state of your presentation layer.
EDIT
Now that we have some code I can be concrete
To sync the model layer to the presentation layer you need to set your model to the final state before adding the animation:
var animation = bounceAnimation(frame.height)
animation.delegate = AnimationDelegate(completion: completion)
layer.transform = CATransform3DMakeTranslation(0, 0, 0)
layer.addAnimation(animation, forKey: "bounce")
David Rönnqvist has a good rant about this issue in this gist.
SOLUTION:
The help Tiago Almeida offered was very useful to get me thinking about what the animation was actually doing (how it was actually working). My basic assumption that the animation would begin with the current model layer transform was incorrect. As Tiago points out, the presentation layer is a separate and distinct layer from the model layer.
Once I got that through my thick head, I realized that I had to manually concatenate the current model transform with the transform being built for the animation frame and viola! Things work as expected.
The updated animation code:
var transforms: [AnyObject] = []
for factor in factors {
let positionOffset = factor/256.0 * itemHeight
var transform = CATransform3DMakeTranslation(0, -positionOffset, 0)
transform = CATransform3DConcat(self.layer.transform, transform)
transforms.append(NSValue(CATransform3D: transform))
}
Basically I want to build the scrollView like the one in iOS7 safari tab switcher. I use CATransform3D to get the feeling of oblique. However when I employ transform the layers just don't display in proper order.(They are in correct order before transform, seems like a total reversal in order.Back layer jumps to the front). How can I fix this thorny problem?
By the way in my case superView.bringSubViewToFront doesn't work.
My code:
//Create imageView
...
scroller.addSubview(imageView)
let layer = imageView.layer
//Set shadow
layer.shadowOpacity = 0.2
layer.shadowRadius = 2
layer.shadowOffset = CGSizeMake(6, -10)
//Transform, if without following code the layer order is correct
var transform = CATransform3DIdentity
transform.m34 = 0.0009
let radiants = 0.11*M_PI
transform = CATransform3DRotate(transform, CGFloat(radiants), 1.0, 0.0, 0.0)
scroller.bringSubviewToFront(imageView)//It doesn't work.
I just found that if you set the z axis to 1.0 in CATransform3DTranslate the layer become in front of other layers, so just make it 0.