Skip to content
This repository has been archived by the owner on Nov 21, 2017. It is now read-only.

Commit

Permalink
Implement new keyframe-based Tween API.
Browse files Browse the repository at this point in the history
Summary: Closes #13

Reviewers: O4 Material Motion Apple platform reviewers, O2 Material Motion, markwei

Reviewed By: O4 Material Motion Apple platform reviewers, O2 Material Motion, markwei

Subscribers: markwei

Tags: #material_motion

Differential Revision: http://codereview.cc/D1879
  • Loading branch information
Jeff Verkoeyen committed Nov 7, 2016
1 parent 7310e5b commit 6200e00
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 47 deletions.
56 changes: 26 additions & 30 deletions examples/PopupMenuViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,33 +87,30 @@ class PopupMenuViewController: UIViewController {
let bounceTimingFunction = CAMediaTimingFunction(controlPoints: 0.5, 1.6, 1, 1)

// Make main button smaller
let smallerBtn = Tween("transform", duration: animDuration)

let initialTransform = CATransform3DIdentity
let endTransform = CATransform3DMakeScale(0.6, 0.6, 1.0)

smallerBtn.from = NSValue(caTransform3D: buttonsShowing ? endTransform : initialTransform)
smallerBtn.to = NSValue(caTransform3D: buttonsShowing ? initialTransform : endTransform)
smallerBtn.timingFunction = bounceTimingFunction
let smallerBtn = Tween("transform",
duration: animDuration,
values: [NSValue(caTransform3D: buttonsShowing ? endTransform : initialTransform),
NSValue(caTransform3D: buttonsShowing ? initialTransform : endTransform)])
smallerBtn.timingFunctions = [bounceTimingFunction]

// Main button shadow smaller
let smallerShadow = Tween("shadowRadius", duration: animDuration)

let initialRadius = 5
let endRadius = 2

smallerShadow.from = NSNumber(value: buttonsShowing ? endRadius : initialRadius)
smallerShadow.to = NSNumber(value: buttonsShowing ? initialRadius : endRadius)
smallerShadow.timingFunction = bounceTimingFunction
let smallerShadow = Tween("shadowRadius",
duration: animDuration,
values: [NSNumber(value: buttonsShowing ? endRadius : initialRadius),
NSNumber(value: buttonsShowing ? initialRadius : endRadius)])
smallerShadow.timingFunctions = [bounceTimingFunction]

// Main button grey
let greyButton = Tween("backgroundColor", duration: animDuration)

let initialColor: AnyObject = UIColor.orange.cgColor
let endColor: AnyObject = UIColor.lightGray.cgColor

greyButton.from = buttonsShowing ? endColor : initialColor
greyButton.to = buttonsShowing ? initialColor : endColor
let initialColor = UIColor.orange.cgColor
let endColor = UIColor.lightGray.cgColor
let greyButton = Tween("backgroundColor",
duration: animDuration,
values: [buttonsShowing ? endColor : initialColor,
buttonsShowing ? initialColor : endColor])

// Move and fade buttons
let btn1Move = generateBtnMove(btn: additionalBtn1!, distance: distFromMainButton, angle: angle1, timing: bounceTimingFunction)
Expand All @@ -127,7 +124,7 @@ class PopupMenuViewController: UIViewController {

func addAndCommit(tween: Tween, to target: CALayer) {
runtime.addPlan(tween, to: target)
tween.commitToValue(to: target)
tween.commitLastValue(to: target)
}
addAndCommit(tween: smallerBtn, to: mainBtn!.layer)
addAndCommit(tween: greyButton, to: mainBtn!.layer)
Expand All @@ -143,29 +140,28 @@ class PopupMenuViewController: UIViewController {
}

func generateBtnMove(btn: CALayer, distance: CGFloat, angle: Float, timing: CAMediaTimingFunction) -> Tween {
let move = Tween("position", duration: animDuration)
let initialPos = mainBtn!.layer.position

let x = initialPos.x - CGFloat(sinf(angle)) * distance
let y = initialPos.y - CGFloat(cosf(angle)) * distance
let endPos = CGPoint(x: x, y: y)

move.from = NSValue(cgPoint: buttonsShowing ? endPos : initialPos)
move.to = NSValue(cgPoint: buttonsShowing ? initialPos : endPos)
move.timingFunction = timing
let move = Tween("position",
duration: animDuration,
values: [NSValue(cgPoint: buttonsShowing ? endPos : initialPos),
NSValue(cgPoint: buttonsShowing ? initialPos : endPos)])
move.timingFunctions = [timing]

return move
}

func generateBtnFade(btn: CALayer) -> Tween {
let fade = Tween("opacity", duration: animDuration)

let initialOpacity = NSNumber(value: 0.0)
let endOpacity = NSNumber(value: 1.0)

fade.from = buttonsShowing ? endOpacity : initialOpacity
fade.to = buttonsShowing ? initialOpacity : endOpacity

return fade
return Tween("opacity",
duration: animDuration,
values: [buttonsShowing ? endOpacity : initialOpacity,
buttonsShowing ? initialOpacity : endOpacity])
}
}
11 changes: 11 additions & 0 deletions src/Tween+Commit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,22 @@
import Foundation

extension Tween {
/** Commits the tween's first value to the given target. */
public func commitFirstValue(to target: CALayer) {
target.setValue(values.first, forKeyPath: keyPath)
}
/** Commits the tween's last value to the given target. */
public func commitLastValue(to target: CALayer) {
target.setValue(values.last, forKeyPath: keyPath)
}

/** Commits the tween's from value to the given target. */
@available(*, deprecated, message: "Use commitFirstValue instead. Deprecated in v1.2.0.")
public func commitFromValue(to target: CALayer) {
target.setValue(from, forKeyPath: keyPath)
}
/** Commits the tween's to value to the given target. */
@available(*, deprecated, message: "Use commitLastValue instead. Deprecated in v1.2.0.")
public func commitToValue(to target: CALayer) {
target.setValue(to, forKeyPath: keyPath)
}
Expand Down
67 changes: 57 additions & 10 deletions src/Tween.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,23 +28,42 @@ public final class Tween: NSObject, Plan {
/** The delay of the animation in seconds. */
public var delay: CFTimeInterval = 0

/** The initial value of the tween. See CABasicAnimation documentation for more details. */
public var from: AnyObject?
/**
An array of objects providing the value of the animation for each keyframe.

/** The final value of the tween. See CABasicAnimation documentation for more details. */
public var to: AnyObject?
If values.count == 1 then the sole value will be treated as the toValue in a basic animation.

See CAKeyframeAnimation documentation for more details.
*/
public var values: [Any]

/**
The timing function to apply to the animation.
An optional array of double values defining the pacing of the animation. Each position
corresponds to one value in the `values' array, and defines when the value should be used in the
animation function. Each value in the array is a floating point number in the range [0,1].

A nil timing function indicates linear pacing.
See CAKeyframeAnimation documentation for more details.
*/
public var timingFunction: CAMediaTimingFunction?
public var keyPositions: [Double]?

@objc(initWithKeyPath:duration:)
public init(_ keyPath: String, duration: CFTimeInterval) {
/**
An optional array of CAMediaTimingFunction objects. If the `values' array defines n keyframes,
there should be n-1 objects in the `timingFunctions' array. Each function describes the pacing of
one keyframe to keyframe segment.

If values.count == 1 then a single timing function may be provided to configure the basic
animation.

See CAKeyframeAnimation documentation for more details.
*/
public var timingFunctions: [CAMediaTimingFunction]?

/** Initializes a tween instance with its required properties. */
@objc(initWithKeyPath:duration:values:)
public init(_ keyPath: String, duration: CFTimeInterval, values: [Any]) {
self.keyPath = keyPath
self.duration = duration
self.values = values
super.init()
}

Expand All @@ -54,11 +73,39 @@ public final class Tween: NSObject, Plan {
}
/** Returns a copy of this plan. */
public func copy(with zone: NSZone? = nil) -> Any {
let tween = Tween(keyPath, duration: duration)
let tween = Tween(keyPath, duration: duration, values: values)
tween.keyPositions = keyPositions
tween.timingFunctions = timingFunctions
tween.from = from
tween.to = to
tween.timingFunction = timingFunction
tween.delay = delay
return tween
}

/** The initial value of the tween. See CABasicAnimation documentation for more details. */
@available(*, deprecated, message: "Use values instead. Deprecated in v1.2.0.")
public var from: AnyObject?

/** The final value of the tween. See CABasicAnimation documentation for more details. */
@available(*, deprecated, message: "Use values instead. Deprecated in v1.2.0.")
public var to: AnyObject?

/**
The timing function to apply to the animation.

A nil timing function indicates linear pacing.
*/
@available(*, deprecated, message: "No replacement API. Deprecated in v1.2.0.")
public var timingFunction: CAMediaTimingFunction?

/** Initializes a tween instance with its required properties. */
@available(*, deprecated, message: "Use initWithKeyPath:duration:values: instead. Deprecated in v1.2.0.")
@objc(initWithKeyPath:duration:)
public init(_ keyPath: String, duration: CFTimeInterval) {
self.keyPath = keyPath
self.duration = duration
self.values = []
super.init()
}
}
20 changes: 15 additions & 5 deletions src/private/TweenPerformer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,26 @@ class TweenPerformer: NSObject, ContinuousPerforming {
func addPlan(_ plan: Plan) {
let tween = plan as! Tween

let animation = CABasicAnimation(keyPath: tween.keyPath)
animation.fromValue = tween.from
animation.toValue = tween.to
animation.timingFunction = tween.timingFunction
let animation: CAAnimation
if tween.values.count > 1 {
let keyframeAnimation = CAKeyframeAnimation(keyPath: tween.keyPath)
keyframeAnimation.values = tween.values
keyframeAnimation.keyTimes = tween.keyPositions?.map { NSNumber(value: $0) }
keyframeAnimation.timingFunctions = tween.timingFunctions
animation = keyframeAnimation
} else {
let basicAnimation = CABasicAnimation(keyPath: tween.keyPath)
basicAnimation.toValue = tween.values.last
basicAnimation.timingFunction = tween.timingFunctions?.first
animation = basicAnimation
}
animation.duration = tween.duration
animation.beginTime = tween.delay

guard let token = tokenGenerator.generate() else { return }

CATransaction.begin()

guard let token = tokenGenerator.generate() else { return }
CATransaction.setCompletionBlock {
token.terminate()
}
Expand Down
6 changes: 5 additions & 1 deletion tests/unit/ObjectiveCAPITests.m
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,12 @@ @interface ObjectiveCAPITests : XCTestCase
@implementation ObjectiveCAPITests

- (void)testTweenAPI {
MDMTween *tween = [[MDMTween alloc] initWithKeyPath:@"opacity" duration:0.1];
MDMTween *tween = [[MDMTween alloc] initWithKeyPath:@"opacity" duration:0.1 values:@[]];
tween.delay = 0.1;
tween.values = @[];
tween.keyPositions = @[];
tween.timingFunctions = @[];

tween.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
tween.from = @2;
tween.to = @2;
Expand Down
20 changes: 19 additions & 1 deletion tests/unit/TweenTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import MaterialMotionCoreAnimationFamily

class TweenTests: XCTestCase {

func testDidPerformAndIdle() {
@available(*, deprecated)
func testDeprecatedAPIDidPerformAndIdle() {
let animation = Tween("opacity", duration: 0.1)

let view = UIView()
Expand All @@ -36,4 +37,21 @@ class TweenTests: XCTestCase {

XCTAssertEqual(runtime.activityState, .idle)
}

func testDidPerformAndIdle() {
let animation = Tween("opacity", duration: 0.1, values: [])

let view = UIView()

let runtime = Runtime()
let delegate = TestableRuntimeDelegate()
delegate.didIdleExpectation = expectation(description: "Did idle")
runtime.delegate = delegate

runtime.addPlan(animation, to: view)

waitForExpectations(timeout: 0.3)

XCTAssertEqual(runtime.activityState, .idle)
}
}

0 comments on commit 6200e00

Please sign in to comment.