-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for interactive animations
- Loading branch information
Showing
6 changed files
with
463 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
172 changes: 172 additions & 0 deletions
172
Example/Stagehand/InteractiveAnimationsViewController.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
// | ||
// Copyright 2020 Square Inc. | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
// | ||
|
||
import Stagehand | ||
import UIKit | ||
|
||
final class InteractiveAnimationViewController: DemoViewController { | ||
|
||
// MARK: - Life Cycle | ||
|
||
override init() { | ||
super.init() | ||
|
||
contentView = mainView | ||
|
||
animationRows = [ | ||
("Animate to Beginning (Linear)", { [unowned self] in | ||
self.animationInstance?.animateToBeginning() | ||
}), | ||
("Animate to End (Linear)", { [unowned self] in | ||
self.animationInstance?.animateToEnd() | ||
}), | ||
("Cancel (Halt)", { [unowned self] in | ||
self.animationInstance?.cancel() | ||
}), | ||
("Use Linear Curve", { [unowned self] in | ||
self.animationCurve = LinearAnimationCurve() | ||
self.animationInstance?.cancel() | ||
self.animationInstance = self.makeAnimation().performInteractive(on: self.mainView.animatableView) | ||
}), | ||
("Use Easing Curve", { [unowned self] in | ||
self.animationCurve = CubicBezierAnimationCurve.easeInEaseOut | ||
self.animationInstance?.cancel() | ||
self.animationInstance = self.makeAnimation().performInteractive(on: self.mainView.animatableView) | ||
}), | ||
] | ||
|
||
mainView.progressSlider.addTarget(self, action: #selector(progressUpdateDidBegin), for: .touchDown) | ||
mainView.progressSlider.addTarget(self, action: #selector(progressValueChanged), for: .valueChanged) | ||
mainView.progressSlider.addTarget(self, action: #selector(progressUpdateDidEnd), for: .touchUpInside) | ||
mainView.progressSlider.addTarget(self, action: #selector(progressUpdateDidEnd), for: .touchUpOutside) | ||
mainView.progressSlider.addTarget(self, action: #selector(progressUpdateDidEnd), for: .touchCancel) | ||
} | ||
|
||
// MARK: - Private Properties | ||
|
||
private var mainView: View = .init() | ||
|
||
private var animationInstance: InteractiveAnimationInstance? | ||
|
||
private var animationCurve: AnimationCurve = LinearAnimationCurve() | ||
|
||
// MARK: - Private Methods | ||
|
||
private func makeAnimation() -> Animation<UIView> { | ||
var animation = Animation<UIView>() | ||
animation.addKeyframe( | ||
for: \.transform, | ||
at: 0, | ||
value: .identity | ||
) | ||
animation.addKeyframe( | ||
for: \.transform, | ||
at: 0.5, | ||
value: CGAffineTransform.identity | ||
.translatedBy(x: (mainView.bounds.width - 100) / 2, y: 0) | ||
.rotated(by: .pi / 4) | ||
) | ||
animation.addKeyframe( | ||
for: \.transform, | ||
at: 1, | ||
value: CGAffineTransform.identity | ||
.translatedBy(x: mainView.bounds.width - 100, y: 0) | ||
.rotated(by: .pi / 2) | ||
) | ||
animation.duration = 2.5 | ||
animation.curve = animationCurve | ||
|
||
animation.addPerFrameExecution { context in | ||
self.mainView.progressSlider.value = Float(context.uncurvedProgress) | ||
} | ||
|
||
return animation | ||
} | ||
|
||
@objc private func progressUpdateDidBegin() { | ||
guard animationInstance == nil else { | ||
return | ||
} | ||
|
||
self.animationInstance = makeAnimation().performInteractive(on: mainView.animatableView) | ||
} | ||
|
||
@objc private func progressValueChanged() { | ||
let slider = mainView.progressSlider | ||
let progress = Double((slider.value - slider.minimumValue) / (slider.maximumValue - slider.minimumValue)) | ||
animationInstance?.updateProgress(to: progress) | ||
} | ||
|
||
@objc private func progressUpdateDidEnd() { | ||
let slider = mainView.progressSlider | ||
let progress = Double((slider.value - slider.minimumValue) / (slider.maximumValue - slider.minimumValue)) | ||
|
||
if progress < 0.5 { | ||
animationInstance?.animateToBeginning(using: CubicBezierAnimationCurve.easeInEaseOut) | ||
} else { | ||
animationInstance?.animateToEnd(using: CubicBezierAnimationCurve.easeInEaseOut) | ||
} | ||
} | ||
|
||
} | ||
|
||
// MARK: - | ||
|
||
extension InteractiveAnimationViewController { | ||
|
||
final class View: UIView { | ||
|
||
// MARK: - Life Cycle | ||
|
||
override init(frame: CGRect) { | ||
super.init(frame: frame) | ||
|
||
animatableView.backgroundColor = .red | ||
addSubview(animatableView) | ||
|
||
addSubview(progressSlider) | ||
} | ||
|
||
@available(*, unavailable) | ||
required init?(coder: NSCoder) { | ||
fatalError("init(coder:) has not been implemented") | ||
} | ||
|
||
// MARK: - Public Properties | ||
|
||
var animatableView: UIView = .init() | ||
|
||
let progressSlider: UISlider = .init() | ||
|
||
// MARK: - UIView | ||
|
||
override func layoutSubviews() { | ||
animatableView.bounds.size = .init(width: 50, height: 50) | ||
animatableView.center = .init( | ||
x: bounds.minX + 50, | ||
y: bounds.minY + 60 | ||
) | ||
|
||
progressSlider.bounds.size = progressSlider.sizeThatFits(bounds.insetBy(dx: 24, dy: 0).size) | ||
progressSlider.frame.origin = .init( | ||
x: 24, | ||
y: animatableView.bounds.maxY + 60 | ||
) | ||
} | ||
|
||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
84 changes: 84 additions & 0 deletions
84
Sources/Stagehand/AnimationInstance/InteractiveAnimationInstance.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
// | ||
// Copyright 2020 Square Inc. | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
// | ||
|
||
import Foundation | ||
|
||
extension Animation { | ||
|
||
/// Begins an interactive animation on the given `element`. | ||
/// | ||
/// The animation will not be applied to the `element` until an action is taken with the returned | ||
/// `InteractiveAnimationInstance`, either by setting the progress directly or starting progression to the beggining | ||
/// or end of the animation. | ||
/// | ||
/// - parameter element: The element to be animated. | ||
public func performInteractive( | ||
on element: ElementType | ||
) -> InteractiveAnimationInstance { | ||
return InteractiveAnimationInstance( | ||
animation: self, | ||
element: element | ||
) | ||
} | ||
|
||
} | ||
|
||
// MARK: - | ||
|
||
public final class InteractiveAnimationInstance: AnimationInstance { | ||
|
||
// MARK: - Life Cycle | ||
|
||
internal init<ElementType: AnyObject>( | ||
animation: Animation<ElementType>, | ||
element: ElementType | ||
) { | ||
let driver = InteractiveDriver(duration: animation.duration) | ||
|
||
self.interactiveDriver = driver | ||
|
||
super.init(animation: animation, element: element, driver: driver) | ||
} | ||
|
||
// MARK: - Private Properties | ||
|
||
private let interactiveDriver: InteractiveDriver | ||
|
||
// MARK: - Public Methods | ||
|
||
/// Updates the progress of the animation to the `relativeTimestamp`. | ||
/// | ||
/// If the animation is currently running on its own (from calling either `animateToBeginning(using:)` or | ||
/// `animateToEnd(using:)`), the animation will be paused. | ||
public func updateProgress(to relativeTimestamp: Double) { | ||
interactiveDriver.updateProgress(to: relativeTimestamp) | ||
} | ||
|
||
/// Animate in reverse to the beginning of the animation. | ||
/// | ||
/// The `curve` is applied on top of the animation's curve. | ||
public func animateToBeginning(using curve: AnimationCurve = LinearAnimationCurve()) { | ||
interactiveDriver.animateToBeginning(using: curve) | ||
} | ||
|
||
/// Animate forward to the end of the animation. | ||
/// | ||
/// The `curve` is applied on top of the animation's curve. | ||
public func animateToEnd(using curve: AnimationCurve = LinearAnimationCurve()) { | ||
interactiveDriver.animateToEnd(using: curve) | ||
} | ||
|
||
} |
Oops, something went wrong.