Drehe Rechteck und kontrolliere Animation mit Slider - wurzelsand/uikit-memos GitHub Wiki
Das Rechteck soll bei 15º Neigung starten und fast eine komplette Drehung im Uhrzeigersinn machen (345º). Beim Berühren soll die Animation starten. Dabei bewegt sich ein Slider mit. Mit diesem Slider lässt sich durch die gesamte Animation scrollen.
-
UIViewPropertyAnimator
um die Animation zu steuern. -
NSLayoutConstraints
um das Rechteck zu zentrieren. -
CADisplayLink
um den Slider bei jedem Bildaufbau zu aktualisieren und so mit der Animation zu synchronisieren.
import UIKit
class ViewController: UIViewController {
let slider = UISlider()
var displayLink: CADisplayLink?
let animator = UIViewPropertyAnimator(duration: 2, curve: .easeInOut)
override func viewDidLoad() {
super.viewDidLoad()
let redBox = UIView()
view.addSubview(redBox)
redBox.backgroundColor = .red
redBox.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate(
[redBox.widthAnchor.constraint(equalToConstant: 150),
redBox.heightAnchor.constraint(equalToConstant: 100),
redBox.centerXAnchor.constraint(equalTo: view.centerXAnchor),
redBox.centerYAnchor.constraint(equalTo: view.centerYAnchor)]
)
redBox.transform = CGAffineTransform(rotationAngle: 1 / 12 * .pi )
view.addSubview(slider)
slider.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate(
[slider.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -32),
slider.widthAnchor.constraint(equalTo: view.widthAnchor)]
)
slider.addTarget(self, action: #selector(sliderChanged(_:)), for: .valueChanged)
animator.addAnimations {
redBox.rotate(by: (2 - 1 / 12) * .pi)
}
animator.pausesOnCompletion = true
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
displayLink = CADisplayLink(target: self, selector: #selector(update))
displayLink?.add(to: .main, forMode: .common)
animator.startAnimation()
}
@objc func sliderChanged(_ sender: UISlider) {
animator.fractionComplete = CGFloat(sender.value)
}
@objc func update() {
slider.value = Float(animator.fractionComplete)
if animator.fractionComplete == 1 {
animator.pauseAnimation()
displayLink?.invalidate()
displayLink = nil
}
}
}
public extension UIView {
/**
- Parameter by: Positive values for clockwise rotation, negative values for contra clockwise rotation.
Multiple full rotations are possible bei choosing angles > 2π.
*/
func rotate(by angle: CGFloat) {
let fraction: CGFloat = 3 / 4 * .pi
let times = Int((abs(angle) / fraction))
let remainder = angle.truncatingRemainder(dividingBy: fraction)
for _ in 0..<times {
transform = transform.rotated(by: angle >= 0 ? fraction : -fraction)
}
transform = transform.rotated(by: remainder)
}
}
- Statt
CGAffineTransformation(rotationAngle:)
hätte ich auchCATransform3DRotate
wählen können. -
CGAffineTransformation
ergibt bei positiven Winkeln bis einschließlich 𝞹 eine Rotation im Uhrzeigersinn, bei negativen Winkeln die größer als -𝞹 sind eine Rotation entgegen dem Uhrzeigersinn. Winkel die sich nur um einen geringen Betrag von 𝞹 oder -𝞹 unterscheiden, können in Kombination mit anderen Transformationen Rundungsfehler ergeben. Daher habe ich den Gesamtwinkel durch ¾𝞹 geteilt und nicht durch 𝞹. - Ich beobachte, dass sich der Slider beim Abspielen der Animation gleichmäßig schnell bewegt, während sich gleichzeitig die Rotation des Rechtecks durch die easeInOut-Curve beschleunigt und wieder verlangsamt. Steuer ich hingegen die Rotation durch eine gleichmäßige Bewegung des Sliders, ist auch die Rotation völlig gleichförmig. Außerdem springt der Slider vor oder zurück, nachdem ich den Slider bewegt habe und anschließend die Animation fortsetze. Es macht also einen Unterschied, ob ich
fractionComplete
lese oder setze. Beim Lesen ergibtfractionComplete
wieviel Prozent der Zeit vergangen ist, während ich beim Setzen festlege, wieviel Prozent des Zielwertes (Position, Rotationswinkel, Alpha-Wert etc.) erreicht ist.