I'm implementing a wheel of fortune with a CAKeyframeAnimation and try to read the result after the animation has stopped. But here I do not get deterministic results. Is it not possible to read the rotation after the animation has stopped?
Here my animation code:
let anim = CAKeyframeAnimation(keyPath: "transform.rotation.z")
anim.duration = max(strength / 2, 1.0)
anim.isCumulative = true
anim.values = [NSNumber(value: Float(p)), Float(circleRotationOffset)]
anim.keyTimes = [NSNumber(value: Float(0)),NSNumber(value: Float(1.0))]
anim.timingFunctions = [CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)]
anim.isRemovedOnCompletion = false
anim.fillMode = kCAFillModeForwards
anim.delegate = self
wheelImage.layer.removeAllAnimations()
wheelImage.layer.add(anim, forKey: "rotate")
And here how I read the rotation:
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
readImageOrientation()
}
func readImageOrientation(){
let radians:Double = atan2( Double(wheelImage.transform.b), Double(wheelImage.transform.a))
let degrees:CGFloat = CGFloat(radians) * (CGFloat(180) / CGFloat(M_PI))
sectionForDegrees(degree: degrees)
}
For the sake of completeness here my complete class.
class WOFView: UIView, CAAnimationDelegate {
@IBOutlet weak var wheelImage: UIImageView!
private var history = [Dictionary<String, Any>]()
private var rotation: CGFloat = 0
private var startAngle: CGFloat = 0
private var circleRotationOffset: CGFloat = 0
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
if let touchPoint = touches.first?.location(in: self){
if startAngle == 0{
startAngle = atan2(self.frame.width - touchPoint.y, self.frame.height - touchPoint.x)
}
rotation = startAngle
if !touch(touches.first!, isInLeftHalfOf: wheelImage) {
rotation = -rotation
}
history.removeAll()
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesMoved(touches, with: event)
guard let touchPoint = touches.first?.location(in: self) else {
return
}
let dic = ["time" : NSNumber(value: CFAbsoluteTimeGetCurrent()),
"point": NSValue(cgPoint: touchPoint),
"rotation": NSNumber(value: Float(circleRotationOffset + rotation))]
history.insert(dic, at: 0)
if history.count == 3{
history.removeLast()
}
rotation = atan2(self.frame.width - touchPoint.y, self.frame.height - touchPoint.x) - startAngle
if !touch(touches.first!, isInLeftHalfOf: wheelImage) {
rotation = -rotation
}
wheelImage.transform = CGAffineTransform(rotationAngle: circleRotationOffset + rotation)
print("offset: \(circleRotationOffset)")
readImageOrientation()
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
guard let touchPoint = touches.first?.location(in: self) else {
return
}
guard let lastObject = history.last else{
return
}
guard let pointValue = lastObject["point"] as? CGPoint else{
return
}
guard let timeValue = lastObject["time"] as? NSNumber else {
return
}
guard let rotationValue = lastObject["rotation"] as? NSNumber else {
return
}
let timeDif = CFAbsoluteTimeGetCurrent() - (timeValue.doubleValue)
circleRotationOffset = circleRotationOffset + rotation
let lastRotation = rotationValue.floatValue
let dist = sqrt(((pointValue.x - touchPoint.x) * (pointValue.x - touchPoint.x)) +
((pointValue.y - touchPoint.y) * (pointValue.y - touchPoint.y)))
let strength = max(Double(min(1.0, dist / 80.0)) * (timeDif / 0.25) * M_PI * 2, 0.3) * 30
let p = circleRotationOffset
let dif = circleRotationOffset - CGFloat(lastRotation)
var inc = dif > 0
if dif > 3 || dif < -3{
inc = !inc
}
if (inc){
circleRotationOffset += CGFloat(strength)
}else{
circleRotationOffset -= CGFloat(strength)
}
let anim = CAKeyframeAnimation(keyPath: "transform.rotation.z")
anim.duration = max(strength / 2, 1.0)
anim.isCumulative = true
anim.values = [NSNumber(value: Float(p)), Float(circleRotationOffset)]
anim.keyTimes = [NSNumber(value: Float(0)),NSNumber(value: Float(1.0))]
anim.timingFunctions = [CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)]
anim.isRemovedOnCompletion = false
anim.fillMode = kCAFillModeForwards
anim.delegate = self
wheelImage.layer.removeAllAnimations()
wheelImage.layer.add(anim, forKey: "rotate")
}
func touch(_ touch:UITouch, isInLeftHalfOf view: UIView) -> Bool {
let positionInView = touch.location(in: view)
return positionInView.x < view.frame.midX
}
func touch(_ touch:UITouch, isInUpperHalfOf view: UIView) -> Bool {
let positionInView = touch.location(in: view)
return positionInView.y < view.frame.midY
}
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
readImageOrientation()
}
func readImageOrientation(){
let radians:Double = acos(Double(wheelImage.transform.a))
let degrees:CGFloat = CGFloat(radians) * (CGFloat(180) / CGFloat(M_PI))
sectionForDegrees(degree: degrees)
}
func sectionForDegrees(degree: CGFloat){
var result = "not defined"
switch degree{
case 0 ... 90:
result = "3 \(degree)"
case 90.1...180:
result = "2 \(degree)"
case 181.1...270:
result = "1 \(degree)"
case 270.1...360:
result = "4 \(degree)"
default:
result = "not defined: \(degree)"
}
print(result)
}
}
According to this Objective-C answer there are (at least) four ways to get the rotation:
let view = UIImageView()
view.transform = CGAffineTransform(rotationAngle: 0.02);
let x = view.value(forKeyPath: "layer.transform.rotation.z")
let a = acos(view.transform.a)
let b = asin(view.transform.b)
let c = atan2(view.transform.b, view.transform.a)
print(a)
print(b)
print(c)
print(x!)
Prints:
0.0200000000000011
0.02
0.02
0.02
I think you should use acos()
instead of atan2()
, based on how a three dimensional rotation matrix looks:
In this Rz matrix we can see that transform.a and transform.b will be given as cosine theta. Ref CGAffineTransform documentation:
Without testing, I'm pretty sure a simple acos(Double(wheelImage.transform.a))
would be enough to give you your theta rotation. EDIT: This was bad advice, replacing atan2
with acos
means you have to check if your answer is a positive or a negative cosine value, and is completely pointless. Another thing to remember is that the transform will also change if you apply any scaling to your wheel.
If I'm wrong, you can always use the strength
value to calculate how long you are going to allow the wheel to spin and find the angle from that. This would be require you to learn how the kCAMediaTimingFunctionEaseOut
works exactly, so I would instead suggest you to change the animation to first calculate an angle (that you save as a parameter) then apply this directly to the layer.transform
instead of using the duration as the decisive factor.
After seeing your complete code, I found out that you had only interpreted the angle wrong, here is a rewrite of your sectionForDegrees
function that will give the correct section:
func sectionForDegrees(degree: CGFloat){
var result = "not defined"
switch degree{
case 0.1 ... 90:
result = "1 \(degree)"
case 90.1...180:
result = "4 \(degree)"
case -90.1...0:
result = "2 \(degree)"
case -180...(-90):
result = "3 \(degree)"
default:
result = "not defined: \(degree)"
}
print(result)
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With