How do eliminate the "jump" in my CABasicAnimation? - ios

Using this code:
func rotateTheView(_ aView: UIView, inClockwiseDirection isClockwise: Bool) {
let multiplier = (isClockwise ? 1 : -1)
let key = (isClockwise ? "Spin" : "Rotate")
var rotation: CABasicAnimation?
rotation = CABasicAnimation(keyPath: "transform.rotation")
rotation!.fromValue = Int(0)
let multiplicand = multiplier * 2
rotation!.toValue = Int(Double(multiplicand) * .pi)
rotation!.duration = 30 // Speed
rotation!.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
rotation!.repeatCount = HUGE //HUGE_VALF Repeat forever.
aView.layer.add(rotation!, forKey: key)
}
I get the animation I want. (Either a wheel spins, or a cell rotates fast enough in the opposite direction to always remain exactly right side up).
However, when the 30 seconds (duration) is up, there is a flicker as the view jumps back to how it looked before the animation.
I understand it is supposed to work this way.
How do I apply the rotation to the "before" image so that when the duration expires I don't see any cells jump?
Increasing the duration of the animation slows the wheel's spin, so that is not an appropriate solution.
If #22521690 applies, I don't understand how - I do not have an explicit CATransaction.

Try
rotation!.toValue = Double(multiplicand) * .pi
instead of
rotation!.toValue = Int(Double(multiplicand) * .pi)
The issue is with the radian precision which is lost due to Int conversion.

On the next line after applying the animation to the layer, set the property you're animating to its ending value.
Because an animation is "in-flight", the normal display of the layer is covered the presentation layer, a special animation layer.
Once the animation is complete, the presentation layer is hidden/removed, and the actual layer is exposed. If it is a the same state as the presentation layer at the end of the animation then there's no jump.
That code might look like this:
func rotateTheView(_ aView: UIView, inClockwiseDirection isClockwise: Bool) {
let multiplier = (isClockwise ? 1 : -1)
let key = (isClockwise ? "Spin" : "Rotate")
var rotation: CABasicAnimation?
rotation = CABasicAnimation(keyPath: "transform.rotation")
rotation!.fromValue = 0.0
let multiplicand = multiplier * 2
let finalAngle = Double(multiplicand) * .pi
rotation!.toValue = finalAngle
rotation!.duration = 30 // Speed
rotation!.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
rotation!.repeatCount = HUGE //HUGE_VALF Repeat forever.
aView.layer.add(rotation!, forKey: key)
//-------------------
//Set the final transform on the layer to the final rotation,
//but without animation
CATransaction.begin()
CATransaction.setDisableActions(true)
let finalTransform = CATransform3DMakeRotation(CGFloat(finalAngle), 0.0, 0.0, 1.0)
aView.layer.transform = finalTransform
CATransaction.commit()
//-------------------
}

Related

How do you wipe in from left to right and simultaneously (after a few milliseconds) while out an image view?

I have an image and I am trying to show only part of image from left to right:
So what I am trying to achieve is:
To make it feel like it is traveling from one point to another.
I am confused as to how to achieve this like having a wipe in animation and wait for second and have wipe out from same direction to recreate the effect using static image.
I did work on animation where the image wipes from bottom using CABasicAnimation
func reveal() -> Void {
let anim1: CABasicAnimation = CABasicAnimation(keyPath: "startPoint.y")
// anim1 animates the gradient start point Y
// to -0.1 (1/10th above the top of the view)
anim1.toValue = -0.1
anim1.duration = 1.0
anim1.isRemovedOnCompletion = false
anim1.fillMode = .forwards
let anim2: CABasicAnimation = CABasicAnimation(keyPath: "endPoint.y")
// anim2 animates the gradient end point Y
// to 0.0 (the top of the view)
anim2.toValue = 0.0
anim2.duration = 1.0
anim2.isRemovedOnCompletion = false
anim2.fillMode = .forwards
maskLayer.add(anim1, forKey: nil)
maskLayer.add(anim2, forKey: nil)
}
Could someone point me in the right direction of how to achieve this?

What is a correct way to layer styling before begin time of core animation?

