Skip to content

Commit

Permalink
Add support for interactive animations
Browse files Browse the repository at this point in the history
  • Loading branch information
NickEntin committed Jul 21, 2020
1 parent c911283 commit b1f29c9
Show file tree
Hide file tree
Showing 6 changed files with 463 additions and 1 deletion.
4 changes: 4 additions & 0 deletions Example/Stagehand.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
3DD79B712488D51600954004 /* SnapshotTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DD79B702488D51600954004 /* SnapshotTestCase.swift */; };
3DD79B732488D6E600954004 /* CGAffineTransformInterpolationSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DD79B722488D6E600954004 /* CGAffineTransformInterpolationSnapshotTests.swift */; };
3DEA0C9624837CAB00F1ECE7 /* TransformPerformanceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DEA0C9524837CAB00F1ECE7 /* TransformPerformanceTests.swift */; };
3DDE7FF024C5123800999ABA /* InteractiveAnimationsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DDE7FEF24C5123800999ABA /* InteractiveAnimationsViewController.swift */; };
3DEE440A231331DD0057D796 /* AnimationGroupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DEE4409231331DD0057D796 /* AnimationGroupViewController.swift */; };
607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD51AFB9204008FA782 /* AppDelegate.swift */; };
607FACD81AFB9204008FA782 /* RootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD71AFB9204008FA782 /* RootViewController.swift */; };
Expand Down Expand Up @@ -102,6 +103,7 @@
3DD79B702488D51600954004 /* SnapshotTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnapshotTestCase.swift; sourceTree = "<group>"; };
3DD79B722488D6E600954004 /* CGAffineTransformInterpolationSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGAffineTransformInterpolationSnapshotTests.swift; sourceTree = "<group>"; };
3DEA0C9524837CAB00F1ECE7 /* TransformPerformanceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransformPerformanceTests.swift; sourceTree = "<group>"; };
3DDE7FEF24C5123800999ABA /* InteractiveAnimationsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractiveAnimationsViewController.swift; sourceTree = "<group>"; };
3DEE4409231331DD0057D796 /* AnimationGroupViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimationGroupViewController.swift; sourceTree = "<group>"; };
3EEB62F0CC3F471BC6454D87 /* Pods-Stagehand_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Stagehand_Example.debug.xcconfig"; path = "Target Support Files/Pods-Stagehand_Example/Pods-Stagehand_Example.debug.xcconfig"; sourceTree = "<group>"; };
472E62FC731AD89405017C59 /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = "<group>"; };
Expand Down Expand Up @@ -194,6 +196,7 @@
3D875EFF2323542500670803 /* PerformanceBenchmarkViewController.swift */,
3D2712ED236A17C5001D3B4B /* AnimationQueueViewController.swift */,
3D2C05902489F807003F5A4B /* CGAffineTransformDebuggingViewController.swift */,
3DDE7FEF24C5123800999ABA /* InteractiveAnimationsViewController.swift */,
);
name = "Demo View Controllers";
sourceTree = "<group>";
Expand Down Expand Up @@ -593,6 +596,7 @@
3DEE440A231331DD0057D796 /* AnimationGroupViewController.swift in Sources */,
3D1D304A22F007B7003E392C /* ChildAnimationsViewController.swift in Sources */,
607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */,
3DDE7FF024C5123800999ABA /* InteractiveAnimationsViewController.swift in Sources */,
3D8E3D8422F2BC9300D70FCB /* RepeatingAnimationsViewController.swift in Sources */,
3D8E3D8022F16C1B00D70FCB /* ChildAnimationsWithCurvesViewController.swift in Sources */,
3DB3927D23249D680009E8B3 /* ColorAnimationsViewController.swift in Sources */,
Expand Down
172 changes: 172 additions & 0 deletions Example/Stagehand/InteractiveAnimationsViewController.swift
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
)
}

}

}
1 change: 1 addition & 0 deletions Example/Stagehand/RootViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ final class RootViewController: UITableViewController {
("Collection Keyframes", { CollectionKeyframesViewController() }),
("Performance Benchmark", { PerformanceBenchmarkViewController() }),
("Animation Queues", { AnimationQueueViewController() }),
("Interactive Animations", { InteractiveAnimationViewController() }),
("CGAffineTransform Debugging", { CGAffineTransformDebuggingViewController() }),
]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import Foundation
/// Do not create an `AnimationInstance` directly. Instead, construct an `Animation`, then call the animation's
/// `perform(on:delay:completion:)` method to begin the animation. That method will return an instance of this class.
/// The `AnimationInstance` can then be used to track the `status` of the animation, or to cancel it.
public final class AnimationInstance {
public class AnimationInstance {

// MARK: - Life Cycle

Expand Down
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)
}

}
Loading

0 comments on commit b1f29c9

Please sign in to comment.