ios UIImageView rotation direction in swift at variable degree in clockwise direction - ios

I am rotating image through this code
UIView.animate(withDuration: 2) {
self.blueNeedleImageView.transform = CGAffineTransform(rotationAngle: CGFloat(myDegreeValue * Double.pi / 180))
}
Problem is that it is rotating in shortest path. I want always clockwise rotation and rotation degree is variable. this and this does not provide a solution.

Just split it to two animations if the angle is more than 180°.

You can rotate your Image as per your requirement with the help of CoreAnimation like below:
let angle = myDegreeValue
if let n = NumberFormatter().number(from: angle) {
let floatAngle = Double(truncating: n)
CATransaction.begin()
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotationAnimation.byValue = NSNumber(value: floatAngle * (Double.pi / 180))
rotationAnimation.duration = 2
rotationAnimation.isRemovedOnCompletion = true
CATransaction.setCompletionBlock {
self.blueNeedleImageView.transform = CGAffineTransform(rotationAngle: CGFloat(floatAngle * (Double.pi / 180)))
}
self.blueNeedleImageView.layer.add(rotationAnimation, forKey: "rotationAnimation")
CATransaction.commit()
}

Related

Circle animation iOS UIKit behaviour with tail not completing fully

So I'm trying to learn how to draw circles in UIKit and I've got them pretty much figured it out but I'm just trying to implement one more thing. In the video below when the tail of the circle reaches the end I would like for the tail to not reach the head fully, meaning I would like the size of the circle to not shrink completely.
I sort of have it in the video below but there is still the snap were the tails goes away and the animation starts again at the head. So I would like the disappearance of the tail to not go away.
Video Demo: https://github.com/DJSimonSays93/CircleAnimation/blob/main/README.md
Here is the code:
class SpinningView: UIView {
let circleLayer = CAShapeLayer()
let rotationAnimation: CAAnimation = {
let animation = CABasicAnimation(keyPath: "transform.rotation.z")
animation.fromValue = 0
animation.toValue = Double.pi * 2
animation.duration = 3 // increase this duration to slow down the circle animation effect
animation.repeatCount = MAXFLOAT
return animation
}()
override func awakeFromNib() {
super.awakeFromNib()
setup()
}
func setup() {
circleLayer.lineWidth = 10.0
circleLayer.fillColor = nil
//circleLayer.strokeColor = UIColor(red: 0.8078, green: 0.2549, blue: 0.2392, alpha: 1.0).cgColor
circleLayer.strokeColor = UIColor.systemBlue.cgColor
circleLayer.lineCap = .round
layer.addSublayer(circleLayer)
updateAnimation()
}
override func layoutSubviews() {
super.layoutSubviews()
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let radius = min(bounds.width, bounds.height) / 2 - circleLayer.lineWidth / 2
let startAngle: CGFloat = -90.0
let endAngle: CGFloat = startAngle + 360.0
circleLayer.position = center
circleLayer.path = createCircle(startAngle: startAngle, endAngle: endAngle, radius: radius).cgPath
}
private func updateAnimation() {
//The strokeStartAnimation beginTime + duration value need to add up to the strokeAnimationGroup.duration value
let strokeStartAnimation: CABasicAnimation = CABasicAnimation(keyPath: "strokeStart")
strokeStartAnimation.beginTime = 0.5
strokeStartAnimation.fromValue = 0
strokeStartAnimation.toValue = 0.93 //change this to 0.93 for cool effect
strokeStartAnimation.duration = 3.0
strokeStartAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
let strokeEndAnimation: CABasicAnimation = CABasicAnimation(keyPath: "strokeEnd")
strokeEndAnimation.fromValue = 0
strokeEndAnimation.toValue = 1.0
strokeEndAnimation.duration = 2.0
strokeEndAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
let colorAnimation = CABasicAnimation(keyPath: "strokeColor")
colorAnimation.fromValue = UIColor.systemBlue.cgColor
colorAnimation.toValue = UIColor.systemRed.cgColor
let strokeAnimationGroup: CAAnimationGroup = CAAnimationGroup()
strokeAnimationGroup.duration = 3.5
strokeAnimationGroup.repeatCount = Float.infinity
strokeAnimationGroup.fillMode = .forwards
strokeAnimationGroup.isRemovedOnCompletion = false
strokeAnimationGroup.animations = [strokeStartAnimation, strokeEndAnimation, colorAnimation]
circleLayer.add(strokeAnimationGroup, forKey: nil)
circleLayer.add(rotationAnimation, forKey: "rotation")
}
private func createCircle(startAngle: CGFloat, endAngle: CGFloat, radius: CGFloat) -> UIBezierPath {
return UIBezierPath(arcCenter: CGPoint.zero,
radius: radius,
startAngle: startAngle.toRadians(),
endAngle: endAngle.toRadians(),
clockwise: true)
}
Something like this?
There is nothing special here. It is almost exactly the same as your initial code but with a small tweak for the rotation angle.
Approach
Your initial animation looks great to start with! Like you said, the "snap" where the animation restarts from 0% of the strokeEnd is what gives it off.
As #MadProgrammer pointed out, theoretically you can get rid of the "snap" by never starting or ending the stroke at 0%. This ensures there is always some portion of the stroke visible.
This is a great start, but unfortunately strokeStart and strokeEnd do not allow values outside of the [0.0, 1.0] range. So you can't exactly create an animation (without many keyframes) so that the stroke positions overlap in each animation loop (because you would need to use values out of range to cover the full circle).
So, what I have done is use the above method anyway and ended up with the animation shown below. The arc length of the stroke at the start and end of the animation are equal - very important.
Then, using your existing rotation animation I very slightly rotate the entire drawing during the stroke animation so that the start and end arcs seem to land on top of each other. The rotation angle was calculated as follows.
0.07 was selected by subtracting your initial value for strokeStartAnimation.toValue by 1.0.
The scalar length of the arc would then be, 0.07 (S).
The radius of the circle would bounds.width / 2 (r).
To obtain the arc length (L), we need to multiply scalar length by the Perimeter (P).
The relationship between arc length (L) and the rotation angle (theta) is,
2 * Theta * r = L
But L is also equal to S * P, so some substituting around and we get,
theta = 2S (in Radians)
The Solution
So, with that out of the way. The solution is the following changes to your code.
Define the scalar arc length as a class variable, startOffset.
Use startOffset to set the toValue of the strokeStart anim.
Use startOffset to set the fromValue of the strokeEnd anim.
Set the to value of rotationAnimation to 2 * theta.
Match Rotation animation duration with stroke animation duration.
The final rotation animation looks like this:
var rotationAnimation: CAAnimation{
get{
let radius = Double(bounds.width) / 2.0
let perimeter = 2 * Double.pi * radius
let theta = perimeter * startOffset / (2 * radius)
let animation = CABasicAnimation(keyPath: "transform.rotation.z")
animation.fromValue = 0
animation.toValue = theta * 2 + Double.pi * 2
animation.duration = 3.5
animation.repeatCount = MAXFLOAT
return animation
}
}
And the strokes:
let strokeStartAnimation: CABasicAnimation = CABasicAnimation(keyPath: "strokeStart")
strokeStartAnimation.beginTime = 0.5
strokeStartAnimation.fromValue = 0
strokeStartAnimation.toValue = 1.0 - startOffset
strokeStartAnimation.duration = 3.0
strokeStartAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
let strokeEndAnimation: CABasicAnimation = CABasicAnimation(keyPath: "strokeEnd")
strokeEndAnimation.fromValue = startOffset
strokeEndAnimation.toValue = 1.0
strokeEndAnimation.duration = 2.0
strokeEndAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
I made a pull request to your existing code. Try it out and let me know how it goes.

Reverse the rotation of UIImageView using CAKeyframeAnimation

I'm using the following code to rotate an image infinitely, the rotation is clockwise but I also need it to reverse rotation to counter clockwise every 1-2 rotations and then back to clockwise, how to get this to work?
/// Image Rotation - CAKeyframeAnimation
func rotate(imageView: UIImageView, rotationSpeed: Double) {
let animation = CAKeyframeAnimation(keyPath: "transform.rotation.z")
animation.duration = rotationSpeed
animation.fillMode = CAMediaTimingFillMode.forwards
animation.repeatCount = .infinity
animation.values = [0, Double.pi/2, Double.pi, Double.pi*3/2, Double.pi*2]
/// Percentage of each key frame
let moments = [NSNumber(value: 0.0), NSNumber(value: 0.1),
NSNumber(value: 0.3), NSNumber(value: 0.8), NSNumber(value: 1.0)]
animation.keyTimes = moments
imageView.layer.add(animation, forKey: "rotate")
}
Set animation.autoreverses = true, will reverse an animation.
for counter clockwise rotation, you can do this:
self.imageView?.transform = CGAffineTransform(rotationAngle: -2*CGFloat.pi)

How to control animation rotation direction in Swift past 180º?

According to the documentation for rotated(by:):
angle
The angle, in radians, by which to rotate the affine transform. In iOS, a positive value specifies counterclockwise rotation and a negative value specifies clockwise rotation.
This has caused much confusion and has been partly answered, but even if we accept that the coordinate system is flipped (and just do the opposite of what the documentation says), it still provides inconsistent results when animating.
The direction of rotation -- clockwise or counterclockwise -- depends on the proximity of the target rotation to the current rotation:
<= 180º animates clockwise
> 180º animates counter-clockwise
Using UIView.animate or UIViewPropertyAnimator shows the inconsistency:
// Animates CLOCKWISE
UIView.animate(withDuration: 2.0) {
let radians = Angle(179).radians // 3.12413936106985
view.transform = view.transform.rotated(by: CGFloat(radians))
}
// Animates COUNTER-CLOCKWISE
UIViewPropertyAnimator(duration: 2, curve: .linear) {
let radians = Angle(181).radians // 3.15904594610974
view.transform = view.transform.rotated(by: CGFloat(radians))
}
// Does not animate
UIViewPropertyAnimator(duration: 2, curve: .linear) {
let radians = Angle(360).radians // 6.28318530717959
view.transform = view.transform.rotated(by: CGFloat(radians))
}
Try this:
let multiplier: Double = isSelected ? 1 : -2
let angle = CGFloat(multiplier * Double.pi)
UIView.animate(withDuration: 0.4) {
self.indicatorImageView.transform = CGAffineTransform(rotationAngle: angle)
}
The answer is to use CABasicAnimation for rotations past 180º, keeping in mind that positive values are clockwise and negative values are counterclockwise.
// Rotate 360º clockwise.
let rotate = CABasicAnimation(keyPath: "transform.rotation")
rotate.fromValue = Angle(0).radians
rotate.toValue = Angle(360).radians
rotate.duration = 2.0
self.layer.add(rotate, forKey: "transform.rotation")
if sender.isSelected {
/// clockwise
let rotate = CABasicAnimation(keyPath: "transform.rotation")
rotate.fromValue = 0
rotate.toValue = CGFloat.pi
rotate.duration = 0.25
rotate.fillMode = CAMediaTimingFillMode.forwards
rotate.isRemovedOnCompletion = false
self.arrowButton.layer.add(rotate, forKey: "transform.rotation")
} else {
/// anticlockwise
let rotate = CABasicAnimation(keyPath: "transform.rotation")
rotate.fromValue = CGFloat.pi
rotate.toValue = 0
rotate.duration = 0.25
rotate.fillMode = CAMediaTimingFillMode.forwards
rotate.isRemovedOnCompletion = false
self.arrowButton.layer.add(rotate, forKey: "transform.rotation")
}
The third option allows you to rotate over 180º in the desired direction:
// #1 This rotates 90º clockwise
UIView.animate(withDuration: 0.25, animations:{
view.transform = CGAffineTransform(rotationAngle: -CGFloat.pi/2)
})
// #2 This rotates 90º counter clockwise back to #1
UIView.animate(withDuration: 0.25, animations:{
view.transform = CGAffineTransform(rotationAngle: CGFloat.pi)
})
// #3 This rotates 270º clockwise back to #1
UIView.animate(withDuration: 0.1875, animations:{
view.transform = CGAffineTransform(rotationAngle: CGFloat.pi/2)
}, completion: { _ in
UIView.animate(withDuration: 0.0625, animations:{
view.transform = CGAffineTransform(rotationAngle: CGFloat.pi)
})
})

StrokeEnd not drawing full circle

I'm trying to build a progress view for a Music Player. I am totaly new to programming in general and now really reached a dead end here.
My progress works fine. I am animatig "strokeEnd" with the value (value/duration) of the Mediafile, but strokeEnd only ever reaches have of the circle. However, the song is then finished as well, this is why I think, somewhere it calculats wrong.
It is a very simple Shaplayer I am using.
var value: CGFloat = 0.0{
didSet {
redrawStrokeEnd() }
}
private func setupShapeLayer(shapeLayer: CAShapeLayer) {
let startAngle = CGFloat(M_PI_2)
let endAngle = CGFloat(M_PI_2 * 2 + M_PI_2)
progressLayer.path = UIBezierPath(arcCenter:centerPoint, radius: CGRectGetWidth(frame)/2 + 5, startAngle:startAngle, endAngle: endAngle, clockwise: true).CGPath
progressLayer.strokeStart = 0.0
progressLayer.strokeEnd = 0.0 }
func redrawStrokeEnd() {
progressLayer.strokeEnd = CGFloat(value / CGFloat(duration))}
value = currentTime and duration = Playbackduration.
I can't figure out, why stroke End does not move the full circle.
Perhaps you're using the wrong constant? As some of the other commenters have pointed out, a circle in radians traverses 2 * pi. However the constant M_PI_2 is equal to (pi / 2) So right now you're only traversing from (pi/2) -> (3pi/2)... or pi. If you really want to start at pi/2 try:
let startAngle = CGFloat(M_PI_2)
let endAngle = CGFloat(M_PI_2 + 2.0 * M_PI)
It's not strokeEnd the reason you see half circle. You just have wrong start and end angles for your path. You can set them as below to have a full circle :
let startAngle = CGFloat(M_PI_2)
let endAngle = CGFloat(5*M_PI_2)
Or better if you don't want to set angles by radians set them in degrees like this :
let startAngle = CGFloat(0 * M_PI / 180)
let endAngle = CGFloat(360 * M_PI / 180.0)
Just changing the start angle and end angle in your code will work for you.
let startAngle = -90.0
let endAngle = -90.01

How to change start point of a circular animation in Swift by using CABasicAnimation?

I have a function to draw an animated circle as a processing bar. I can get it animated from the bottom of the circle. However, I would like to change the start point to the top of the circle.
I attached my code below:
func animateProgressView(_finishPoint:CGFloat, _stringShowAtEnd: String) {
progressLabel.text = "Rating..."
progressLayer.strokeEnd = 0.0
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = CGFloat(0.0)
animation.toValue = CGFloat(_finishPoint)
animation.duration = 2.0
animation.delegate = self
animation.removedOnCompletion = false
animation.additive = true
animation.fillMode = kCAFillModeForwards
progressLayer.addAnimation(animation, forKey: "strokeEnd")
stringShowAtEnd=_stringShowAtEnd
}
What I have quickly tried:
changed fromValue and toValue to 0.5 and 1.0. //didn't work
changed the fillMode to kCAFillModeBackwards. //didn't work
changed the keyPath and forKey to "strokeStart" //didn't work
I haven't spent too much time on reading the official documents.
Anyone can provide a quick answer how can I change the start point to the top like the picture shown below?
Solution:
Thank "rob mayoff". He point me out I should post the path creation function, which I didn't notice and recognize. I solved the problem by modifying the angles of both startAngle and endAngle.
I attached the code below. I hope it will be helpful for everyone who has the similar issue.
private func createProgressLayer() {
//let startAngle = CGFloat(M_PI_2) //old angle, Pi/2=90 degrees ,started from the bottom
//let endAngle = CGFloat(M_PI * 2 + M_PI_2) //old angle,2*Pi+Pi/2=360 + 90 degrees ,end at the bottom
let startAngle = CGFloat(M_PI_2*3) //new angle, (Pi/2)*3=90*3 degrees ,starts from the top
let endAngle = CGFloat(M_PI * 2 + M_PI_2*3) //new angle, 2*Pi+(Pi/2)*3=360 + 90*3 degrees ,ends at the top
let centerPoint = CGPointMake(CGRectGetWidth(frame)/2 , CGRectGetHeight(frame)/2)
var gradientMaskLayer = gradientMask()
progressLayer.path = UIBezierPath(arcCenter:centerPoint, radius: CGRectGetWidth(frame)/2 - 30.0, startAngle:startAngle, endAngle:endAngle, clockwise: true).CGPath
progressLayer.backgroundColor = UIColor.clearColor().CGColor
progressLayer.fillColor = nil
progressLayer.strokeColor = UIColor.blackColor().CGColor
progressLayer.lineWidth = 10.0
progressLayer.strokeStart = 0.0
progressLayer.strokeEnd = 0.0
gradientMaskLayer.mask = progressLayer
layer.addSublayer(gradientMaskLayer)
}
You're creating the path so that the start point is at the bottom. You need to create the path so that the start point is at the top.

Resources