Skip to content
This repository was archived by the owner on Aug 30, 2023. It is now read-only.

Commit 524b3cb

Browse files
authored
Only animate CGSize and CGPoint key paths additively if additive is enabled. (#72)
Prior to this change, CGSize and CGPoint animations were being animated additively even if the additive property was disabled. Added tests to catch regressions in the future.
1 parent 71d22e2 commit 524b3cb

File tree

4 files changed

+277
-6
lines changed

4 files changed

+277
-6
lines changed

examples/apps/Catalog/MotionAnimatorCatalog.xcodeproj/project.pbxproj

+8
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
6625876C1FB4DB9C00BC7DF1 /* InitialVelocityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6625876B1FB4DB9C00BC7DF1 /* InitialVelocityTests.swift */; };
1313
664F59941FCCE27E002EC56D /* UIKitBehavioralTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 664F59931FCCE27E002EC56D /* UIKitBehavioralTests.swift */; };
1414
664F59961FCDB2E6002EC56D /* QuartzCoreBehavioralTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 664F59951FCDB2E5002EC56D /* QuartzCoreBehavioralTests.swift */; };
15+
664F599A1FCE6661002EC56D /* NonAdditiveAnimatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 664F59991FCE6661002EC56D /* NonAdditiveAnimatorTests.swift */; };
16+
664F599C1FCE67DB002EC56D /* AdditiveAnimatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 664F599B1FCE67DB002EC56D /* AdditiveAnimatorTests.swift */; };
1517
666FAA841D384A6B000363DA /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 666FAA831D384A6B000363DA /* AppDelegate.swift */; };
1618
666FAA8B1D384A6B000363DA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 666FAA8A1D384A6B000363DA /* Assets.xcassets */; };
1719
666FAA8E1D384A6B000363DA /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 666FAA8C1D384A6B000363DA /* LaunchScreen.storyboard */; };
@@ -55,6 +57,8 @@
5557
6625876B1FB4DB9C00BC7DF1 /* InitialVelocityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InitialVelocityTests.swift; sourceTree = "<group>"; };
5658
664F59931FCCE27E002EC56D /* UIKitBehavioralTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitBehavioralTests.swift; sourceTree = "<group>"; };
5759
664F59951FCDB2E5002EC56D /* QuartzCoreBehavioralTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QuartzCoreBehavioralTests.swift; sourceTree = "<group>"; };
60+
664F59991FCE6661002EC56D /* NonAdditiveAnimatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonAdditiveAnimatorTests.swift; sourceTree = "<group>"; };
61+
664F599B1FCE67DB002EC56D /* AdditiveAnimatorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdditiveAnimatorTests.swift; sourceTree = "<group>"; };
5862
666FAA801D384A6B000363DA /* MotionAnimatorCatalog.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MotionAnimatorCatalog.app; sourceTree = BUILT_PRODUCTS_DIR; };
5963
666FAA831D384A6B000363DA /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = Catalog/AppDelegate.swift; sourceTree = "<group>"; };
6064
666FAA8A1D384A6B000363DA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
@@ -218,13 +222,15 @@
218222
66FD99F81EE9FBA000C53A82 /* unit */ = {
219223
isa = PBXGroup;
220224
children = (
225+
664F599B1FCE67DB002EC56D /* AdditiveAnimatorTests.swift */,
221226
66A6A6671FBA158000DE54CB /* AnimationRemovalTests.swift */,
222227
66EF6F271FC33C4800C83A63 /* HeadlessLayerImplicitAnimationTests.swift */,
223228
66BF5A8E1FB0E4CB00E864F6 /* ImplicitAnimationTests.swift */,
224229
6625876B1FB4DB9C00BC7DF1 /* InitialVelocityTests.swift */,
225230
66EF6F291FC48D6A00C83A63 /* InstantAnimationTests.swift */,
226231
66FD99F91EE9FBBE00C53A82 /* MotionAnimatorTests.m */,
227232
668726491EF04B4C00113675 /* MotionAnimatorTests.swift */,
233+
664F59991FCE6661002EC56D /* NonAdditiveAnimatorTests.swift */,
228234
664F59951FCDB2E5002EC56D /* QuartzCoreBehavioralTests.swift */,
229235
660636011FACC24300C3DFB8 /* TimeScaleFactorTests.swift */,
230236
664F59931FCCE27E002EC56D /* UIKitBehavioralTests.swift */,
@@ -505,11 +511,13 @@
505511
files = (
506512
6625876C1FB4DB9C00BC7DF1 /* InitialVelocityTests.swift in Sources */,
507513
66EF6F281FC33C4800C83A63 /* HeadlessLayerImplicitAnimationTests.swift in Sources */,
514+
664F599A1FCE6661002EC56D /* NonAdditiveAnimatorTests.swift in Sources */,
508515
664F59941FCCE27E002EC56D /* UIKitBehavioralTests.swift in Sources */,
509516
66EF6F2A1FC48D6A00C83A63 /* InstantAnimationTests.swift in Sources */,
510517
660636021FACC24300C3DFB8 /* TimeScaleFactorTests.swift in Sources */,
511518
66BF5A8F1FB0E4CB00E864F6 /* ImplicitAnimationTests.swift in Sources */,
512519
664F59961FCDB2E6002EC56D /* QuartzCoreBehavioralTests.swift in Sources */,
520+
664F599C1FCE67DB002EC56D /* AdditiveAnimatorTests.swift in Sources */,
513521
66A6A6681FBA158000DE54CB /* AnimationRemovalTests.swift in Sources */,
514522
6687264A1EF04B4C00113675 /* MotionAnimatorTests.swift in Sources */,
515523
66FD99FA1EE9FBBE00C53A82 /* MotionAnimatorTests.m in Sources */,

src/private/CABasicAnimation+MotionAnimator.m

+12-6
Original file line numberDiff line numberDiff line change
@@ -170,9 +170,12 @@ void MDMConfigureAnimation(CABasicAnimation *animation,
170170
CGSize from = [animation.fromValue CGSizeValue];
171171
CGSize to = [animation.toValue CGSizeValue];
172172
CGSize additiveDisplacement = CGSizeMake(from.width - to.width, from.height - to.height);
173-
animation.fromValue = [NSValue valueWithCGSize:additiveDisplacement];
174-
animation.toValue = [NSValue valueWithCGSize:CGSizeZero];
175-
animation.additive = true;
173+
174+
if (wantsAdditive) {
175+
animation.fromValue = [NSValue valueWithCGSize:additiveDisplacement];
176+
animation.toValue = [NSValue valueWithCGSize:CGSizeZero];
177+
animation.additive = true;
178+
}
176179

177180
#pragma clang diagnostic push
178181
// CASpringAnimation is a private API on iOS 8 - we're able to make use of it because we're
@@ -199,9 +202,12 @@ void MDMConfigureAnimation(CABasicAnimation *animation,
199202
CGPoint from = [animation.fromValue CGPointValue];
200203
CGPoint to = [animation.toValue CGPointValue];
201204
CGPoint additiveDisplacement = CGPointMake(from.x - to.x, from.y - to.y);
202-
animation.fromValue = [NSValue valueWithCGPoint:additiveDisplacement];
203-
animation.toValue = [NSValue valueWithCGPoint:CGPointZero];
204-
animation.additive = true;
205+
206+
if (wantsAdditive) {
207+
animation.fromValue = [NSValue valueWithCGPoint:additiveDisplacement];
208+
animation.toValue = [NSValue valueWithCGPoint:CGPointZero];
209+
animation.additive = true;
210+
}
205211

206212
#pragma clang diagnostic push
207213
// CASpringAnimation is a private API on iOS 8 - we're able to make use of it because we're
+118
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*
2+
Copyright 2017-present The Material Motion Authors. All Rights Reserved.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import XCTest
18+
#if IS_BAZEL_BUILD
19+
import _MotionAnimator
20+
#else
21+
import MotionAnimator
22+
#endif
23+
24+
class AdditiveAnimationTests: XCTestCase {
25+
var animator: MotionAnimator!
26+
var timing: MotionTiming!
27+
var view: UIView!
28+
29+
override func setUp() {
30+
super.setUp()
31+
32+
animator = MotionAnimator()
33+
34+
animator.additive = true
35+
36+
timing = MotionTiming(delay: 0,
37+
duration: 1,
38+
curve: MotionCurveMakeBezier(p1x: 0, p1y: 0, p2x: 1, p2y: 1),
39+
repetition: .init(type: .none, amount: 0, autoreverses: false))
40+
41+
let window = UIWindow()
42+
window.makeKeyAndVisible()
43+
view = UIView() // Need to animate a view's layer to get implicit animations.
44+
window.addSubview(view)
45+
46+
// Connect our layers to the render server.
47+
CATransaction.flush()
48+
}
49+
50+
override func tearDown() {
51+
animator = nil
52+
timing = nil
53+
view = nil
54+
55+
super.tearDown()
56+
}
57+
58+
func testNumericKeyPathsAnimateAdditively() {
59+
animator.animate(with: timing, to: view.layer, withValues: [1, 0], keyPath: .cornerRadius)
60+
61+
XCTAssertNotNil(view.layer.animationKeys(),
62+
"Expected an animation to be added, but none were found.")
63+
guard let animationKeys = view.layer.animationKeys() else {
64+
return
65+
}
66+
XCTAssertEqual(animationKeys.count, 1,
67+
"Expected only one animation to be added, but the following were found: "
68+
+ "\(animationKeys).")
69+
guard let key = animationKeys.first,
70+
let animation = view.layer.animation(forKey: key) as? CABasicAnimation else {
71+
return
72+
}
73+
74+
XCTAssertTrue(animation.isAdditive, "Animation is not additive when it should be.")
75+
}
76+
77+
func testCGSizeKeyPathsAnimateAdditively() {
78+
animator.animate(with: timing, to: view.layer,
79+
withValues: [CGSize(width: 0, height: 0),
80+
CGSize(width: 1, height: 2)], keyPath: .shadowOffset)
81+
82+
XCTAssertNotNil(view.layer.animationKeys(),
83+
"Expected an animation to be added, but none were found.")
84+
guard let animationKeys = view.layer.animationKeys() else {
85+
return
86+
}
87+
XCTAssertEqual(animationKeys.count, 1,
88+
"Expected only one animation to be added, but the following were found: "
89+
+ "\(animationKeys).")
90+
guard let key = animationKeys.first,
91+
let animation = view.layer.animation(forKey: key) as? CABasicAnimation else {
92+
return
93+
}
94+
95+
XCTAssertTrue(animation.isAdditive, "Animation is not additive when it should be.")
96+
}
97+
98+
func testCGPointKeyPathsAnimateAdditively() {
99+
animator.animate(with: timing, to: view.layer,
100+
withValues: [CGPoint(x: 0, y: 0),
101+
CGPoint(x: 1, y: 2)], keyPath: .position)
102+
103+
XCTAssertNotNil(view.layer.animationKeys(),
104+
"Expected an animation to be added, but none were found.")
105+
guard let animationKeys = view.layer.animationKeys() else {
106+
return
107+
}
108+
XCTAssertEqual(animationKeys.count, 1,
109+
"Expected only one animation to be added, but the following were found: "
110+
+ "\(animationKeys).")
111+
guard let key = animationKeys.first,
112+
let animation = view.layer.animation(forKey: key) as? CABasicAnimation else {
113+
return
114+
}
115+
116+
XCTAssertTrue(animation.isAdditive, "Animation is not additive when it should be.")
117+
}
118+
}
+139
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/*
2+
Copyright 2017-present The Material Motion Authors. All Rights Reserved.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import XCTest
18+
#if IS_BAZEL_BUILD
19+
import _MotionAnimator
20+
#else
21+
import MotionAnimator
22+
#endif
23+
24+
class NonAdditiveAnimationTests: XCTestCase {
25+
var animator: MotionAnimator!
26+
var timing: MotionTiming!
27+
var view: UIView!
28+
29+
override func setUp() {
30+
super.setUp()
31+
32+
animator = MotionAnimator()
33+
34+
animator.additive = false
35+
36+
timing = MotionTiming(delay: 0,
37+
duration: 1,
38+
curve: MotionCurveMakeBezier(p1x: 0, p1y: 0, p2x: 0, p2y: 0),
39+
repetition: .init(type: .none, amount: 0, autoreverses: false))
40+
41+
let window = UIWindow()
42+
window.makeKeyAndVisible()
43+
view = UIView() // Need to animate a view's layer to get implicit animations.
44+
window.addSubview(view)
45+
46+
// Connect our layers to the render server.
47+
CATransaction.flush()
48+
}
49+
50+
override func tearDown() {
51+
animator = nil
52+
timing = nil
53+
view = nil
54+
55+
super.tearDown()
56+
}
57+
58+
func testNumericKeyPathsDontAnimateAdditively() {
59+
animator.animate(with: timing, to: view.layer, withValues: [1, 0], keyPath: .cornerRadius)
60+
61+
XCTAssertNotNil(view.layer.animationKeys(),
62+
"Expected an animation to be added, but none were found.")
63+
guard let animationKeys = view.layer.animationKeys() else {
64+
return
65+
}
66+
XCTAssertEqual(animationKeys.count, 1,
67+
"Expected only one animation to be added, but the following were found: "
68+
+ "\(animationKeys).")
69+
guard let key = animationKeys.first,
70+
let animation = view.layer.animation(forKey: key) as? CABasicAnimation else {
71+
return
72+
}
73+
74+
XCTAssertFalse(animation.isAdditive, "Animation is additive when it shouldn't be.")
75+
}
76+
77+
func testSizeKeyPathsDontAnimateAdditively() {
78+
animator.animate(with: timing, to: view.layer,
79+
withValues: [CGSize(width: 0, height: 0),
80+
CGSize(width: 1, height: 2)], keyPath: .shadowOffset)
81+
82+
XCTAssertNotNil(view.layer.animationKeys(),
83+
"Expected an animation to be added, but none were found.")
84+
guard let animationKeys = view.layer.animationKeys() else {
85+
return
86+
}
87+
XCTAssertEqual(animationKeys.count, 1,
88+
"Expected only one animation to be added, but the following were found: "
89+
+ "\(animationKeys).")
90+
guard let key = animationKeys.first,
91+
let animation = view.layer.animation(forKey: key) as? CABasicAnimation else {
92+
return
93+
}
94+
95+
XCTAssertFalse(animation.isAdditive, "Animation is additive when it shouldn't be.")
96+
}
97+
98+
func testPositionKeyPathsDontAnimateAdditively() {
99+
animator.animate(with: timing, to: view.layer,
100+
withValues: [CGPoint(x: 0, y: 0),
101+
CGPoint(x: 1, y: 2)], keyPath: .position)
102+
103+
XCTAssertNotNil(view.layer.animationKeys(),
104+
"Expected an animation to be added, but none were found.")
105+
guard let animationKeys = view.layer.animationKeys() else {
106+
return
107+
}
108+
XCTAssertEqual(animationKeys.count, 1,
109+
"Expected only one animation to be added, but the following were found: "
110+
+ "\(animationKeys).")
111+
guard let key = animationKeys.first,
112+
let animation = view.layer.animation(forKey: key) as? CABasicAnimation else {
113+
return
114+
}
115+
116+
XCTAssertFalse(animation.isAdditive, "Animation is additive when it shouldn't be.")
117+
}
118+
119+
func testRectKeyPathsDontAnimateAdditively() {
120+
animator.animate(with: timing, to: view.layer,
121+
withValues: [CGRect(x: 0, y: 0, width: 0, height: 0),
122+
CGRect(x: 0, y: 0, width: 100, height: 50)], keyPath: .bounds)
123+
124+
XCTAssertNotNil(view.layer.animationKeys(),
125+
"Expected an animation to be added, but none were found.")
126+
guard let animationKeys = view.layer.animationKeys() else {
127+
return
128+
}
129+
XCTAssertEqual(animationKeys.count, 1,
130+
"Expected only one animation to be added, but the following were found: "
131+
+ "\(animationKeys).")
132+
guard let key = animationKeys.first,
133+
let animation = view.layer.animation(forKey: key) as? CABasicAnimation else {
134+
return
135+
}
136+
137+
XCTAssertFalse(animation.isAdditive, "Animation is additive when it shouldn't be.")
138+
}
139+
}

0 commit comments

Comments
 (0)