I created a function with Core Animation that animate the layer height from 0 to N and it's delayable.
extension CALayer {
func animate(to height: CGFloat, duration: Double, delay: Double) {
let animation: CABasicAnimation = .init(keyPath: "bounds.size.height")
animation.fromValue = 0
animation.toValue = height
animation.beginTime = CACurrentMediaTime() + delay
animation.duration: duration
animation.timingFunction = .init(name: kCAMediaTimingFunctionEaseInEaseOut)
// I want to improvement this part.
//
// self.isHidden = true
//
// DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
// self.isHidden = false
// }
self.bounds.size.height = height
self.add(animation, forKey: "bounds.size.height")
}
}
And the layer does transform well during animation, but it returns to original height before begin time and after finish. So I had to set isHidden of layer according to time to delay.
But I don't think it's a safe way. So I want to improvement that code.
What do you usually do in this case?
Try setting animation.fillMode = .backwards. I think that will make the animation will apply its fromValue to the layer until the animation's beginTime is reached.

How to make image spin (animation)

I have an image which is a square and I know how to make it spin. But not sure how to make it spin exactly like this animation here:
Notice how it spins... then stops a little... then spins again... and so on.
What I have just is a basic spin but doesn't look like the gif above:
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)
}
}
Any thoughts on how this is done?
You need to use a timing function that accelerates and decelerates, also known as an ease-in-ease-out timing function.
I've modified your function to use Core Animation's standard ease-in-ease-out timing function:
extension UIView {
func rotate360Degrees(duration: CFTimeInterval = 0.8) {
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotateAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
let radians = CGFloat.pi / 4
rotateAnimation.fromValue = radians
rotateAnimation.toValue = radians + .pi
rotateAnimation.isRemovedOnCompletion = false
rotateAnimation.duration = duration
rotateAnimation.repeatCount=Float.infinity
self.layer.add(rotateAnimation, forKey: nil)
}
}
Result:
Note that in your image, it looks like the box pauses every 180 degrees, so I've changed the function to rotate by only 180 degrees. Since the box has 90 degree radial symmetry, it still looks like it goes all the way around, with pauses at 180 and 360 degrees.
If you need to animate an image without any radial symmetry, you'll need to use a CAKeyframeAnimation to achieve the ease-in-ease-out at both 180 and 360 degrees.
Similar effect can also be achieved with a repeated animation with a delay.
UIView.animate(withDuration: 1,
delay: 0.2,
options: [.repeat],
animations: { imageView.transform = imageView.transform.rotated(by: CGFloat.pi) },
completion: nil)

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)

Collapse A UIImageView From A Center Point

I have a UIImageView that I want to scale its outter edges towards the center (the top and bottom edges to be precise). The best I am able to get so far is just one edge collapsing into the other (which doesn't move). I want both edges to collapse towards the middle.
I have tried setting the the anchor point to 0.5,0.5 but that doesnt address the issue (nothing changes).
Is there a way to setup an ImageView such that it will scale inwards towards the middle so the outter edges collapse towards the center?
Here was my best attempt:
override public func viewWillAppear(animated: Bool)
{
super.viewWillAppear(animated)
var uimg = UIImage(named: "img.PNG")!
var img:UIImageView = UIImageView(image: uimg)
img.layer.anchorPoint = CGPointMake(0.5, 0.5)
view.addSubview(img)
UIView.animateWithDuration(5, animations:
{ () -> Void in
img.frame = CGRectMake(img.frame.origin.x,img.frame.origin.y,img.frame.width, 0)
})
{ (finished) -> Void in
//
}
}
You can use scaling using UIView's transform property. It seems like scaling does not allow you to make the value 0 and it goes to the final value without any animation. So, you could basically make the scaling y value negligible like 0.001.
imageView.transform = CGAffineTransformMakeScale(1, 0.001)
You could also use core animation to scale, using CABasicAnimation, this would be more simpler as this,
let animation = CABasicAnimation(keyPath: "transform.scale.y")
animation.toValue = 0
animation.duration = 2.0
animation.fillMode = kCAFillModeForwards
animation.removedOnCompletion = false
imageView.layer.addAnimation(animation, forKey: "anim")
Or you can also use the approach you have been using, simply setting the frame,
var finalFrame = imageView.frame
finalFrame.size.height = 0
finalFrame.origin.y = imageView.frame.origin.y + (imageView.frame.size.height / 2)
UIView.animateWithDuration(2.0, animations: { () -> Void in
self.imageView.frame = finalFrame
})
Your new frame has the origin in the same place. You need to move the origin down, at the same time as you reduce the height:
img.frame = CGRectMake(img.frame.origin.x, img.frame.origin.y + (img.frame.size.height/2), img.frame.size.width, 0);

Resources