ios jerky animation when applying scaling transformation - ios

I have this icon that I want the user to be able to move around and when it enters a specific area of its parent view, it should scale up in size to indicate that. I retrieve where the user wants to move the icon with a pan gesture recognizer and the command:
let translationPoint = sender.translation(in: view)
Then I try to animate the desired behavior with CGAffineTranformations with the following code:
let moveTransformation = CGAffineTransform(translationX: response.translationPoint.x, y: response.translationPoint.y)
var scaleTransformation: CGAffineTransform = CGAffineTransform(scaleX: 1, y: 1)
if response.shouldScaleUp {
scaleTransformation = CGAffineTransform(scaleX: 1.3, y: 1.3)
}
let transformation = scaleTransformation.concatenating(moveTransformation)
And then I apply the transformation on the icon view. It works quite well except for the fact that it jerks a bit when I enter and exit the area that should trigger this behavior.
I've read online that it's in general a bad idea to be applying two transformations, and I thought that maybe I should just update the actual frame of the view itself, but animating transformation changes makes it easier to reset the position when the user lets go (I've also heard it's meant to be lighter to do).
You guys and girls have any suggestions? Thanks for your help
UPDATE
My animation code:
DispatchQueue.main.async {
UIView.animate(withDuration: 0.15) {
self.iconView.transform = transformation
}
}

There should be no problem applying multiple transforms. Without seeing more of your code(Animation Block or Network Call for response) I am going to make two guesses
1) You are missing your UIView animation block.
let translationPoint = sender.translation(in: view)
let moveTransformation = CGAffineTransform(translationX: response.translationPoint.x, y: response.translationPoint.y)
var scaleTransformation: CGAffineTransform = .identity
if response.shouldScaleUp {
scaleTransformation = CGAffineTransform(scaleX: 1.3, y: 1.3)
}
let transformation = scaleTransformation.concatenating(moveTransformation)
DispatchQueue.main.async {
UIView.animate(withDuration: 0.2) {
iconView.transform = scaleTransformation
}
}
2) More likely or combined with the problem above you are trying to change the UI on a background thread from the network call. This could create delays and jerkiness and would need to be wrapped in a main thread call like above animation.
let translationPoint = sender.translation(in: view)
let moveTransformation = CGAffineTransform(translationX: response.translationPoint.x, y: response.translationPoint.y)
var scaleTransformation: CGAffineTransform = .identity
if response.shouldScaleUp {
scaleTransformation = CGAffineTransform(scaleX: 1.3, y: 1.3)
}
let transformation = scaleTransformation.concatenating(moveTransformation)
DispatchQueue.main.async {
iconView.transform = scaleTransformation
}

Nest the icon view in a parent view. Apply the position translation to the parent view and the scale to the child view.
This keeps the transforms separate and the result more easily predictable.

Related

How to implement such pop over view animation?

I was using this ride-sharing ios app and found this pop over animation of options view. I wanted to implement similar pop over but could figure out if it is a custom transition or animation? Here is a link to the GIF of popover in application.
It will be helpful to link me to examples/tutorial/code with similar animation so that I can begin implementing on my ios app.
This is pretty simple. Just position your options view on the screen where you want it to appear and set the alpha to 0 so it is hidden. Then, prior to animation, make the view wider using scaleX, and translate it down using translationY. Then simply animate it back to .identity and animate the alpha back to 1.0 so it fades in. A basic example is below. When you dismiss the view you just do the opposite. Let me know if you need help with that.
yourView.transform = CGAffineTransform(scaleX: 1.3, y: 0)
yourView.transform = CGAffineTransform(translationX: 0.0, y: 200.0)
yourView.alpha = 0
UIView.animate(withDuration: 0.3, animations: {
yourView.transform = .identity
yourView.alpha = 1.0
})
I achieved the following animation using leading, trailing and button constraints. Please let me know if such animation can be done in other ways.
yourview.alpha = 0
UIView.animate(withDuration: 0.6, animations: {
self.leadingConstraints.constant = 20
self.trailConstraints.constant = -20
self.buttonConstraints.constant = 20
self.yourview.alpha = 1
self.yourview.layoutIfNeeded()
})

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.animate problems with view frame

