Funky behavior when animating rotation of UIImageView - ios

I'm trying to rotate an UIImageView with the following code:
var x = -((self.needleImage.frame.size.width/2) - 15) //x for rotation point
UIView.animateWithDuration(5.0, delay: 10.0, options: nil, animations: {
var transform = CGAffineTransformMakeTranslation(x, 0)
transform = CGAffineTransformRotate(transform, CGFloat(-M_PI_2))
transform = CGAffineTransformTranslate(transform, -x, 0)
self.needleImage.transform = transformm
The end position is right, but during the animation/rotation, the image is shifted a little to the left before settling at the right place.
I tried the same code with a UIView from this and it doesn't do that.
Then I tried wrapping the Imageview inside a View and rotating that, but that didn't help either.
I have drawn a cicle ontop of the rotation point to check if it isn't in the right place, but it seems alright.

I've used this code snippet in the past to rotate a view perpetually, but by removing the last closure it should work to rotate an image 360 degrees. This can be adjusted, or course. Right now it's set to 2PI. You'll have to convert your rotation angle to radians.
func animateRotator() {
UIView.animateWithDuration(0.5, delay: 0, options: .CurveLinear, animations: { () -> Void in
self.rotator.transform = CGAffineTransformRotate(self.rotator.transform, CGFloat(M_PI_2))
}) { (finished) -> Void in
self.animateRotator()
}
}

Related

iOS: jumping uiview when rotating via CATransform3DRotate

I try to rotate a uiview (height & width of 100) with an angle of 90 degrees with an own anchor point. After changing the anchor point I translate my view so that both changes cancel each other out.
When I rotate the view with (Double.pi) it animates as expected but when I change to (Double.pi/2) the view is jumping up.
testView.layer.anchorPoint = CGPoint(x: 0.5, y: 1)
testView.layer.transform = CATransform3DMakeTranslation(0, -50, 0)
UIView.animate(withDuration: 2.3, delay: 0, options: .curveLinear, animations: { () -> Void in
var allTransofrom = self.testView.layer.transform
var c3d = CATransform3DIdentity
c3d.m34 = 2.5 / (2000)
let rot = CATransform3DRotate(c3d, CGFloat(Double.pi/2), 1, 0, 0)
allTransofrom = CATransform3DConcat(allTransofrom, rot)
self.testView.layer.transform = allTransofrom
}, completion: {(finished) in
} )
full code here
I was able to found a solution by myself but
I still don't know where the jump is coming from.
Instead of doing a translation via CGAffineTransform.translatedBy or CATransform3DMakeTranslation I simply adapted my NSLayoutConstraint.
testViewAnchors[0].constant = testViewAnchors[0].constant + CGFloat(50)
In my case an animation for the "translation" wasn't necessary but as it is possible to animate NSLayoutConstraints it shouldn't be a problem.

UIView animation with autolayout and child views

I have a weird problem that seems to be fairly easy to solve. I think I could use some workaround to be able to have the behavior that I want but I want to know if there is a better way to do it.
I have a view called contentView (blue in the image) that it will expand its height using a UIView.animation, this is no problem at all and works as expected.
The problem here is that this view has a child component (a button) that has an autolayout constraint to its bottom equal to 22 like this:
This is the autolayout constraint:
If I do the height resize without the animation it works fine, the view height change and the button are always 22 points of the bottom of the contentView. But when I use an animation to make this change more smoothy and user-friendly the button moves to the end position before the animation even start.
I want to know how I could achieve a smooth animation here but with the button moving along its parent view
The part of the code that handles the animation is pretty straightforward but I'll post it in here:
#IBAction func openDetail(_ sender: ExpandCourseDetail) {
let rotation = sender.getOpen() ? CGAffineTransform.identity : CGAffineTransform(rotationAngle: CGFloat.pi)
UIView.animate(withDuration: 0.5, delay: 0.1, options: [.curveEaseInOut], animations: {
sender.transform = rotation
}, completion: {
success in
sender.setOpen(!sender.getOpen())
})
UIView.animate(withDuration: 1.0, delay: 0.5, options: [.curveEaseInOut], animations: {
self.contentView.frame.size.height = sender.getOpen() ? self.contentView.frame.height - 300 : self.contentView.frame.height + 300
}, completion: nil)
}
As a side note, the button itself has an animation that rotates the button 180 degrees to show the user that the view is expanding.
Thank you so much for your help.
It's super easy with constraints, just create a superView height constraint IBOutlet and change its constant value.
#IBAction func btnPressed(_ sender: UIButton) {
self.toggleButton.isSelected = !sender.isSelected
//Animation starts here
self.view.layoutIfNeeded()
UIView.animate(withDuration: 0.7) {
if self.toggleButton.isSelected {
//transform button
self.toggleButton.transform = CGAffineTransform(rotationAngle: CGFloat(Double.pi))
//change your ParentView height to desired one
self.constContentViewHeight.constant = self.view.frame.size.height - 220
self.view.layoutIfNeeded()
} else {
self.toggleButton.transform = CGAffineTransform(rotationAngle: CGFloat(Double.pi*2))
// Set height constraint to original value
self.constContentViewHeight.constant = 250
self.view.layoutIfNeeded()
}
}
}
I have created a demo, check it out.
The issue you are facing is due to two animation blocks. So I have changed some lines and put both button transformation and height animation into one animation block.
func openDetail(_ sender: ExpandCourseDetail) {
let isOpen = sender.getOpen() ? CGAffineTransform.identity : CGAffineTransform(rotationAngle: CGFloat.pi)
UIView.animate(withDuration: 1.0, delay: 0.5, options: [.curveEaseInOut], animations: {
sender.transform = rotation
self.contentView.frame.size.height = self.contentView.frame.height + (isOpen ? 300 : -300)
}, completion: { (success) in
sender.setOpen(!isOpen)
})
}

Continuous rotation is not centered

Using this code:
UIView.animate(withDuration: 0.5, delay: 0, options: [.repeat], animations: {
self.star.transform = self.star.transform.rotated(by: CGFloat(M_PI_2))
})
My view is doing this:
Using this code:
extension UIView {
func rotate360Degrees(duration: CFTimeInterval = 3) {
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotateAnimation.fromValue = 0.0
rotateAnimation.toValue = CGFloat(M_PI * 2)
rotateAnimation.isRemovedOnCompletion = false
rotateAnimation.duration = duration
rotateAnimation.repeatCount=Float.infinity
self.layer.add(rotateAnimation, forKey: nil)
}
}
My view is doing this:
Well both are not doing what I want. The view that is rotating is an UIImageView with scale to fill. I want the image to stay exactly in the middle. How can I accomplish that? The functions are executed in viewDidAppear. The last gif looks way better, but notice the star is not perfectly centered... This is the image.
Problem
The center of your image is not the center of your star.
Solutions
There are two possible solutions
Edit the image so that center of the star is at the center of the image.
Set the anchor of the rotating layer to the center of the star (x: 1004px, y: 761px).

How to stop animation block from making uibutton background image flicker

I have two buttons in a view controller that I animate up and then back to their original positions when pressed. 50% of the time there is a discernible gap that appears between the top of the button and it's background image. I think it has to do with using spring/damping but I don't want to not use that. I tried rounding to the nearest whole CGFloat both the button frame and destination coordinates, no luck. Is there a work around to this?
let profileMainScreenButtonAnimateToY = -ceil((profileMainButton.frame.origin.y - 45.0))
let profileStandingsButtonAnimateToY = -ceil((profileStandingsButton.frame.origin.y - 45.0))
UIView.animateWithDuration(0.7, delay: 0.0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.8, options: [.CurveEaseInOut], animations: { () -> Void in
self.profileMainButton.transform = CGAffineTransformMakeTranslation(0.0, profileMainScreenButtonAnimateToY)
self.profileStandingsButton.transform = CGAffineTransformMakeTranslation(0.0, profileStandingsButtonAnimateToY)
}) { (Bool) -> Void in
}
It's difficult to see in the image but there is a yellow horizontal line at the top of the image.

Translate transformation applied while only scale transformation added