I'm struggling trying to make an animation. This is what's happening: video
The back card was supposed to do the same animation as in the beginning of the video, but it's doing a completely different thing. I'm checking the UIView.frame at the beginning of the animation and it's the same as the first time the card enters, but obviously something is wrong... Here is the code:
func cardIn() {
let xPosition = (self.darkCardView?.frame.origin.x)! - 300
let yPosition = (self.darkCardView?.frame.origin.y)!
self.initialPos = self.darkCardView.frame
let height = self.darkCardView?.frame.size.height
let width = self.darkCardView?.frame.size.width
self.darkCardView?.transform = (self.darkCardImageView?.transform.rotated(by: CGFloat(Double.pi/4)))!
UIView.animate(withDuration: 1.1, animations: {
self.darkCardView?.frame = CGRect(x: xPosition, y: yPosition, width: width!, height: height!)
self.darkCardView?.transform = (self.darkCardImageView?.transform.rotated(by: CGFloat(0)))!
})
}
func cardOut() {
let xPosition = (self.darkCardView?.frame.origin.x)! - 600
let yPosition = (self.darkCardView?.frame.origin.y)!
let height = self.darkCardView?.frame.size.height
let width = self.darkCardView?.frame.size.width
self.darkCardView?.transform = (self.darkCardImageView?.transform.rotated(by: CGFloat(0)))!
UIView.animate(withDuration: 1.0, delay: 0, options: .allowAnimatedContent, animations: {
self.darkCardView?.frame = CGRect(x: xPosition, y: yPosition, width: width!, height: height!)
self.darkCardView?.transform = (self.darkCardImageView?.transform.rotated(by: CGFloat(-Double.pi/4)))!
}) { (true) in
self.darkCardView?.transform = (self.darkCardImageView?.transform.rotated(by: CGFloat(0)))!
self.darkCardView?.frame = self.initialPos
self.cardIn()
}
}
Does somebody know how can I repeat the same animation that's in the beginning of the video after cardOut function is called?
Given that we do not know where/when you are calling the above two functions I can only take a guess at what is happening.
So you have an animate in and an animate out, but what about a reset function?
Timeline:
Animate the card in.
...
Eventually animate the card out
...
Reset the cards location to off screen to the right (location before animating in for the first time)
...
animate the card in
...
Repeat, ect....
I managed to solve the animation problem by calling viewDidLoad() instead of calling self.cardIn() in the end o cardOut completion block. But I don't want to depend on calling viewDidLoad every time a new card has to enter the screen...
Also, I didn't set any properties of the back card in viewDidLoad. I only have it in the .xib file. Any ideas?
Note that if you set a view's transform to anything other than the identity transform then the view's frame rect becomes "undefined". You can neither set it to a new value nor read it's value reliably.
You should change your code to use the view's center property if you're changing the transform.
As Jerland points out in their post, you also probably need to reset your back card to it's starting position before beginning the next animation cycle. (Set it to hidden, put it's center back to the starting position and set it's transform back to the identity transform.
EDIT:
This code:
UIView.animate(withDuration: 1.0, delay: 0, options: .allowAnimatedContent, animations: {
self.darkCardView?.frame = CGRect(x: xPosition, y: yPosition, width: width!, height: height!)
self.darkCardView?.transform = (self.darkCardImageView?.transform.rotated(by: CGFloat(-Double.pi/4)))!
}) { (true) in
self.darkCardView?.transform = (self.darkCardImageView?.transform.rotated(by: CGFloat(0)))!
self.darkCardView?.frame = self.initialPos
self.cardIn()
}
Does not set the transform to identity. Try changing that line to
self.darkCardView?.transform = .identity

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).

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