I am creating an animation that I want to use when the app is retrieving some data online. The idea is that I have some dots in a row, they will be scaled smaller that their original sizes then return to their original size and all of this with a small delay between each scaling. The animation is repeated and use auto-reverse mode.
To do that I create some dots using a core graphic method, add them to a view and position them using a CGAffineTransformMakeTranslation transformation. Then I use a loop to animate them one by one with a delay and I use a CGAffineTransformScale transformation for scaling.
Problem: I don't get the expected animation (at least what I'm expecting). When the dots are being scaled, they also move back to their original position.
Can someone enlighten me why there is a translate transformation while in the UIView animation, I'm only specifying a scaling?
Here is the code:
private var dots = [UIImage]()
public init(numberOfDots: Int, hexaColor: Int, dotDiameter: CGFloat = 30, animationDuration: NSTimeInterval = 1) {
self.dotDiameter = dotDiameter
self.animationDuration = animationDuration
for _ in 0 ..< numberOfDots {
dots.append(GraphicHelper.drawDisk(hexaColor, rectForDisk: CGRect(x: 0, y: 0, width: dotDiameter, height: dotDiameter), withStroke: false))
}
let spaceBetweenDisks: CGFloat = dotDiameter / 3
let viewWidth: CGFloat = CGFloat(numberOfDots) * dotDiameter + CGFloat(numberOfDots - 1) * spaceBetweenDisks
super.init(frame: CGRectMake(0, 0, viewWidth, dotDiameter))
setup()
}
private func setup() {
for (i, dot) in dots.enumerate() {
let dotImageView = UIImageView(image: dot)
addSubview(dotImageView)
let spaceBetweenDisks: CGFloat = dotDiameter / 3
let xOffset = CGFloat(i) * (dotDiameter + spaceBetweenDisks)
dotImageView.transform = CGAffineTransformMakeTranslation(xOffset, 0)
}
}
public func startAnimation() {
for i in 0 ..< self.dots.count {
let dotImageView: UIImageView = self.subviews[i] as! UIImageView
let transformBeforeAnimation = dotImageView.transform
let delay: NSTimeInterval = NSTimeInterval(i)/NSTimeInterval(self.dots.count) * animationDuration
UIView.animateWithDuration(animationDuration, delay: delay, options: [UIViewAnimationOptions.Repeat, UIViewAnimationOptions.Autoreverse], animations: {
dotImageView.transform = CGAffineTransformScale(dotImageView.transform, 0.05, 0.05)
}, completion: { finished in
dotImageView.transform = CGAffineTransformIdentity
dotImageView.transform = transformBeforeAnimation
})
}
}
EDIT:
I found a fix but I don't understand how come it's fixing it. So if anyone can explain.
I added these 2 lines:
dotImageView.transform = CGAffineTransformIdentity
dotImageView.transform = transformBeforeAnimation
before this line in startAnimation:
dotImageView.transform = CGAffineTransformScale(dotImageView.transform, 0.05, 0.05)
Combining translate and scale transforms is confusing and hard to get right.
I have to spend far too much time with graph paper and deep thought in order to figure it out, and I'm too tired for that right now.
Don't do that. Place your dot image views by moving their center coordinates, and leave the transform at identity. Then when you scale them they should scale in place like you want.
Note that if you want them to move and scale at the same time you can both alter the view's center property and it's transform scale in the same animateWithDuration call and it works correctly. (Not so with changing the frame by the way. If you change the transform then the frame property doesn't work correctly any more. Apple's docs say that the results of reading/writing the frame property of a view with a non-identity transform are "undefined".)
Are you sure its going back to its original position and not scaling based on the original center point instead? Try changing the order of applying transforms by doing this:
public func startAnimation() {
for i in 0 ..< self.dots.count {
let dotImageView: UIImageView = self.subviews[i] as! UIImageView
let transformBeforeAnimation = dotImageView.transform
let delay: NSTimeInterval = NSTimeInterval(i)/NSTimeInterval(self.dots.count) * animationDuration
UIView.animateWithDuration(animationDuration, delay: delay, options: [UIViewAnimationOptions.Repeat, UIViewAnimationOptions.Autoreverse], animations: {
// make scale transform separately and concat the two
let scaleTransform = CGAffineTransformMakeScale(0.05, 0.05)
dotImageView.transform = CGAffineTransformConcat(transformBeforeAnimation, scaleTransform)
}, completion: { finished in
dotImageView.transform = CGAffineTransformIdentity
dotImageView.transform = transformBeforeAnimation
})
}
}
From apple docs:
Note that matrix operations are not commutative—the order in which you concatenate matrices is important. That is, the result of multiplying matrix t1 by matrix t2 does not necessarily equal the result of multiplying matrix t2 by matrix t1.
So, keep in mind that assigning a transformation creates a new affine transformation matrix, and concatenation will modify the existing matrix with the new one - the order you apply these in can create different results.
To make this work, I also updated the value of your translation on dotImageView. It needs to be requiredTranslation / scale.. if applying the translation before the scale. So in your viewDidLoad:
dotImageViewtransform = CGAffineTransformMakeTranslation(1000, 0)
And then the animation:
// make scale transform separately and concat the two
let scaleTransform = CGAffineTransformMakeScale(0.05, 0.05)
self.card.transform = CGAffineTransformConcat(transformBeforeAnimation, scaleTransform)

Resources