From cf183c788fba2fef2e144674d412da67d5adc438 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Thu, 14 Dec 2017 10:32:34 -0500 Subject: [PATCH 01/22] Add banner and drop most of the preamble docs in preparation for the new readme. --- README.md | 86 +-------------------------------- img/motion-animator-banner.gif | Bin 0 -> 16804 bytes 2 files changed, 2 insertions(+), 84 deletions(-) create mode 100644 img/motion-animator-banner.gif diff --git a/README.md b/README.md index 2c1c480..096bc59 100644 --- a/README.md +++ b/README.md @@ -1,95 +1,13 @@ -# Motion Animator +![Motion Interchange Banner](img/motion-interchange-banner.gif) -> A Motion Animator creates performant, interruptible animations from motion specs. +> An iOS animator that combines the best aspects of the UIView and CALayer animation APIs. [![Build Status](https://travis-ci.org/material-motion/motion-animator-objc.svg?branch=develop)](https://travis-ci.org/material-motion/motion-animator-objc) [![codecov](https://codecov.io/gh/material-motion/motion-animator-objc/branch/develop/graph/badge.svg)](https://codecov.io/gh/material-motion/motion-animator-objc) [![CocoaPods Compatible](https://img.shields.io/cocoapods/v/MotionAnimator.svg)](https://cocoapods.org/pods/MotionAnimator) [![Platform](https://img.shields.io/cocoapods/p/MotionAnimator.svg)](http://cocoadocs.org/docsets/MotionAnimator) ---- -This library provides APIs that turn [Motion Interchange](https://github.com/material-motion/motion-interchange-objc) -**motion specifications** into animations. - ---- - -#### What is a motion specification? - -A **motion specification** defines the delay, duration, and acceleration of animations in a simple -data format that can live separate from your animation logic. - -For example, let's say we wanted to describe the motion for this animation: - - - -We might create a specification like so: - -```objc -struct CalendarChipTiming { - MDMMotionTiming chipWidth; - MDMMotionTiming chipHeight; - MDMMotionTiming chipY; - - MDMMotionTiming chipContentOpacity; - MDMMotionTiming headerContentOpacity; - - MDMMotionTiming navigationBarY; -}; -typedef struct CalendarChipTiming CalendarChipTiming; - -struct CalendarChipMotionSpec { - CalendarChipTiming expansion; - CalendarChipTiming collapse; -}; -typedef struct CalendarChipMotionSpec CalendarChipMotionSpec; - -FOUNDATION_EXTERN struct CalendarChipMotionSpec CalendarChipSpec; -``` - -With our implementation of the spec looking like so: - -```objc -struct CalendarChipMotionSpec CalendarChipSpec = { - .expansion = { - .chipWidth = { - .delay = 0.000, .duration = 0.285, .curve = MDMEightyForty, - }, - .chipHeight = { - .delay = 0.015, .duration = 0.360, .curve = MDMEightyForty, - }, - ... - }, - .collapse = { - .chipWidth = { - .delay = 0.045, .duration = 0.330, .curve = MDMEightyForty, - }, - .chipHeight = { - .delay = 0.000, .duration = 0.330, .curve = MDMEightyForty, - }, - ... - }, -}; -``` - -Our spec defines two different transition states: expansion and collapse. At runtime, we determine -which of these two specs we need and then use the timings to animate our views with an instance of -`MDMMotionAnimator`: - -```objc -CalendarChipTiming timing = _expanded ? CalendarChipSpec.expansion : CalendarChipSpec.collapse; - -MDMMotionAnimator *animator = [[MDMMotionAnimator alloc] init]; -animator.shouldReverseValues = !_expanded; - -[animator animateWithTiming:timing.chipHeight - toLayer:chipView.layer - withValues:@[ @(chipFrame.size.height), @(headerFrame.size.height) ] - keyPath:MDMKeyPathHeight]; -... -``` - -A working implementation of this example can be seen in the included example app. ## Example apps/unit tests diff --git a/img/motion-animator-banner.gif b/img/motion-animator-banner.gif new file mode 100644 index 0000000000000000000000000000000000000000..5728afa2bcb12932218fc6782e64388fd7d1f9ce GIT binary patch literal 16804 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 Cbk~sp literal 0 HcmV?d00001 From 966ae6769288e4913219c7de156b846af5789d96 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Thu, 14 Dec 2017 10:33:29 -0500 Subject: [PATCH 02/22] Fix the banner url. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 096bc59..cf861dd 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![Motion Interchange Banner](img/motion-interchange-banner.gif) +![Motion Animator Banner](img/motion-animator-banner.gif) > An iOS animator that combines the best aspects of the UIView and CALayer animation APIs. From 0ca2e7c7e154da62fac508d6f8c21f1ff3b37c02 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Thu, 14 Dec 2017 10:34:30 -0500 Subject: [PATCH 03/22] Min SDK support. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cf861dd..18705fd 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ![Motion Animator Banner](img/motion-animator-banner.gif) -> An iOS animator that combines the best aspects of the UIView and CALayer animation APIs. +> An iOS 8+-compatible animator that combines the best aspects of modern UIView and CALayer animation APIs. [![Build Status](https://travis-ci.org/material-motion/motion-animator-objc.svg?branch=develop)](https://travis-ci.org/material-motion/motion-animator-objc) [![codecov](https://codecov.io/gh/material-motion/motion-animator-objc/branch/develop/graph/badge.svg)](https://codecov.io/gh/material-motion/motion-animator-objc) From 4bf33e213af26511e0aca40f42ebceb0a077076a Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Thu, 14 Dec 2017 10:35:54 -0500 Subject: [PATCH 04/22] Wording order. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 18705fd..4f3224d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ![Motion Animator Banner](img/motion-animator-banner.gif) -> An iOS 8+-compatible animator that combines the best aspects of modern UIView and CALayer animation APIs. +> An animator for iOS 8+ that combines the best aspects of modern UIView and CALayer animation APIs. [![Build Status](https://travis-ci.org/material-motion/motion-animator-objc.svg?branch=develop)](https://travis-ci.org/material-motion/motion-animator-objc) [![codecov](https://codecov.io/gh/material-motion/motion-animator-objc/branch/develop/graph/badge.svg)](https://codecov.io/gh/material-motion/motion-animator-objc) From 69469aedb987e516ff1f43a123b3ee29dfef38ca Mon Sep 17 00:00:00 2001 From: featherless Date: Thu, 14 Dec 2017 13:32:47 -0500 Subject: [PATCH 05/22] Add UIKit equivalent APIs for animating implicitly. (#90) This will enable easier migrations from existing UIKit code paths while allowing clients to benefit from the increased support for implicitly animatable properties. --- .../project.pbxproj | 4 + src/MDMMotionAnimator.h | 166 +++++++++++++++++- src/MDMMotionAnimator.m | 52 ++++++ .../unit/MotionAnimatorBehavioralTests.swift | 17 ++ tests/unit/UIKitBehavioralTests.swift | 9 +- tests/unit/UIKitEquivalencyTests.swift | 124 +++++++++++++ 6 files changed, 359 insertions(+), 13 deletions(-) create mode 100644 tests/unit/UIKitEquivalencyTests.swift diff --git a/examples/apps/Catalog/MotionAnimatorCatalog.xcodeproj/project.pbxproj b/examples/apps/Catalog/MotionAnimatorCatalog.xcodeproj/project.pbxproj index f32c841..7b2557b 100644 --- a/examples/apps/Catalog/MotionAnimatorCatalog.xcodeproj/project.pbxproj +++ b/examples/apps/Catalog/MotionAnimatorCatalog.xcodeproj/project.pbxproj @@ -24,6 +24,7 @@ 667A3F4C1DEE269400CB3A99 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 667A3F4A1DEE269400CB3A99 /* LaunchScreen.storyboard */; }; 667A3F541DEE273000CB3A99 /* TableOfContents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 667A3F531DEE273000CB3A99 /* TableOfContents.swift */; }; 6687264A1EF04B4C00113675 /* MotionAnimatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 668726491EF04B4C00113675 /* MotionAnimatorTests.swift */; }; + 668819FA1FE2EB36003A9420 /* UIKitEquivalencyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 668819F91FE2EB36003A9420 /* UIKitEquivalencyTests.swift */; }; 669B6CA91FD0547100B80B76 /* MotionAnimatorBehavioralTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669B6CA81FD0547100B80B76 /* MotionAnimatorBehavioralTests.swift */; }; 66A6A6681FBA158000DE54CB /* AnimationRemovalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66A6A6671FBA158000DE54CB /* AnimationRemovalTests.swift */; }; 66BF5A8F1FB0E4CB00E864F6 /* ImplicitAnimationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66BF5A8E1FB0E4CB00E864F6 /* ImplicitAnimationTests.swift */; }; @@ -78,6 +79,7 @@ 667A3F4D1DEE269400CB3A99 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 667A3F531DEE273000CB3A99 /* TableOfContents.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableOfContents.swift; sourceTree = ""; }; 668726491EF04B4C00113675 /* MotionAnimatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MotionAnimatorTests.swift; sourceTree = ""; }; + 668819F91FE2EB36003A9420 /* UIKitEquivalencyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIKitEquivalencyTests.swift; sourceTree = ""; }; 669B6CA81FD0547100B80B76 /* MotionAnimatorBehavioralTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MotionAnimatorBehavioralTests.swift; sourceTree = ""; }; 66A6A6671FBA158000DE54CB /* AnimationRemovalTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimationRemovalTests.swift; sourceTree = ""; }; 66BF5A8E1FB0E4CB00E864F6 /* ImplicitAnimationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImplicitAnimationTests.swift; sourceTree = ""; }; @@ -243,6 +245,7 @@ 664F59951FCDB2E5002EC56D /* QuartzCoreBehavioralTests.swift */, 660636011FACC24300C3DFB8 /* TimeScaleFactorTests.swift */, 664F59931FCCE27E002EC56D /* UIKitBehavioralTests.swift */, + 668819F91FE2EB36003A9420 /* UIKitEquivalencyTests.swift */, ); path = unit; sourceTree = ""; @@ -533,6 +536,7 @@ 664F59981FCE5CE2002EC56D /* BeginFromCurrentStateTests.swift in Sources */, 66FD99FA1EE9FBBE00C53A82 /* MotionAnimatorTests.m in Sources */, 669B6CA91FD0547100B80B76 /* MotionAnimatorBehavioralTests.swift in Sources */, + 668819FA1FE2EB36003A9420 /* UIKitEquivalencyTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/src/MDMMotionAnimator.h b/src/MDMMotionAnimator.h index 630577c..36f2e05 100644 --- a/src/MDMMotionAnimator.h +++ b/src/MDMMotionAnimator.h @@ -99,13 +99,13 @@ NS_SWIFT_NAME(MotionAnimator) @param completion A block object to be executed when the animation ends or is removed from the animation hierarchy. If the duration of the animation is 0, this block is executed immediately. The block is escaping and will be released once the animations have completed. The provided - didComplete argument is currently always YES. + `finished` argument is currently always YES. */ - (void)animateWithTraits:(nonnull MDMAnimationTraits *)traits between:(nonnull NSArray *)values layer:(nonnull CALayer *)layer keyPath:(nonnull MDMAnimatableKeyPath)keyPath - completion:(nullable void(^)(BOOL didComplete))completion; + completion:(nullable void(^)(BOOL finished))completion; /** If enabled, explicitly-provided values will be reversed before animating. @@ -139,12 +139,12 @@ NS_SWIFT_NAME(MotionAnimator) non-escaping. @param completion A block object to be executed once the animation sequence ends or it has been removed from the animation hierarchy. If the duration of the animation is 0, this block is executed - immediately. The block is escaping and will be released once the animation sequence has completed. The provided - didComplete argument is currently always YES. + immediately. The block is escaping and will be released once the animation sequence has completed. + The provided `finished` argument is currently always YES. */ - (void)animateWithTraits:(nonnull MDMAnimationTraits *)traits animations:(nonnull void (^)(void))animations - completion:(nullable void(^)(BOOL didComplete))completion; + completion:(nullable void(^)(BOOL finished))completion; #pragma mark - Managing active animations @@ -167,6 +167,162 @@ NS_SWIFT_NAME(MotionAnimator) @end +@interface MDMMotionAnimator (UIKitEquivalency) + +/** + Similar to the UIKit method of the same name with some intentional differences in behavior. + + This API does not disable user interaction during animations, unlike the default behavior of + UIView's similar API. + + Like the UIKit API, this method performs the specified animations immediately using the + UIViewAnimationOptionCurveEaseInOut animation option. + + @param duration From UIKit's documentation: "The total duration of the animations, measured in + seconds. If you specify a negative value or 0, the changes are made without + animating them." + + @param animations From UIKit's documentation: "A block object containing the changes to commit to + the views. This is where you programmatically change any animatable properties of + the views in your view hierarchy. This block takes no parameters and has no + return value. This parameter must not be NULL." + Supports animating additional CALayer properties beyond what UIView's similar API + supports. See MDMAnimatableKeyPaths for a full list of implicilty animatable + CALayer properties. + */ ++ (void)animateWithDuration:(NSTimeInterval)duration + animations:(void (^ __nonnull)(void))animations; + +/** + Similar to the UIKit method of the same name with some intentional differences in behavior. + + This API does not disable user interaction during animations, unlike the default behavior of + UIView's similar API. + + Like the UIKit API, this method performs the specified animations immediately using the + UIViewAnimationOptionCurveEaseInOut animation option. + + @param duration From UIKit's documentation: "The total duration of the animations, measured in + seconds. If you specify a negative value or 0, the changes are made without + animating them." + + @param animations From UIKit's documentation: "A block object containing the changes to commit to + the views. This is where you programmatically change any animatable properties of + the views in your view hierarchy. This block takes no parameters and has no + return value. This parameter must not be NULL." + Supports animating additional CALayer properties beyond what UIView's similar API + supports. See MDMAnimatableKeyPaths for a full list of implicilty animatable + CALayer properties. + + @param completion From UIKit's documentation: "A block object to be executed when the animation + sequence ends. This block has no return value and takes a single Boolean argument + that indicates whether or not the animations actually finished before the + completion handler was called." + Unlike UIKit's API, if the duration of the animation is 0, this block is + performed immediately. This parameter may be NULL. + */ ++ (void)animateWithDuration:(NSTimeInterval)duration + animations:(void (^ __nonnull)(void))animations + completion:(void (^ __nullable)(BOOL finished))completion; + +/** + Similar to the UIKit method of the same name with some intentional differences in behavior. + + This API does not disable user interaction during animations, unlike the default behavior of + UIView's similar API. + + Like the UIKit API, this method performs the specified animations immediately. + + The only options that are presently supported are the UIViewAnimationOptionCurve flags. + + @param duration From UIKit's documentation: "The total duration of the animations, measured in + seconds. If you specify a negative value or 0, the changes are made without + animating them." + + @param delay From UIKit's documentation: "The amount of time (measured in seconds) to wait + before beginning the animations. Specify a value of 0 to begin the animations + immediately." + + @param options From UIKit's documentation: "A mask of options indicating how you want to perform + the animations. For a list of valid constants, see UIViewAnimationOptions." Only + the UIViewAnimationOptionCurve flags are supported presently. + + @param animations From UIKit's documentation: "A block object containing the changes to commit to + the views. This is where you programmatically change any animatable properties of + the views in your view hierarchy. This block takes no parameters and has no + return value. This parameter must not be NULL." + Supports animating additional CALayer properties beyond what UIView's similar API + supports. See MDMAnimatableKeyPaths for a full list of implicilty animatable + CALayer properties. + + @param completion From UIKit's documentation: "A block object to be executed when the animation + sequence ends. This block has no return value and takes a single Boolean argument + that indicates whether or not the animations actually finished before the + completion handler was called." + Unlike UIKit's API, if the duration of the animation is 0, this block is + performed immediately. This parameter may be NULL. + */ ++ (void)animateWithDuration:(NSTimeInterval)duration + delay:(NSTimeInterval)delay + options:(UIViewAnimationOptions)options + animations:(void (^ __nonnull)(void))animations + completion:(void (^ __nullable)(BOOL finished))completion; + +/** + Similar to the UIKit method of the same name with some intentional differences in behavior. + + This API does not disable user interaction during animations, unlike the default behavior of + UIView's similar API. + + Like the UIKit API, this method performs the specified animations immediately. + + @param duration From UIKit's documentation: "The total duration of the animations, measured in + seconds. If you specify a negative value or 0, the changes are made without + animating them." + + @param delay From UIKit's documentation: "The amount of time (measured in seconds) to wait + before beginning the animations. Specify a value of 0 to begin the animations + immediately." + + @param options Ignored. + + @param dampingRatio From UIKit's documentation: "The damping ratio for the spring animation as it + approaches its quiescent state. To smoothly decelerate the animation without + oscillation, use a value of 1. Employ a damping ratio closer to zero to + increase oscillation." + + @param velocity From UIKit's documentation: "The initial spring velocity. For smooth start to + the animation, match this value to the view’s velocity as it was prior to + attachment. A value of 1 corresponds to the total animation distance traversed + in one second. For example, if the total animation distance is 200 points and + you want the start of the animation to match a view velocity of 100 pt/s, use a + value of 0.5." + + @param animations From UIKit's documentation: "A block object containing the changes to commit to + the views. This is where you programmatically change any animatable properties + of the views in your view hierarchy. This block takes no parameters and has no + return value. This parameter must not be NULL." + Supports animating additional CALayer properties beyond what UIView's similar + API supports. See MDMAnimatableKeyPaths for a full list of implicilty + animatable CALayer properties. + + @param completion From UIKit's documentation: "A block object to be executed when the animation + sequence ends. This block has no return value and takes a single Boolean + argument that indicates whether or not the animations actually finished before + the completion handler was called." + Unlike UIKit's API, if the duration of the animation is 0, this block is + performed immediately. This parameter may be NULL. + */ ++ (void)animateWithDuration:(NSTimeInterval)duration + delay:(NSTimeInterval)delay + usingSpringWithDamping:(CGFloat)dampingRatio + initialSpringVelocity:(CGFloat)velocity + options:(UIViewAnimationOptions)options + animations:(void (^ __nonnull)(void))animations + completion:(void (^ __nullable)(BOOL finished))completion; + +@end + @interface MDMMotionAnimator (Legacy) /** diff --git a/src/MDMMotionAnimator.m b/src/MDMMotionAnimator.m index 310f690..7c139c8 100644 --- a/src/MDMMotionAnimator.m +++ b/src/MDMMotionAnimator.m @@ -197,6 +197,58 @@ - (void)stopAllAnimations { [_registrar removeAllAnimations]; } +#pragma mark - UIKit equivalency + ++ (void)animateWithDuration:(NSTimeInterval)duration + animations:(void (^ __nonnull)(void))animations { + [self animateWithDuration:duration animations:animations completion:nil]; +} + ++ (void)animateWithDuration:(NSTimeInterval)duration + animations:(void (^)(void))animations + completion:(void (^)(BOOL))completion { + [self animateWithDuration:duration + delay:UIViewAnimationOptionCurveEaseInOut + options:0 + animations:animations + completion:completion]; +} + ++ (void)animateWithDuration:(NSTimeInterval)duration + delay:(NSTimeInterval)delay + options:(UIViewAnimationOptions)options + animations:(void (^ __nonnull)(void))animations + completion:(void (^ __nullable)(BOOL finished))completion { + MDMMotionAnimator *animator = [[[self class] alloc] init]; + MDMAnimationTraits *traits = [[MDMAnimationTraits alloc] initWithDuration:duration]; + traits.delay = delay; + if ((options & UIViewAnimationOptionCurveEaseIn) == UIViewAnimationOptionCurveEaseIn) { + traits.timingCurve = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn]; + } else if ((options & UIViewAnimationOptionCurveEaseOut) == UIViewAnimationOptionCurveEaseOut) { + traits.timingCurve = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]; + } else if ((options & UIViewAnimationOptionCurveLinear) == UIViewAnimationOptionCurveLinear) { + traits.timingCurve = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; + } + [animator animateWithTraits:traits animations:animations completion:completion]; +} + ++ (void)animateWithDuration:(NSTimeInterval)duration + delay:(NSTimeInterval)delay + usingSpringWithDamping:(CGFloat)dampingRatio + initialSpringVelocity:(CGFloat)velocity + options:(UIViewAnimationOptions)options + animations:(void (^)(void))animations + completion:(void (^)(BOOL))completion { + MDMMotionAnimator *animator = [[[self class] alloc] init]; + MDMAnimationTraits *traits = [[MDMAnimationTraits alloc] initWithDuration:duration]; + traits.delay = delay; + traits.timingCurve = + [[MDMSpringTimingCurveGenerator alloc] initWithDuration:duration + dampingRatio:dampingRatio + initialVelocity:velocity].springTimingCurve; + [animator animateWithTraits:traits animations:animations completion:completion]; +} + #pragma mark - Legacy - (void)animateWithTiming:(MDMMotionTiming)timing animations:(void (^)(void))animations { diff --git a/tests/unit/MotionAnimatorBehavioralTests.swift b/tests/unit/MotionAnimatorBehavioralTests.swift index e7c64d3..3848cf3 100644 --- a/tests/unit/MotionAnimatorBehavioralTests.swift +++ b/tests/unit/MotionAnimatorBehavioralTests.swift @@ -154,4 +154,21 @@ class AnimatorBehavioralTests: XCTestCase { } } + func testAllKeyPathsImplicitlyAnimateWithHeadlessLayerWithUIKitAPI() { + for (keyPath, value) in properties { + let layer = CAShapeLayer() + window.layer.addSublayer(layer) + + // Connect our layers to the render server. + CATransaction.flush() + + MotionAnimator.animate(withDuration: 0.5, animations: { + layer.setValue(value, forKeyPath: keyPath.rawValue) + }) + + XCTAssertNotNil(layer.animationKeys(), "Expected \(keyPath.rawValue) to generate animations") + + layer.removeFromSuperlayer() + } + } } diff --git a/tests/unit/UIKitBehavioralTests.swift b/tests/unit/UIKitBehavioralTests.swift index 07211fc..01e7de3 100644 --- a/tests/unit/UIKitBehavioralTests.swift +++ b/tests/unit/UIKitBehavioralTests.swift @@ -61,6 +61,7 @@ class UIKitBehavioralTests: XCTestCase { let oldSuperview = view.superview! view.removeFromSuperview() view = ShapeLayerBackedView() // Need to animate a view's layer to get implicit animations. + view.layer.anchorPoint = .zero oldSuperview.addSubview(view) // Connect our layers to the render server. @@ -71,15 +72,7 @@ class UIKitBehavioralTests: XCTestCase { func testSomePropertiesImplicitlyAnimateAdditively() { let baselineProperties: [AnimatableKeyPath: Any] = [ - .bounds: CGRect(x: 0, y: 0, width: 123, height: 456), .height: 100, - .position: CGPoint(x: 50, y: 20), - .rotation: 42, - .scale: 2.5, - .transform: CGAffineTransform(scaleX: 1.5, y: 1.5), - .width: 25, - .x: 12, - .y: 23, ] let properties: [AnimatableKeyPath: Any] if #available(iOS 11.0, *) { diff --git a/tests/unit/UIKitEquivalencyTests.swift b/tests/unit/UIKitEquivalencyTests.swift new file mode 100644 index 0000000..42b53e8 --- /dev/null +++ b/tests/unit/UIKitEquivalencyTests.swift @@ -0,0 +1,124 @@ +/* + Copyright 2017-present The Material Motion Authors. All Rights Reserved. + + 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 XCTest +#if IS_BAZEL_BUILD +import _MotionAnimator +#else +import MotionAnimator +#endif + +class UIKitEquivalencyTests: XCTestCase { + var view: UIView! + + var originalImplementation: IMP? + override func setUp() { + super.setUp() + + let window = UIWindow() + window.makeKeyAndVisible() + view = ShapeLayerBackedView() + window.addSubview(view) + + rebuildView() + } + + override func tearDown() { + view = nil + + super.tearDown() + } + + private func rebuildView() { + let oldSuperview = view.superview! + view.removeFromSuperview() + view = ShapeLayerBackedView() // Need to animate a view's layer to get implicit animations. + view.layer.anchorPoint = .zero // Needed to ensure that animating the width/height don't also + oldSuperview.addSubview(view) + + // Connect our layers to the render server. + CATransaction.flush() + } + + // MARK: Each animatable property needs to be added to exactly one of the following three tests + + func testMostPropertiesImplicitlyAnimateAdditively() { + let baselineProperties: [AnimatableKeyPath: Any] = [ + .cornerRadius: 3, + .position: CGPoint(x: 50, y: 20), + .rotation: 42, + .scale: 2.5, + .shadowOffset: CGSize(width: 1, height: 1), + .shadowOpacity: 0.3, + .shadowRadius: 5, + .strokeStart: 0.2, + .strokeEnd: 0.5, + .transform: CGAffineTransform(scaleX: 1.5, y: 1.5), + .x: 12, + .y: 23, + + // Should animate additively, but blocked by + // https://github.com/material-motion/motion-animator-objc/issues/74 + //.bounds: CGRect(x: 0, y: 0, width: 123, height: 456), + //.height: 100, + //.width: 25, + ] + for (keyPath, value) in baselineProperties { + rebuildView() + + MotionAnimator.animate(withDuration: 0.01) { + self.view.layer.setValue(value, forKeyPath: keyPath.rawValue) + } + + XCTAssertNotNil(view.layer.animationKeys(), + "Expected \(keyPath.rawValue) to generate at least one animation.") + if let animationKeys = view.layer.animationKeys() { + for key in animationKeys { + let animation = view.layer.animation(forKey: key) as! CABasicAnimation + XCTAssertTrue(animation.isAdditive, + "Expected \(key) to be additive as a result of animating " + + "\(keyPath.rawValue), but it was not: \(animation.debugDescription).") + } + } + } + } + + func testSomePropertiesImplicitlyAnimateButNotAdditively() { + let baselineProperties: [AnimatableKeyPath: Any] = [ + .backgroundColor: UIColor.blue, + .opacity: 0.5, + ] + for (keyPath, value) in baselineProperties { + rebuildView() + + MotionAnimator.animate(withDuration: 0.01) { + self.view.layer.setValue(value, forKeyPath: keyPath.rawValue) + } + + XCTAssertNotNil(view.layer.animationKeys(), + "Expected \(keyPath.rawValue) to generate at least one animation.") + if let animationKeys = view.layer.animationKeys() { + for key in animationKeys { + let animation = view.layer.animation(forKey: key) as! CABasicAnimation + XCTAssertFalse(animation.isAdditive, + "Expected \(key) not to be additive as a result of animating " + + "\(keyPath.rawValue), but it was: \(animation.debugDescription).") + } + } + } + } + +} From 1e76e2ba8fe9bc08d623a55623f5fd40579d0287 Mon Sep 17 00:00:00 2001 From: featherless Date: Thu, 14 Dec 2017 13:53:44 -0500 Subject: [PATCH 06/22] Throw an assertion when an unrecognized timing curve is provided. (#92) --- src/private/CABasicAnimation+MotionAnimator.m | 1 + 1 file changed, 1 insertion(+) diff --git a/src/private/CABasicAnimation+MotionAnimator.m b/src/private/CABasicAnimation+MotionAnimator.m index 8557b21..17f05ae 100644 --- a/src/private/CABasicAnimation+MotionAnimator.m +++ b/src/private/CABasicAnimation+MotionAnimator.m @@ -97,6 +97,7 @@ static BOOL IsAnimationKeyPathAlwaysNonAdditive(NSString *keyPath) { return animation; } + NSCAssert(NO, @"Unsupported animation trait: %@", traits); return nil; } From 46fd517e18b3a9555e46dcdd942601dd4ccd5149 Mon Sep 17 00:00:00 2001 From: featherless Date: Thu, 14 Dec 2017 13:53:56 -0500 Subject: [PATCH 07/22] Add support for using a spring generator as a timing curve. (#91) * Add support for using a spring generator as a timing curve. * Fix method name. --- .../project.pbxproj | 4 + src/private/CABasicAnimation+MotionAnimator.m | 31 +++++--- tests/unit/SpringTimingCurveTests.swift | 76 +++++++++++++++++++ 3 files changed, 100 insertions(+), 11 deletions(-) create mode 100644 tests/unit/SpringTimingCurveTests.swift diff --git a/examples/apps/Catalog/MotionAnimatorCatalog.xcodeproj/project.pbxproj b/examples/apps/Catalog/MotionAnimatorCatalog.xcodeproj/project.pbxproj index 7b2557b..fef005f 100644 --- a/examples/apps/Catalog/MotionAnimatorCatalog.xcodeproj/project.pbxproj +++ b/examples/apps/Catalog/MotionAnimatorCatalog.xcodeproj/project.pbxproj @@ -25,6 +25,7 @@ 667A3F541DEE273000CB3A99 /* TableOfContents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 667A3F531DEE273000CB3A99 /* TableOfContents.swift */; }; 6687264A1EF04B4C00113675 /* MotionAnimatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 668726491EF04B4C00113675 /* MotionAnimatorTests.swift */; }; 668819FA1FE2EB36003A9420 /* UIKitEquivalencyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 668819F91FE2EB36003A9420 /* UIKitEquivalencyTests.swift */; }; + 668819F81FE2E5C6003A9420 /* SpringTimingCurveTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 668819F71FE2E5C6003A9420 /* SpringTimingCurveTests.swift */; }; 669B6CA91FD0547100B80B76 /* MotionAnimatorBehavioralTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669B6CA81FD0547100B80B76 /* MotionAnimatorBehavioralTests.swift */; }; 66A6A6681FBA158000DE54CB /* AnimationRemovalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66A6A6671FBA158000DE54CB /* AnimationRemovalTests.swift */; }; 66BF5A8F1FB0E4CB00E864F6 /* ImplicitAnimationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66BF5A8E1FB0E4CB00E864F6 /* ImplicitAnimationTests.swift */; }; @@ -80,6 +81,7 @@ 667A3F531DEE273000CB3A99 /* TableOfContents.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableOfContents.swift; sourceTree = ""; }; 668726491EF04B4C00113675 /* MotionAnimatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MotionAnimatorTests.swift; sourceTree = ""; }; 668819F91FE2EB36003A9420 /* UIKitEquivalencyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIKitEquivalencyTests.swift; sourceTree = ""; }; + 668819F71FE2E5C6003A9420 /* SpringTimingCurveTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpringTimingCurveTests.swift; sourceTree = ""; }; 669B6CA81FD0547100B80B76 /* MotionAnimatorBehavioralTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MotionAnimatorBehavioralTests.swift; sourceTree = ""; }; 66A6A6671FBA158000DE54CB /* AnimationRemovalTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimationRemovalTests.swift; sourceTree = ""; }; 66BF5A8E1FB0E4CB00E864F6 /* ImplicitAnimationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImplicitAnimationTests.swift; sourceTree = ""; }; @@ -241,6 +243,7 @@ 669B6CA81FD0547100B80B76 /* MotionAnimatorBehavioralTests.swift */, 66FD99F91EE9FBBE00C53A82 /* MotionAnimatorTests.m */, 668726491EF04B4C00113675 /* MotionAnimatorTests.swift */, + 668819F71FE2E5C6003A9420 /* SpringTimingCurveTests.swift */, 664F59991FCE6661002EC56D /* NonAdditiveAnimatorTests.swift */, 664F59951FCDB2E5002EC56D /* QuartzCoreBehavioralTests.swift */, 660636011FACC24300C3DFB8 /* TimeScaleFactorTests.swift */, @@ -522,6 +525,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 668819F81FE2E5C6003A9420 /* SpringTimingCurveTests.swift in Sources */, 6625876C1FB4DB9C00BC7DF1 /* InitialVelocityTests.swift in Sources */, 66EF6F281FC33C4800C83A63 /* HeadlessLayerImplicitAnimationTests.swift in Sources */, 664F599A1FCE6661002EC56D /* NonAdditiveAnimatorTests.swift in Sources */, diff --git a/src/private/CABasicAnimation+MotionAnimator.m b/src/private/CABasicAnimation+MotionAnimator.m index 17f05ae..087f97f 100644 --- a/src/private/CABasicAnimation+MotionAnimator.m +++ b/src/private/CABasicAnimation+MotionAnimator.m @@ -81,20 +81,29 @@ static BOOL IsAnimationKeyPathAlwaysNonAdditive(NSString *keyPath) { return animation; } - if ([traits.timingCurve isKindOfClass:[MDMSpringTimingCurve class]]) { - MDMSpringTimingCurve *springTiming = (MDMSpringTimingCurve *)traits.timingCurve; - + CABasicAnimation *(^animationFromSpring)(MDMSpringTimingCurve *) = + ^(MDMSpringTimingCurve *springTiming) { #pragma clang diagnostic push - // CASpringAnimation is a private API on iOS 8 - we're able to make use of it because we're - // linking against the public API on iOS 9+. + // CASpringAnimation is a private API on iOS 8 - we're able to make use of it because we're + // linking against the public API on iOS 9+. #pragma clang diagnostic ignored "-Wpartial-availability" - CASpringAnimation *animation = [CASpringAnimation animation]; + CASpringAnimation *animation = [CASpringAnimation animation]; #pragma clang diagnostic pop - animation.mass = springTiming.mass; - animation.stiffness = springTiming.tension; - animation.damping = springTiming.friction; - animation.duration = traits.duration; - return animation; + animation.mass = springTiming.mass; + animation.stiffness = springTiming.tension; + animation.damping = springTiming.friction; + animation.duration = traits.duration; + return animation; + }; + + if ([traits.timingCurve isKindOfClass:[MDMSpringTimingCurveGenerator class]]) { + MDMSpringTimingCurveGenerator *springTimingGenerator = + (MDMSpringTimingCurveGenerator *)traits.timingCurve; + return animationFromSpring(springTimingGenerator.springTimingCurve); + } + + if ([traits.timingCurve isKindOfClass:[MDMSpringTimingCurve class]]) { + return animationFromSpring((MDMSpringTimingCurve *)traits.timingCurve); } NSCAssert(NO, @"Unsupported animation trait: %@", traits); diff --git a/tests/unit/SpringTimingCurveTests.swift b/tests/unit/SpringTimingCurveTests.swift new file mode 100644 index 0000000..92ebed2 --- /dev/null +++ b/tests/unit/SpringTimingCurveTests.swift @@ -0,0 +1,76 @@ +/* + Copyright 2017-present The Material Motion Authors. All Rights Reserved. + + 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 XCTest +#if IS_BAZEL_BUILD +import _MotionAnimator +#else +import MotionAnimator +#endif + +class SpringTimingCurveTests: XCTestCase { + var animator: MotionAnimator! + var traits: MDMAnimationTraits! + var view: UIView! + + var originalImplementation: IMP? + override func setUp() { + super.setUp() + + animator = MotionAnimator() + traits = MDMAnimationTraits(duration: 1) + + let window = UIWindow() + window.makeKeyAndVisible() + view = UIView() // Need to animate a view's layer to get implicit animations. + window.addSubview(view) + + // Connect our layers to the render server. + CATransaction.flush() + } + + override func tearDown() { + animator = nil + traits = nil + view = nil + + super.tearDown() + } + + @available(iOS 9.0, *) + func testGeneratorCanBeUsedAsATimingCurve() { + traits.timingCurve = MDMSpringTimingCurveGenerator(duration: traits.duration, dampingRatio: 0.5) + + animator.animate(with: traits, between: [1, 0], layer: view.layer, keyPath: .cornerRadius) + + XCTAssertNotNil(view.layer.animationKeys(), + "Expected an animation to be added, but none were found.") + guard let animationKeys = view.layer.animationKeys() else { + return + } + XCTAssertEqual(animationKeys.count, 1, + "Expected only one animation to be added, but the following were found: " + + "\(animationKeys).") + guard let key = animationKeys.first else { + return + } + XCTAssertTrue(view.layer.animation(forKey: key) is CASpringAnimation, + "Expected the animation to be a spring, but it was not: " + + " \(view.layer.animation(forKey: key).debugDescription)") + + } + +} From e41ccb4890b9ed3bad5ab52015f8224adb5bbba6 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Thu, 14 Dec 2017 14:55:07 -0500 Subject: [PATCH 08/22] Add back test properties that were accidentally removed in 69469aedb987e516ff1f43a123b3ee29dfef38ca. --- tests/unit/UIKitBehavioralTests.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/unit/UIKitBehavioralTests.swift b/tests/unit/UIKitBehavioralTests.swift index 01e7de3..c4b97ca 100644 --- a/tests/unit/UIKitBehavioralTests.swift +++ b/tests/unit/UIKitBehavioralTests.swift @@ -72,7 +72,15 @@ class UIKitBehavioralTests: XCTestCase { func testSomePropertiesImplicitlyAnimateAdditively() { let baselineProperties: [AnimatableKeyPath: Any] = [ + .bounds: CGRect(x: 0, y: 0, width: 123, height: 456), .height: 100, + .position: CGPoint(x: 50, y: 20), + .rotation: 42, + .scale: 2.5, + .transform: CGAffineTransform(scaleX: 1.5, y: 1.5), + .width: 25, + .x: 12, + .y: 23, ] let properties: [AnimatableKeyPath: Any] if #available(iOS 11.0, *) { From 024296aca338b106cf49b87fcc83b9feb216ee9d Mon Sep 17 00:00:00 2001 From: featherless Date: Thu, 14 Dec 2017 15:44:25 -0500 Subject: [PATCH 09/22] Standardize our param docs formatting. (#95) --- src/MDMMotionAnimator.h | 65 ++++++++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 27 deletions(-) diff --git a/src/MDMMotionAnimator.h b/src/MDMMotionAnimator.h index 36f2e05..efec880 100644 --- a/src/MDMMotionAnimator.h +++ b/src/MDMMotionAnimator.h @@ -71,11 +71,14 @@ NS_SWIFT_NAME(MotionAnimator) In this case, multiple invocations of this function on the same key path will remove the animations added from prior invocations. - @param traits The traits to be used for the animation. - @param layer The layer to be animated. - @param values The values to be used in the animation. Must contain exactly two values. Supported - UIKit types will be coerced to their Core Animation equivalent. Supported UIKit values include - UIColor and UIBezierPath. + @param traits The traits to be used for the animation. + + @param layer The layer to be animated. + + @param values The values to be used in the animation. Must contain exactly two values. Supported + UIKit types will be coerced to their Core Animation equivalent. Supported UIKit + values include UIColor and UIBezierPath. + @param keyPath The key path of the property to be animated. */ - (void)animateWithTraits:(nonnull MDMAnimationTraits *)traits @@ -90,16 +93,20 @@ NS_SWIFT_NAME(MotionAnimator) In this case, multiple invocations of this function on the same key path will remove the animations added from prior invocations. - @param traits The traits to be used for the animation. - @param layer The layer to be animated. - @param values The values to be used in the animation. Must contain exactly two values. Supported - UIKit types will be coerced to their Core Animation equivalent. Supported UIKit values include - UIColor and UIBezierPath. - @param keyPath The key path of the property to be animated. - @param completion A block object to be executed when the animation ends or is removed from the - animation hierarchy. If the duration of the animation is 0, this block is executed immediately. - The block is escaping and will be released once the animations have completed. The provided - `finished` argument is currently always YES. + @param traits The traits to be used for the animation. + + @param layer The layer to be animated. + + @param values The values to be used in the animation. Must contain exactly two values. + Supported UIKit types will be coerced to their Core Animation equivalent. + + @param keyPath The key path of the property to be animated. + + @param completion A block object to be executed when the animation ends or is removed from the + animation hierarchy. If the duration of the animation is 0, this block is + executed immediately. The block is escaping and will be released once the + animations have completed. The provided `finished` argument is currently always + YES. */ - (void)animateWithTraits:(nonnull MDMAnimationTraits *)traits between:(nonnull NSArray *)values @@ -121,10 +128,11 @@ NS_SWIFT_NAME(MotionAnimator) /** Performs `animations` using the traits provided. - @param traits The traits to be used for the animation. - @param animations The block to be executed. Any animatable properties changed within this block - will result in animations being added to the view's layer with the provided traits. The block is - non-escaping. + @param traits The traits to be used for the animation. + + @param animations The block to be executed. Any animatable properties changed within this block + will result in animations being added to the view's layer with the provided + traits. The block is non-escaping. */ - (void)animateWithTraits:(nonnull MDMAnimationTraits *)traits animations:(nonnull void(^)(void))animations; @@ -133,14 +141,17 @@ NS_SWIFT_NAME(MotionAnimator) Performs `animations` using the traits provided and executes the completion handler once all added animations have completed. - @param traits The traits to be used for the animation. - @param animations The block to be executed. Any animatable properties changed within this block - will result in animations being added to the view's layer with the provided traits. The block is - non-escaping. - @param completion A block object to be executed once the animation sequence ends or it has been - removed from the animation hierarchy. If the duration of the animation is 0, this block is executed - immediately. The block is escaping and will be released once the animation sequence has completed. - The provided `finished` argument is currently always YES. + @param traits The traits to be used for the animation. + + @param animations The block to be executed. Any animatable properties changed within this block + will result in animations being added to the view's layer with the provided + traits. The block is non-escaping. + + @param completion A block object to be executed once the animation sequence ends or it has been + removed from the animation hierarchy. If the duration of the animation is 0, + this block is executed immediately. The block is escaping and will be released + once the animation sequence has completed. The provided `finished` argument is + currently always YES. */ - (void)animateWithTraits:(nonnull MDMAnimationTraits *)traits animations:(nonnull void (^)(void))animations From e260418023430e0541360ec46ed6d4d931dbf05c Mon Sep 17 00:00:00 2001 From: featherless Date: Thu, 14 Dec 2017 15:44:59 -0500 Subject: [PATCH 10/22] Improve the documentation for initial velocity. (#94) The animator provides a more natural interpretation of velocity than UIKit/Core Animation's, so we need to document this fact in the public UIKit-like APIs. --- src/MDMMotionAnimator.h | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/MDMMotionAnimator.h b/src/MDMMotionAnimator.h index efec880..8cb0e44 100644 --- a/src/MDMMotionAnimator.h +++ b/src/MDMMotionAnimator.h @@ -304,10 +304,11 @@ NS_SWIFT_NAME(MotionAnimator) @param velocity From UIKit's documentation: "The initial spring velocity. For smooth start to the animation, match this value to the view’s velocity as it was prior to - attachment. A value of 1 corresponds to the total animation distance traversed - in one second. For example, if the total animation distance is 200 points and - you want the start of the animation to match a view velocity of 100 pt/s, use a - value of 0.5." + attachment." + Unlike UIKit's API, the initial velocity value is measured in terms of absolute + units of motion. For example, if animating a position from 0 to 10 with an + initial velocity of 100 points/second, the provided initial velocity value + should be 100. @param animations From UIKit's documentation: "A block object containing the changes to commit to the views. This is where you programmatically change any animatable properties From 32c78d4d93b6c82ff55693c54176b2826f718e5e Mon Sep 17 00:00:00 2001 From: featherless Date: Thu, 14 Dec 2017 15:48:57 -0500 Subject: [PATCH 11/22] Add support for additively animating bounds. (#93) Closes https://github.com/material-motion/motion-animator-objc/issues/74 --- src/MDMAnimatableKeyPaths.h | 4 +- src/private/CABasicAnimation+MotionAnimator.m | 42 +++++++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/MDMAnimatableKeyPaths.h b/src/MDMAnimatableKeyPaths.h index 2a9bbc3..e02a037 100644 --- a/src/MDMAnimatableKeyPaths.h +++ b/src/MDMAnimatableKeyPaths.h @@ -58,9 +58,7 @@ FOUNDATION_EXPORT MDMAnimatableKeyPath MDMKeyPathBackgroundColor NS_SWIFT_NAME(b Equivalent UIView property: bounds Equivalent CALayer property: bounds Expected value type: CGRect or NSValue (containing a CGRect). - Additive animation supported: No. - TODO( https://github.com/material-motion/motion-animator-objc/issues/74 ): - Add support for additively animating CGRect types. + Additive animation supported: Yes. */ FOUNDATION_EXPORT MDMAnimatableKeyPath MDMKeyPathBounds NS_SWIFT_NAME(bounds); diff --git a/src/private/CABasicAnimation+MotionAnimator.m b/src/private/CABasicAnimation+MotionAnimator.m index 087f97f..3f212b5 100644 --- a/src/private/CABasicAnimation+MotionAnimator.m +++ b/src/private/CABasicAnimation+MotionAnimator.m @@ -44,6 +44,15 @@ static BOOL IsCGSizeType(id someValue) { return NO; } +static BOOL IsCGRectType(id someValue) { + if ([someValue isKindOfClass:[NSValue class]]) { + NSValue *asValue = (NSValue *)someValue; + const char *objCType = @encode(CGRect); + return strncmp(asValue.objCType, objCType, strlen(objCType)) == 0; + } + return NO; +} + static BOOL IsCATransform3DType(id someValue) { if ([someValue isKindOfClass:[NSValue class]]) { NSValue *asValue = (NSValue *)someValue; @@ -270,6 +279,39 @@ void MDMConfigureAnimation(CABasicAnimation *animation, MDMAnimationTraits * tra } } + } else if (IsCGRectType(animation.toValue)) { + CGRect from = [animation.fromValue CGRectValue]; + CGRect to = [animation.toValue CGRectValue]; + CGRect additiveDisplacement = CGRectMake(from.origin.x - to.origin.x, + from.origin.y - to.origin.y, + from.size.width - to.size.width, + from.size.height - to.size.height); + + if (animation.additive) { + animation.fromValue = [NSValue valueWithCGRect:additiveDisplacement]; + animation.toValue = [NSValue valueWithCGRect:CGRectZero]; + } + + if (isSpringAnimation) { + // Core Animation's velocity system is single dimensional, so we pick the dominant direction + // of movement and normalize accordingly. + CGFloat biggestDelta = additiveDisplacement.origin.x; + if (fabs(additiveDisplacement.origin.y) > fabs(biggestDelta)) { + biggestDelta = additiveDisplacement.origin.y; + } + if (fabs(additiveDisplacement.size.width) > fabs(biggestDelta)) { + biggestDelta = additiveDisplacement.size.width; + } + if (fabs(additiveDisplacement.size.height) > fabs(biggestDelta)) { + biggestDelta = additiveDisplacement.size.height; + } + CGFloat displacement = -biggestDelta; + CGFloat absoluteInitialVelocity = springTimingCurve.initialVelocity; + if (fabs(displacement) > 0.00001) { + springAnimation.initialVelocity = absoluteInitialVelocity / displacement; + } + } + } else if (IsCATransform3DType(animation.toValue)) { CATransform3D from = [animation.fromValue CATransform3DValue]; CATransform3D to = [animation.toValue CATransform3DValue]; From 23cd380bf2414b2834da7f8d55548cd00166d6f8 Mon Sep 17 00:00:00 2001 From: featherless Date: Thu, 14 Dec 2017 15:58:29 -0500 Subject: [PATCH 12/22] Add support for animating z position. (#96) --- src/MDMAnimatableKeyPaths.h | 10 ++++++++++ src/MDMAnimatableKeyPaths.m | 1 + src/private/MDMBlockAnimations.m | 3 ++- tests/unit/MotionAnimatorBehavioralTests.swift | 1 + tests/unit/QuartzCoreBehavioralTests.swift | 1 + tests/unit/UIKitBehavioralTests.swift | 1 + tests/unit/UIKitEquivalencyTests.swift | 1 + 7 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/MDMAnimatableKeyPaths.h b/src/MDMAnimatableKeyPaths.h index e02a037..9291635 100644 --- a/src/MDMAnimatableKeyPaths.h +++ b/src/MDMAnimatableKeyPaths.h @@ -218,3 +218,13 @@ FOUNDATION_EXPORT MDMAnimatableKeyPath MDMKeyPathX NS_SWIFT_NAME(x); Additive animation supported: Yes. */ FOUNDATION_EXPORT MDMAnimatableKeyPath MDMKeyPathY NS_SWIFT_NAME(y); + +/** + Z position. + + Equivalent UIView property: N/A + Equivalent CALayer property: zPosition + Expected value type: CGFloat or NSNumber. + Additive animation supported: Yes. + */ +FOUNDATION_EXPORT MDMAnimatableKeyPath MDMKeyPathZ NS_SWIFT_NAME(z); diff --git a/src/MDMAnimatableKeyPaths.m b/src/MDMAnimatableKeyPaths.m index 1fc39a8..99fc8bf 100644 --- a/src/MDMAnimatableKeyPaths.m +++ b/src/MDMAnimatableKeyPaths.m @@ -33,3 +33,4 @@ MDMAnimatableKeyPath MDMKeyPathWidth = @"bounds.size.width"; MDMAnimatableKeyPath MDMKeyPathX = @"position.x"; MDMAnimatableKeyPath MDMKeyPathY = @"position.y"; +MDMAnimatableKeyPath MDMKeyPathZ = @"zPosition"; diff --git a/src/private/MDMBlockAnimations.m b/src/private/MDMBlockAnimations.m index bfa0ad8..d1dcb3a 100644 --- a/src/private/MDMBlockAnimations.m +++ b/src/private/MDMBlockAnimations.m @@ -43,7 +43,8 @@ MDMKeyPathTransform, MDMKeyPathWidth, MDMKeyPathX, - MDMKeyPathY]]; + MDMKeyPathY, + MDMKeyPathZ]]; }); return animatableKeyPaths; } diff --git a/tests/unit/MotionAnimatorBehavioralTests.swift b/tests/unit/MotionAnimatorBehavioralTests.swift index 3848cf3..14ad794 100644 --- a/tests/unit/MotionAnimatorBehavioralTests.swift +++ b/tests/unit/MotionAnimatorBehavioralTests.swift @@ -60,6 +60,7 @@ class AnimatorBehavioralTests: XCTestCase { .width: 25, .x: 12, .y: 23, + .z: 3, ] func testAllKeyPathsExplicitlyAnimateWithLayerBackingUIView() { diff --git a/tests/unit/QuartzCoreBehavioralTests.swift b/tests/unit/QuartzCoreBehavioralTests.swift index 6599114..eb0f804 100644 --- a/tests/unit/QuartzCoreBehavioralTests.swift +++ b/tests/unit/QuartzCoreBehavioralTests.swift @@ -85,6 +85,7 @@ class QuartzCoreBehavioralTests: XCTestCase { .width: 25, .x: 12, .y: 23, + .z: 3, ] for (keyPath, value) in properties { rebuildLayer() diff --git a/tests/unit/UIKitBehavioralTests.swift b/tests/unit/UIKitBehavioralTests.swift index c4b97ca..ad72e2c 100644 --- a/tests/unit/UIKitBehavioralTests.swift +++ b/tests/unit/UIKitBehavioralTests.swift @@ -144,6 +144,7 @@ class UIKitBehavioralTests: XCTestCase { .shadowRadius: 5, .strokeStart: 0.2, .strokeEnd: 0.5, + .z: 3, ] let properties: [AnimatableKeyPath: Any] diff --git a/tests/unit/UIKitEquivalencyTests.swift b/tests/unit/UIKitEquivalencyTests.swift index 42b53e8..258199c 100644 --- a/tests/unit/UIKitEquivalencyTests.swift +++ b/tests/unit/UIKitEquivalencyTests.swift @@ -69,6 +69,7 @@ class UIKitEquivalencyTests: XCTestCase { .transform: CGAffineTransform(scaleX: 1.5, y: 1.5), .x: 12, .y: 23, + .z: 3, // Should animate additively, but blocked by // https://github.com/material-motion/motion-animator-objc/issues/74 From 55c23d5c4f8029cc45f6331a894ddf1960863ba7 Mon Sep 17 00:00:00 2001 From: featherless Date: Thu, 14 Dec 2017 16:18:46 -0500 Subject: [PATCH 13/22] Add support for animating border width and color. (#98) --- src/MDMAnimatableKeyPaths.h | 20 +++++++++++++++++++ src/MDMAnimatableKeyPaths.m | 2 ++ src/private/MDMBlockAnimations.m | 2 ++ .../unit/MotionAnimatorBehavioralTests.swift | 2 ++ tests/unit/QuartzCoreBehavioralTests.swift | 2 ++ tests/unit/UIKitBehavioralTests.swift | 2 ++ tests/unit/UIKitEquivalencyTests.swift | 2 ++ 7 files changed, 32 insertions(+) diff --git a/src/MDMAnimatableKeyPaths.h b/src/MDMAnimatableKeyPaths.h index 9291635..9cacb92 100644 --- a/src/MDMAnimatableKeyPaths.h +++ b/src/MDMAnimatableKeyPaths.h @@ -62,6 +62,26 @@ FOUNDATION_EXPORT MDMAnimatableKeyPath MDMKeyPathBackgroundColor NS_SWIFT_NAME(b */ FOUNDATION_EXPORT MDMAnimatableKeyPath MDMKeyPathBounds NS_SWIFT_NAME(bounds); +/** + Border width. + + Equivalent UIView property: N/A + Equivalent CALayer property: borderWidth + Expected value type: CGFloat or NSNumber. + Additive animation supported: Yes. + */ +FOUNDATION_EXPORT MDMAnimatableKeyPath MDMKeyPathBorderWidth NS_SWIFT_NAME(borderWidth); + +/** + Border color. + + Equivalent UIView property: N/A + Equivalent CALayer property: borderColor + Expected value type: UIColor or CGColor. + Additive animation supported: No. + */ +FOUNDATION_EXPORT MDMAnimatableKeyPath MDMKeyPathBorderColor NS_SWIFT_NAME(borderColor); + /** Corner radius. diff --git a/src/MDMAnimatableKeyPaths.m b/src/MDMAnimatableKeyPaths.m index 99fc8bf..771f3e2 100644 --- a/src/MDMAnimatableKeyPaths.m +++ b/src/MDMAnimatableKeyPaths.m @@ -18,6 +18,8 @@ MDMAnimatableKeyPath MDMKeyPathBackgroundColor = @"backgroundColor"; MDMAnimatableKeyPath MDMKeyPathBounds = @"bounds"; +MDMAnimatableKeyPath MDMKeyPathBorderWidth = @"borderWidth"; +MDMAnimatableKeyPath MDMKeyPathBorderColor = @"borderColor"; MDMAnimatableKeyPath MDMKeyPathCornerRadius = @"cornerRadius"; MDMAnimatableKeyPath MDMKeyPathHeight = @"bounds.size.height"; MDMAnimatableKeyPath MDMKeyPathOpacity = @"opacity"; diff --git a/src/private/MDMBlockAnimations.m b/src/private/MDMBlockAnimations.m index d1dcb3a..acd0848 100644 --- a/src/private/MDMBlockAnimations.m +++ b/src/private/MDMBlockAnimations.m @@ -28,6 +28,8 @@ static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ animatableKeyPaths = [NSSet setWithArray:@[MDMKeyPathBackgroundColor, + MDMKeyPathBorderWidth, + MDMKeyPathBorderColor, MDMKeyPathBounds, MDMKeyPathCornerRadius, MDMKeyPathHeight, diff --git a/tests/unit/MotionAnimatorBehavioralTests.swift b/tests/unit/MotionAnimatorBehavioralTests.swift index 14ad794..80f4af4 100644 --- a/tests/unit/MotionAnimatorBehavioralTests.swift +++ b/tests/unit/MotionAnimatorBehavioralTests.swift @@ -44,6 +44,8 @@ class AnimatorBehavioralTests: XCTestCase { private let properties: [AnimatableKeyPath: Any] = [ .backgroundColor: UIColor.blue, + .borderWidth: 5, + .borderColor: UIColor.red, .bounds: CGRect(x: 0, y: 0, width: 123, height: 456), .cornerRadius: 3, .height: 100, diff --git a/tests/unit/QuartzCoreBehavioralTests.swift b/tests/unit/QuartzCoreBehavioralTests.swift index eb0f804..c7cdae5 100644 --- a/tests/unit/QuartzCoreBehavioralTests.swift +++ b/tests/unit/QuartzCoreBehavioralTests.swift @@ -69,6 +69,8 @@ class QuartzCoreBehavioralTests: XCTestCase { func testWhichPropertiesImplicitlyAnimateButNotAdditively() { let properties: [AnimatableKeyPath: Any] = [ + .borderWidth: 5, + .borderColor: UIColor.red, .bounds: CGRect(x: 0, y: 0, width: 123, height: 456), .cornerRadius: 3, .height: 100, diff --git a/tests/unit/UIKitBehavioralTests.swift b/tests/unit/UIKitBehavioralTests.swift index ad72e2c..ade1b5f 100644 --- a/tests/unit/UIKitBehavioralTests.swift +++ b/tests/unit/UIKitBehavioralTests.swift @@ -138,6 +138,8 @@ class UIKitBehavioralTests: XCTestCase { func testSomePropertiesDoNotImplicitlyAnimate() { let baselineProperties: [AnimatableKeyPath: Any] = [ + .borderWidth: 5, + .borderColor: UIColor.red, .cornerRadius: 3, .shadowOffset: CGSize(width: 1, height: 1), .shadowOpacity: 0.3, diff --git a/tests/unit/UIKitEquivalencyTests.swift b/tests/unit/UIKitEquivalencyTests.swift index 258199c..1b9d00c 100644 --- a/tests/unit/UIKitEquivalencyTests.swift +++ b/tests/unit/UIKitEquivalencyTests.swift @@ -57,6 +57,7 @@ class UIKitEquivalencyTests: XCTestCase { func testMostPropertiesImplicitlyAnimateAdditively() { let baselineProperties: [AnimatableKeyPath: Any] = [ + .borderWidth: 5, .cornerRadius: 3, .position: CGPoint(x: 50, y: 20), .rotation: 42, @@ -100,6 +101,7 @@ class UIKitEquivalencyTests: XCTestCase { func testSomePropertiesImplicitlyAnimateButNotAdditively() { let baselineProperties: [AnimatableKeyPath: Any] = [ .backgroundColor: UIColor.blue, + .borderColor: UIColor.red, .opacity: 0.5, ] for (keyPath, value) in baselineProperties { From ca896623a4e5a42a458e6fd59c8f405b55e87e26 Mon Sep 17 00:00:00 2001 From: featherless Date: Thu, 14 Dec 2017 16:34:17 -0500 Subject: [PATCH 14/22] Add support for animating shadow color. (#99) --- src/MDMAnimatableKeyPaths.h | 10 ++++++++++ src/MDMAnimatableKeyPaths.m | 1 + src/private/MDMBlockAnimations.m | 1 + tests/unit/MotionAnimatorBehavioralTests.swift | 1 + tests/unit/QuartzCoreBehavioralTests.swift | 1 + tests/unit/UIKitBehavioralTests.swift | 2 ++ tests/unit/UIKitEquivalencyTests.swift | 1 + 7 files changed, 17 insertions(+) diff --git a/src/MDMAnimatableKeyPaths.h b/src/MDMAnimatableKeyPaths.h index 9cacb92..b4e7c03 100644 --- a/src/MDMAnimatableKeyPaths.h +++ b/src/MDMAnimatableKeyPaths.h @@ -146,6 +146,16 @@ FOUNDATION_EXPORT MDMAnimatableKeyPath MDMKeyPathRotation NS_SWIFT_NAME(rotation */ FOUNDATION_EXPORT MDMAnimatableKeyPath MDMKeyPathScale NS_SWIFT_NAME(scale); +/** + Shadow color. + + Equivalent UIView property: N/A + Equivalent CALayer property: shadowColor + Expected value type: UIColor or CGColor. + Additive animation supported: No. + */ +FOUNDATION_EXPORT MDMAnimatableKeyPath MDMKeyPathShadowColor NS_SWIFT_NAME(shadowColor); + /** Shadow offset. diff --git a/src/MDMAnimatableKeyPaths.m b/src/MDMAnimatableKeyPaths.m index 771f3e2..4637b7a 100644 --- a/src/MDMAnimatableKeyPaths.m +++ b/src/MDMAnimatableKeyPaths.m @@ -26,6 +26,7 @@ MDMAnimatableKeyPath MDMKeyPathPosition = @"position"; MDMAnimatableKeyPath MDMKeyPathRotation = @"transform.rotation.z"; MDMAnimatableKeyPath MDMKeyPathScale = @"transform.scale"; +MDMAnimatableKeyPath MDMKeyPathShadowColor = @"shadowColor"; MDMAnimatableKeyPath MDMKeyPathShadowOffset = @"shadowOffset"; MDMAnimatableKeyPath MDMKeyPathShadowOpacity = @"shadowOpacity"; MDMAnimatableKeyPath MDMKeyPathShadowRadius = @"shadowRadius"; diff --git a/src/private/MDMBlockAnimations.m b/src/private/MDMBlockAnimations.m index acd0848..c0f2517 100644 --- a/src/private/MDMBlockAnimations.m +++ b/src/private/MDMBlockAnimations.m @@ -37,6 +37,7 @@ MDMKeyPathPosition, MDMKeyPathRotation, MDMKeyPathScale, + MDMKeyPathShadowColor, MDMKeyPathShadowOffset, MDMKeyPathShadowOpacity, MDMKeyPathShadowRadius, diff --git a/tests/unit/MotionAnimatorBehavioralTests.swift b/tests/unit/MotionAnimatorBehavioralTests.swift index 80f4af4..796813c 100644 --- a/tests/unit/MotionAnimatorBehavioralTests.swift +++ b/tests/unit/MotionAnimatorBehavioralTests.swift @@ -53,6 +53,7 @@ class AnimatorBehavioralTests: XCTestCase { .position: CGPoint(x: 50, y: 20), .rotation: 42, .scale: 2.5, + .shadowColor: UIColor.blue, .shadowOffset: CGSize(width: 1, height: 1), .shadowOpacity: 0.3, .shadowRadius: 5, diff --git a/tests/unit/QuartzCoreBehavioralTests.swift b/tests/unit/QuartzCoreBehavioralTests.swift index c7cdae5..c1c0db2 100644 --- a/tests/unit/QuartzCoreBehavioralTests.swift +++ b/tests/unit/QuartzCoreBehavioralTests.swift @@ -78,6 +78,7 @@ class QuartzCoreBehavioralTests: XCTestCase { .position: CGPoint(x: 50, y: 20), .rotation: 42, .scale: 2.5, + .shadowColor: UIColor.blue, .shadowOffset: CGSize(width: 1, height: 1), .shadowOpacity: 0.3, .shadowRadius: 5, diff --git a/tests/unit/UIKitBehavioralTests.swift b/tests/unit/UIKitBehavioralTests.swift index ade1b5f..ad72581 100644 --- a/tests/unit/UIKitBehavioralTests.swift +++ b/tests/unit/UIKitBehavioralTests.swift @@ -141,6 +141,7 @@ class UIKitBehavioralTests: XCTestCase { .borderWidth: 5, .borderColor: UIColor.red, .cornerRadius: 3, + .shadowColor: UIColor.blue, .shadowOffset: CGSize(width: 1, height: 1), .shadowOpacity: 0.3, .shadowRadius: 5, @@ -184,6 +185,7 @@ class UIKitBehavioralTests: XCTestCase { .position: CGPoint(x: 50, y: 20), .rotation: 42, .scale: 2.5, + .shadowColor: UIColor.blue, .shadowOffset: CGSize(width: 1, height: 1), .shadowOpacity: 0.3, .shadowRadius: 5, diff --git a/tests/unit/UIKitEquivalencyTests.swift b/tests/unit/UIKitEquivalencyTests.swift index 1b9d00c..df8de42 100644 --- a/tests/unit/UIKitEquivalencyTests.swift +++ b/tests/unit/UIKitEquivalencyTests.swift @@ -103,6 +103,7 @@ class UIKitEquivalencyTests: XCTestCase { .backgroundColor: UIColor.blue, .borderColor: UIColor.red, .opacity: 0.5, + .shadowColor: UIColor.blue, ] for (keyPath, value) in baselineProperties { rebuildView() From 646b6f6fd1ed3a10dae7ab4e98caa43cad65f2ff Mon Sep 17 00:00:00 2001 From: featherless Date: Thu, 14 Dec 2017 16:34:28 -0500 Subject: [PATCH 15/22] Add support for animating anchorPoint. (#97) --- src/MDMAnimatableKeyPaths.h | 10 ++++++++++ src/MDMAnimatableKeyPaths.m | 1 + src/private/CABasicAnimation+MotionAnimator.m | 5 ++++- src/private/MDMBlockAnimations.m | 3 ++- tests/unit/MotionAnimatorBehavioralTests.swift | 1 + tests/unit/QuartzCoreBehavioralTests.swift | 1 + tests/unit/UIKitBehavioralTests.swift | 1 + tests/unit/UIKitEquivalencyTests.swift | 1 + 8 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/MDMAnimatableKeyPaths.h b/src/MDMAnimatableKeyPaths.h index b4e7c03..9345f93 100644 --- a/src/MDMAnimatableKeyPaths.h +++ b/src/MDMAnimatableKeyPaths.h @@ -42,6 +42,16 @@ NS_SWIFT_NAME(AnimatableKeyPath) typedef NSString * const MDMAnimatableKeyPath CF_TYPED_ENUM; +/** + Anchor point. + + Equivalent UIView property: N/A + Equivalent CALayer property: anchorPoint + Expected value type: CGPoint or NSValue (containing a CGPoint). + Additive animation supported: No. + */ +FOUNDATION_EXPORT MDMAnimatableKeyPath MDMKeyPathAnchorPoint NS_SWIFT_NAME(anchorPoint); + /** Background color. diff --git a/src/MDMAnimatableKeyPaths.m b/src/MDMAnimatableKeyPaths.m index 4637b7a..217580a 100644 --- a/src/MDMAnimatableKeyPaths.m +++ b/src/MDMAnimatableKeyPaths.m @@ -16,6 +16,7 @@ #import "MDMAnimatableKeyPaths.h" +MDMAnimatableKeyPath MDMKeyPathAnchorPoint = @"anchorPoint"; MDMAnimatableKeyPath MDMKeyPathBackgroundColor = @"backgroundColor"; MDMAnimatableKeyPath MDMKeyPathBounds = @"bounds"; MDMAnimatableKeyPath MDMKeyPathBorderWidth = @"borderWidth"; diff --git a/src/private/CABasicAnimation+MotionAnimator.m b/src/private/CABasicAnimation+MotionAnimator.m index 3f212b5..cd68e9c 100644 --- a/src/private/CABasicAnimation+MotionAnimator.m +++ b/src/private/CABasicAnimation+MotionAnimator.m @@ -17,6 +17,7 @@ #import "CABasicAnimation+MotionAnimator.h" #import "CAMediaTimingFunction+MotionAnimator.h" +#import "MDMAnimatableKeyPaths.h" #import @@ -66,7 +67,9 @@ static BOOL IsAnimationKeyPathAlwaysNonAdditive(NSString *keyPath) { static NSSet *nonAdditiveKeyPaths = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - nonAdditiveKeyPaths = [NSSet setWithArray:@[@"backgroundColor", @"opacity"]]; + nonAdditiveKeyPaths = [NSSet setWithArray:@[MDMKeyPathAnchorPoint, + MDMKeyPathBackgroundColor, + MDMKeyPathOpacity]]; }); return [nonAdditiveKeyPaths containsObject:keyPath]; diff --git a/src/private/MDMBlockAnimations.m b/src/private/MDMBlockAnimations.m index c0f2517..1e8ad0a 100644 --- a/src/private/MDMBlockAnimations.m +++ b/src/private/MDMBlockAnimations.m @@ -27,7 +27,8 @@ static NSSet *animatableKeyPaths = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - animatableKeyPaths = [NSSet setWithArray:@[MDMKeyPathBackgroundColor, + animatableKeyPaths = [NSSet setWithArray:@[MDMKeyPathAnchorPoint, + MDMKeyPathBackgroundColor, MDMKeyPathBorderWidth, MDMKeyPathBorderColor, MDMKeyPathBounds, diff --git a/tests/unit/MotionAnimatorBehavioralTests.swift b/tests/unit/MotionAnimatorBehavioralTests.swift index 796813c..9001b23 100644 --- a/tests/unit/MotionAnimatorBehavioralTests.swift +++ b/tests/unit/MotionAnimatorBehavioralTests.swift @@ -43,6 +43,7 @@ class AnimatorBehavioralTests: XCTestCase { } private let properties: [AnimatableKeyPath: Any] = [ + .anchorPoint: CGPoint(x: 0.2, y: 0.4), .backgroundColor: UIColor.blue, .borderWidth: 5, .borderColor: UIColor.red, diff --git a/tests/unit/QuartzCoreBehavioralTests.swift b/tests/unit/QuartzCoreBehavioralTests.swift index c1c0db2..b2f353b 100644 --- a/tests/unit/QuartzCoreBehavioralTests.swift +++ b/tests/unit/QuartzCoreBehavioralTests.swift @@ -69,6 +69,7 @@ class QuartzCoreBehavioralTests: XCTestCase { func testWhichPropertiesImplicitlyAnimateButNotAdditively() { let properties: [AnimatableKeyPath: Any] = [ + .anchorPoint: CGPoint(x: 0.2, y: 0.4), .borderWidth: 5, .borderColor: UIColor.red, .bounds: CGRect(x: 0, y: 0, width: 123, height: 456), diff --git a/tests/unit/UIKitBehavioralTests.swift b/tests/unit/UIKitBehavioralTests.swift index ad72581..c81325a 100644 --- a/tests/unit/UIKitBehavioralTests.swift +++ b/tests/unit/UIKitBehavioralTests.swift @@ -113,6 +113,7 @@ class UIKitBehavioralTests: XCTestCase { func testSomePropertiesImplicitlyAnimateButNotAdditively() { let baselineProperties: [AnimatableKeyPath: Any] = [ + .anchorPoint: CGPoint(x: 0.2, y: 0.4), .backgroundColor: UIColor.blue, .opacity: 0.5, ] diff --git a/tests/unit/UIKitEquivalencyTests.swift b/tests/unit/UIKitEquivalencyTests.swift index df8de42..3610053 100644 --- a/tests/unit/UIKitEquivalencyTests.swift +++ b/tests/unit/UIKitEquivalencyTests.swift @@ -100,6 +100,7 @@ class UIKitEquivalencyTests: XCTestCase { func testSomePropertiesImplicitlyAnimateButNotAdditively() { let baselineProperties: [AnimatableKeyPath: Any] = [ + .anchorPoint: CGPoint(x: 0.2, y: 0.4), .backgroundColor: UIColor.blue, .borderColor: UIColor.red, .opacity: 0.5, From c04fd1a5ad5ba910cb609d5edf52f1fa2b88826f Mon Sep 17 00:00:00 2001 From: featherless Date: Thu, 14 Dec 2017 16:40:11 -0500 Subject: [PATCH 16/22] Add a UIKit-ish tap to bounce example as a contrast to the traits example. (#100) --- ...e.swift => TapToBounceTraitsExample.swift} | 3 +- examples/TapToBounceUIKitExample.swift | 67 +++++++++++++++++++ .../project.pbxproj | 16 +++-- examples/apps/Catalog/TableOfContents.swift | 9 ++- 4 files changed, 86 insertions(+), 9 deletions(-) rename examples/{TapToBounceExample.swift => TapToBounceTraitsExample.swift} (93%) create mode 100644 examples/TapToBounceUIKitExample.swift diff --git a/examples/TapToBounceExample.swift b/examples/TapToBounceTraitsExample.swift similarity index 93% rename from examples/TapToBounceExample.swift rename to examples/TapToBounceTraitsExample.swift index 0974145..81dbfd9 100644 --- a/examples/TapToBounceExample.swift +++ b/examples/TapToBounceTraitsExample.swift @@ -17,7 +17,8 @@ import UIKit import MotionAnimator -class TapToBounceExampleViewController: UIViewController { +// This demo shows how to use animation traits to define the timings for an animation. +class TapToBounceTraitsExampleViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() diff --git a/examples/TapToBounceUIKitExample.swift b/examples/TapToBounceUIKitExample.swift new file mode 100644 index 0000000..bdd30d4 --- /dev/null +++ b/examples/TapToBounceUIKitExample.swift @@ -0,0 +1,67 @@ +/* + Copyright 2017-present The Material Motion Authors. All Rights Reserved. + + 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 UIKit +import MotionAnimator + +// This demo shows how to the MotionAnimator UIKit-ish APIs for animating properties and is provided +// as a contrast to the TapToBounceTraits example. +class TapToBounceUIKitExampleViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .white + + let circle = UIButton() + circle.bounds = CGRect(x: 0, y: 0, width: 128, height: 128) + circle.center = view.center + circle.layer.cornerRadius = circle.bounds.width / 2 + circle.backgroundColor = UIColor(red: (CGFloat)(0xEF) / 255.0, + green: (CGFloat)(0x88) / 255.0, + blue: (CGFloat)(0xAA) / 255.0, + alpha: 1) + view.addSubview(circle) + + circle.addTarget(self, action: #selector(didFocus), + for: [.touchDown, .touchDragEnter]) + circle.addTarget(self, action: #selector(didUnfocus), + for: [.touchUpInside, .touchUpOutside, .touchDragExit]) + } + + func didFocus(_ sender: UIButton) { + MotionAnimator.animate(withDuration: 0.8, + delay: 0, + usingSpringWithDamping: 0.5, + initialSpringVelocity: 0, + options: [], + animations: { + sender.transform = CGAffineTransform(scaleX: 1.5, y: 1.5) + }, completion: nil) + } + + func didUnfocus(_ sender: UIButton) { + MotionAnimator.animate(withDuration: 0.8, + delay: 0, + usingSpringWithDamping: 0.5, + initialSpringVelocity: 0, + options: [], + animations: { + sender.transform = .identity + }, completion: nil) + } +} + diff --git a/examples/apps/Catalog/MotionAnimatorCatalog.xcodeproj/project.pbxproj b/examples/apps/Catalog/MotionAnimatorCatalog.xcodeproj/project.pbxproj index fef005f..9ee6750 100644 --- a/examples/apps/Catalog/MotionAnimatorCatalog.xcodeproj/project.pbxproj +++ b/examples/apps/Catalog/MotionAnimatorCatalog.xcodeproj/project.pbxproj @@ -8,9 +8,10 @@ /* Begin PBXBuildFile section */ 2AA864EDA683CEF5FAA721BE /* Pods_UnitTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2DBE814C7B88BAD6337052DB /* Pods_UnitTests.framework */; }; - 660248A41FD1B923004C0147 /* TapToBounceExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 660248A31FD1B923004C0147 /* TapToBounceExample.swift */; }; 660636021FACC24300C3DFB8 /* TimeScaleFactorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 660636011FACC24300C3DFB8 /* TimeScaleFactorTests.swift */; }; 6625876C1FB4DB9C00BC7DF1 /* InitialVelocityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6625876B1FB4DB9C00BC7DF1 /* InitialVelocityTests.swift */; }; + 6635BDB61FE3233500CDCB69 /* TapToBounceTraitsExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6635BDB41FE3233500CDCB69 /* TapToBounceTraitsExample.swift */; }; + 6635BDB71FE3233500CDCB69 /* TapToBounceUIKitExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6635BDB51FE3233500CDCB69 /* TapToBounceUIKitExample.swift */; }; 664F59941FCCE27E002EC56D /* UIKitBehavioralTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 664F59931FCCE27E002EC56D /* UIKitBehavioralTests.swift */; }; 664F59961FCDB2E6002EC56D /* QuartzCoreBehavioralTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 664F59951FCDB2E5002EC56D /* QuartzCoreBehavioralTests.swift */; }; 664F59981FCE5CE2002EC56D /* BeginFromCurrentStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 664F59971FCE5CE2002EC56D /* BeginFromCurrentStateTests.swift */; }; @@ -24,8 +25,8 @@ 667A3F4C1DEE269400CB3A99 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 667A3F4A1DEE269400CB3A99 /* LaunchScreen.storyboard */; }; 667A3F541DEE273000CB3A99 /* TableOfContents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 667A3F531DEE273000CB3A99 /* TableOfContents.swift */; }; 6687264A1EF04B4C00113675 /* MotionAnimatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 668726491EF04B4C00113675 /* MotionAnimatorTests.swift */; }; - 668819FA1FE2EB36003A9420 /* UIKitEquivalencyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 668819F91FE2EB36003A9420 /* UIKitEquivalencyTests.swift */; }; 668819F81FE2E5C6003A9420 /* SpringTimingCurveTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 668819F71FE2E5C6003A9420 /* SpringTimingCurveTests.swift */; }; + 668819FA1FE2EB36003A9420 /* UIKitEquivalencyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 668819F91FE2EB36003A9420 /* UIKitEquivalencyTests.swift */; }; 669B6CA91FD0547100B80B76 /* MotionAnimatorBehavioralTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669B6CA81FD0547100B80B76 /* MotionAnimatorBehavioralTests.swift */; }; 66A6A6681FBA158000DE54CB /* AnimationRemovalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66A6A6671FBA158000DE54CB /* AnimationRemovalTests.swift */; }; 66BF5A8F1FB0E4CB00E864F6 /* ImplicitAnimationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66BF5A8E1FB0E4CB00E864F6 /* ImplicitAnimationTests.swift */; }; @@ -58,9 +59,10 @@ 2DBE814C7B88BAD6337052DB /* Pods_UnitTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_UnitTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 50D808A6F9E944D54276D32F /* Pods_MotionAnimatorCatalog.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MotionAnimatorCatalog.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 52820916F8FAA40E942A7333 /* Pods-UnitTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-UnitTests.release.xcconfig"; path = "../../../Pods/Target Support Files/Pods-UnitTests/Pods-UnitTests.release.xcconfig"; sourceTree = ""; }; - 660248A31FD1B923004C0147 /* TapToBounceExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TapToBounceExample.swift; sourceTree = ""; }; 660636011FACC24300C3DFB8 /* TimeScaleFactorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeScaleFactorTests.swift; sourceTree = ""; }; 6625876B1FB4DB9C00BC7DF1 /* InitialVelocityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InitialVelocityTests.swift; sourceTree = ""; }; + 6635BDB41FE3233500CDCB69 /* TapToBounceTraitsExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TapToBounceTraitsExample.swift; sourceTree = ""; }; + 6635BDB51FE3233500CDCB69 /* TapToBounceUIKitExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TapToBounceUIKitExample.swift; sourceTree = ""; }; 664F59931FCCE27E002EC56D /* UIKitBehavioralTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitBehavioralTests.swift; sourceTree = ""; }; 664F59951FCDB2E5002EC56D /* QuartzCoreBehavioralTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QuartzCoreBehavioralTests.swift; sourceTree = ""; }; 664F59971FCE5CE2002EC56D /* BeginFromCurrentStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeginFromCurrentStateTests.swift; sourceTree = ""; }; @@ -80,8 +82,8 @@ 667A3F4D1DEE269400CB3A99 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 667A3F531DEE273000CB3A99 /* TableOfContents.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableOfContents.swift; sourceTree = ""; }; 668726491EF04B4C00113675 /* MotionAnimatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MotionAnimatorTests.swift; sourceTree = ""; }; - 668819F91FE2EB36003A9420 /* UIKitEquivalencyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIKitEquivalencyTests.swift; sourceTree = ""; }; 668819F71FE2E5C6003A9420 /* SpringTimingCurveTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpringTimingCurveTests.swift; sourceTree = ""; }; + 668819F91FE2EB36003A9420 /* UIKitEquivalencyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIKitEquivalencyTests.swift; sourceTree = ""; }; 669B6CA81FD0547100B80B76 /* MotionAnimatorBehavioralTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MotionAnimatorBehavioralTests.swift; sourceTree = ""; }; 66A6A6671FBA158000DE54CB /* AnimationRemovalTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimationRemovalTests.swift; sourceTree = ""; }; 66BF5A8E1FB0E4CB00E864F6 /* ImplicitAnimationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImplicitAnimationTests.swift; sourceTree = ""; }; @@ -177,7 +179,8 @@ 66DD4BF41EEF0ECB00207119 /* CalendarCardExpansionExample.m */, 66DD4BF61EEF1C4B00207119 /* CalendarChipMotionSpec.h */, 66DD4BF71EEF1C4B00207119 /* CalendarChipMotionSpec.m */, - 660248A31FD1B923004C0147 /* TapToBounceExample.swift */, + 6635BDB41FE3233500CDCB69 /* TapToBounceTraitsExample.swift */, + 6635BDB51FE3233500CDCB69 /* TapToBounceUIKitExample.swift */, ); name = examples; path = ../..; @@ -514,10 +517,11 @@ buildActionMask = 2147483647; files = ( 666FAA841D384A6B000363DA /* AppDelegate.swift in Sources */, + 6635BDB71FE3233500CDCB69 /* TapToBounceUIKitExample.swift in Sources */, 667A3F541DEE273000CB3A99 /* TableOfContents.swift in Sources */, + 6635BDB61FE3233500CDCB69 /* TapToBounceTraitsExample.swift in Sources */, 66DD4BF81EEF1C4B00207119 /* CalendarChipMotionSpec.m in Sources */, 66DD4BF51EEF0ECB00207119 /* CalendarCardExpansionExample.m in Sources */, - 660248A41FD1B923004C0147 /* TapToBounceExample.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/examples/apps/Catalog/TableOfContents.swift b/examples/apps/Catalog/TableOfContents.swift index 9b99ab5..605acfd 100644 --- a/examples/apps/Catalog/TableOfContents.swift +++ b/examples/apps/Catalog/TableOfContents.swift @@ -16,9 +16,14 @@ // MARK: Catalog by convention -extension TapToBounceExampleViewController { +extension TapToBounceTraitsExampleViewController { class func catalogBreadcrumbs() -> [String] { - return ["Tap to bounce"] + return ["Tap to bounce (Traits)"] } } +extension TapToBounceUIKitExampleViewController { + class func catalogBreadcrumbs() -> [String] { + return ["Tap to bounce (UIKit)"] + } +} From e48cc459f281afde1ab2ae2c985b4fb64555c0f9 Mon Sep 17 00:00:00 2001 From: featherless Date: Thu, 14 Dec 2017 17:27:47 -0500 Subject: [PATCH 17/22] Animate the border as well to demonstrate that we can animate CALayer properties with the animator. (#101) --- examples/TapToBounceTraitsExample.swift | 9 +++++++++ examples/TapToBounceUIKitExample.swift | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/examples/TapToBounceTraitsExample.swift b/examples/TapToBounceTraitsExample.swift index 81dbfd9..cf86814 100644 --- a/examples/TapToBounceTraitsExample.swift +++ b/examples/TapToBounceTraitsExample.swift @@ -29,6 +29,10 @@ class TapToBounceTraitsExampleViewController: UIViewController { circle.bounds = CGRect(x: 0, y: 0, width: 128, height: 128) circle.center = view.center circle.layer.cornerRadius = circle.bounds.width / 2 + circle.layer.borderColor = UIColor(red: (CGFloat)(0x88) / 255.0, + green: (CGFloat)(0xEF) / 255.0, + blue: (CGFloat)(0xAA) / 255.0, + alpha: 1).cgColor circle.backgroundColor = UIColor(red: (CGFloat)(0xEF) / 255.0, green: (CGFloat)(0x88) / 255.0, blue: (CGFloat)(0xAA) / 255.0, @@ -51,6 +55,10 @@ class TapToBounceTraitsExampleViewController: UIViewController { let animator = MotionAnimator() animator.animate(with: traits) { sender.transform = CGAffineTransform(scaleX: 1.5, y: 1.5) + + // This would normally not be animatable with the UIView animation APIs, but it is animatable + // with the motion animator. + sender.layer.borderWidth = 10 } } @@ -58,6 +66,7 @@ class TapToBounceTraitsExampleViewController: UIViewController { let animator = MotionAnimator() animator.animate(with: traits) { sender.transform = .identity + sender.layer.borderWidth = 0 } } } diff --git a/examples/TapToBounceUIKitExample.swift b/examples/TapToBounceUIKitExample.swift index bdd30d4..95ff44b 100644 --- a/examples/TapToBounceUIKitExample.swift +++ b/examples/TapToBounceUIKitExample.swift @@ -30,6 +30,10 @@ class TapToBounceUIKitExampleViewController: UIViewController { circle.bounds = CGRect(x: 0, y: 0, width: 128, height: 128) circle.center = view.center circle.layer.cornerRadius = circle.bounds.width / 2 + circle.layer.borderColor = UIColor(red: (CGFloat)(0x88) / 255.0, + green: (CGFloat)(0xEF) / 255.0, + blue: (CGFloat)(0xAA) / 255.0, + alpha: 1).cgColor circle.backgroundColor = UIColor(red: (CGFloat)(0xEF) / 255.0, green: (CGFloat)(0x88) / 255.0, blue: (CGFloat)(0xAA) / 255.0, @@ -50,6 +54,10 @@ class TapToBounceUIKitExampleViewController: UIViewController { options: [], animations: { sender.transform = CGAffineTransform(scaleX: 1.5, y: 1.5) + + // This would normally not be animatable with the UIView animation APIs, but it is animatable + // with the motion animator. + sender.layer.borderWidth = 10 }, completion: nil) } @@ -61,6 +69,7 @@ class TapToBounceUIKitExampleViewController: UIViewController { options: [], animations: { sender.transform = .identity + sender.layer.borderWidth = 0 }, completion: nil) } } From a56cd92874440b975591e421eaad8d4ef28cb9d4 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Sat, 16 Dec 2017 12:35:46 -0500 Subject: [PATCH 18/22] Anchor point became animatable on iOS 11. --- tests/unit/UIKitBehavioralTests.swift | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/unit/UIKitBehavioralTests.swift b/tests/unit/UIKitBehavioralTests.swift index c81325a..19d1d32 100644 --- a/tests/unit/UIKitBehavioralTests.swift +++ b/tests/unit/UIKitBehavioralTests.swift @@ -113,11 +113,19 @@ class UIKitBehavioralTests: XCTestCase { func testSomePropertiesImplicitlyAnimateButNotAdditively() { let baselineProperties: [AnimatableKeyPath: Any] = [ - .anchorPoint: CGPoint(x: 0.2, y: 0.4), .backgroundColor: UIColor.blue, .opacity: 0.5, ] - for (keyPath, value) in baselineProperties { + let properties: [AnimatableKeyPath: Any] + if #available(iOS 11.0, *) { + // Anchor point became implicitly animatable in iOS 11. + var baselineWithCornerRadiusProperties = baselineProperties + baselineWithCornerRadiusProperties[.anchorPoint] = CGPoint(x: 0.2, y: 0.4) + properties = baselineWithCornerRadiusProperties + } else { + properties = baselineProperties + } + for (keyPath, value) in properties { rebuildView() UIView.animate(withDuration: 0.01) { @@ -139,6 +147,7 @@ class UIKitBehavioralTests: XCTestCase { func testSomePropertiesDoNotImplicitlyAnimate() { let baselineProperties: [AnimatableKeyPath: Any] = [ + .anchorPoint: CGPoint(x: 0.2, y: 0.4), .borderWidth: 5, .borderColor: UIColor.red, .cornerRadius: 3, @@ -155,6 +164,7 @@ class UIKitBehavioralTests: XCTestCase { if #available(iOS 11.0, *) { // Corner radius became implicitly animatable in iOS 11. var baselineWithOutCornerRadius = baselineProperties + baselineWithOutCornerRadius.removeValue(forKey: .anchorPoint) baselineWithOutCornerRadius.removeValue(forKey: .cornerRadius) properties = baselineWithOutCornerRadius From 042eb8cb46c077121f817d5e8ebc7b51d4ec54b9 Mon Sep 17 00:00:00 2001 From: Louis Romero Date: Tue, 19 Dec 2017 13:12:49 +0100 Subject: [PATCH 19/22] Add IS_BAZEL_BUILD around MotionInterchange import (#103) --- src/private/CABasicAnimation+MotionAnimator.h | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/private/CABasicAnimation+MotionAnimator.h b/src/private/CABasicAnimation+MotionAnimator.h index c3a29f6..462a4b9 100644 --- a/src/private/CABasicAnimation+MotionAnimator.h +++ b/src/private/CABasicAnimation+MotionAnimator.h @@ -14,12 +14,16 @@ limitations under the License. */ -#import "MotionInterchange.h" - #import #import #import +#ifdef IS_BAZEL_BUILD +#import "MotionInterchange.h" +#else +#import +#endif + // Returns a basic animation configured with the provided traits and scale factor. FOUNDATION_EXPORT CABasicAnimation *MDMAnimationFromTraits(MDMAnimationTraits *traits, CGFloat timeScaleFactor); From 4177b1ae4f5a61e5214627fa5e923cc97fba7c29 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Tue, 19 Dec 2017 14:01:13 -0500 Subject: [PATCH 20/22] Automatic changelog preparation for release. --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 510f79d..07370b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# #develop# + + TODO: Enumerate changes. + + # 2.7.0 This minor release introduces support for the new [v1.5.0](https://github.com/material-motion/motion-interchange-objc/releases/tag/v1.5.0) MotionInterchange format. From 817c288234395f0de30fa149dfbf5ede714ccc2e Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Tue, 19 Dec 2017 14:09:20 -0500 Subject: [PATCH 21/22] Update the changelog. --- CHANGELOG.md | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07370b5..4e18d33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,67 @@ -# #develop# +# 2.8.0 - TODO: Enumerate changes. +This minor release introduces support for animating more key paths and support for drop-in UIView animation API replacements. +## New features + +The MotionAnimator can now implicitly animate the following CALayer properties: `anchorPoint`, `borderWidth`, `borderColor`, `shadowColor`, and `zPosition`. + +There are now UIKit equivalency APIs that can be used as drop-in replacements for existing UIView animation code. + +## Source changes + +* [Add IS_BAZEL_BUILD around MotionInterchange import (#103)](https://github.com/material-motion/motion-animator-objc/commit/042eb8cb46c077121f817d5e8ebc7b51d4ec54b9) (Louis Romero) +* [Anchor point became animatable on iOS 11.](https://github.com/material-motion/motion-animator-objc/commit/a56cd92874440b975591e421eaad8d4ef28cb9d4) (Jeff Verkoeyen) +* [Add support for animating anchorPoint. (#97)](https://github.com/material-motion/motion-animator-objc/commit/646b6f6fd1ed3a10dae7ab4e98caa43cad65f2ff) (featherless) +* [Add support for animating shadow color. (#99)](https://github.com/material-motion/motion-animator-objc/commit/ca896623a4e5a42a458e6fd59c8f405b55e87e26) (featherless) +* [Add support for animating border width and color. (#98)](https://github.com/material-motion/motion-animator-objc/commit/55c23d5c4f8029cc45f6331a894ddf1960863ba7) (featherless) +* [Add support for animating z position. (#96)](https://github.com/material-motion/motion-animator-objc/commit/23cd380bf2414b2834da7f8d55548cd00166d6f8) (featherless) +* [Add support for additively animating bounds. (#93)](https://github.com/material-motion/motion-animator-objc/commit/32c78d4d93b6c82ff55693c54176b2826f718e5e) (featherless) +* [Improve the documentation for initial velocity. (#94)](https://github.com/material-motion/motion-animator-objc/commit/e260418023430e0541360ec46ed6d4d931dbf05c) (featherless) +* [Standardize our param docs formatting. (#95)](https://github.com/material-motion/motion-animator-objc/commit/024296aca338b106cf49b87fcc83b9feb216ee9d) (featherless) +* [Add back test properties that were accidentally removed in 69469aedb987e516ff1f43a123b3ee29dfef38ca.](https://github.com/material-motion/motion-animator-objc/commit/e41ccb4890b9ed3bad5ab52015f8224adb5bbba6) (Jeff Verkoeyen) +* [Add support for using a spring generator as a timing curve. (#91)](https://github.com/material-motion/motion-animator-objc/commit/46fd517e18b3a9555e46dcdd942601dd4ccd5149) (featherless) +* [Throw an assertion when an unrecognized timing curve is provided. (#92)](https://github.com/material-motion/motion-animator-objc/commit/1e76e2ba8fe9bc08d623a55623f5fd40579d0287) (featherless) +* [Add UIKit equivalent APIs for animating implicitly. (#90)](https://github.com/material-motion/motion-animator-objc/commit/69469aedb987e516ff1f43a123b3ee29dfef38ca) (featherless) + +## API changes + +Auto-generated by running: + + apidiff origin/stable release-candidate objc src/MotionAnimator.h + +#### Animatable key paths + +*new* constant: `MDMKeyPathAnchorPoint` + +*new* constant: `MDMKeyPathBorderWidth` + +*new* constant: `MDMKeyPathBorderColor` + +*new* constant: `MDMKeyPathShadowColor` + +*new* constant: `MDMKeyPathZ` + +#### MDMMotionAnimator(UIKitEquivalency) + +*new* class method: `+animateWithDuration:delay:options:animations:completion:` in `MDMMotionAnimator(UIKitEquivalency)` + +*new* class method: `+animateWithDuration:animations:completion:` in `MDMMotionAnimator(UIKitEquivalency)` + +*new* class method: `+animateWithDuration:animations:` in `MDMMotionAnimator(UIKitEquivalency)` + +*new* class method: `+animateWithDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion:` in `MDMMotionAnimator(UIKitEquivalency)` + +## Non-source changes + +* [Animate the border as well to demonstrate that we can animate CALayer properties with the animator. (#101)](https://github.com/material-motion/motion-animator-objc/commit/e48cc459f281afde1ab2ae2c985b4fb64555c0f9) (featherless) +* [Add a UIKit-ish tap to bounce example as a contrast to the traits example. (#100)](https://github.com/material-motion/motion-animator-objc/commit/c04fd1a5ad5ba910cb609d5edf52f1fa2b88826f) (featherless) +* [Wording order.](https://github.com/material-motion/motion-animator-objc/commit/4bf33e213af26511e0aca40f42ebceb0a077076a) (Jeff Verkoeyen) +* [Min SDK support.](https://github.com/material-motion/motion-animator-objc/commit/0ca2e7c7e154da62fac508d6f8c21f1ff3b37c02) (Jeff Verkoeyen) +* [Fix the banner url.](https://github.com/material-motion/motion-animator-objc/commit/966ae6769288e4913219c7de156b846af5789d96) (Jeff Verkoeyen) +* [Add banner and drop most of the preamble docs in preparation for the new readme.](https://github.com/material-motion/motion-animator-objc/commit/cf183c788fba2fef2e144674d412da67d5adc438) (Jeff Verkoeyen) + +--- # 2.7.0 From fa35516336d71c9f350e141c0336e3c6fcd84917 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Tue, 19 Dec 2017 14:10:09 -0500 Subject: [PATCH 22/22] Bump the release. --- .jazzy.yaml | 4 ++-- MotionAnimator.podspec | 2 +- Podfile.lock | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.jazzy.yaml b/.jazzy.yaml index 3f0fa00..39bf2ae 100644 --- a/.jazzy.yaml +++ b/.jazzy.yaml @@ -1,7 +1,7 @@ module: MotionAnimator -module_version: 2.7.0 +module_version: 2.8.0 sdk: iphonesimulator umbrella_header: src/MotionAnimator.h objc: true github_url: https://github.com/material-motion/motion-animator-objc -github_file_prefix: https://github.com/material-motion/motion-animator-objc/tree/v2.7.0 +github_file_prefix: https://github.com/material-motion/motion-animator-objc/tree/v2.8.0 diff --git a/MotionAnimator.podspec b/MotionAnimator.podspec index 23fda17..eaf7bfd 100644 --- a/MotionAnimator.podspec +++ b/MotionAnimator.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "MotionAnimator" s.summary = "A Motion Animator creates performant, interruptible animations from motion specs." - s.version = "2.7.0" + s.version = "2.8.0" s.authors = "The Material Motion Authors" s.license = "Apache 2.0" s.homepage = "https://github.com/material-motion/motion-animator-objc" diff --git a/Podfile.lock b/Podfile.lock index 0b4e1d6..3cac659 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,6 +1,6 @@ PODS: - CatalogByConvention (2.2.0) - - MotionAnimator (2.7.0): + - MotionAnimator (2.8.0): - MotionInterchange (~> 1.6) - MotionInterchange (1.6.0) @@ -14,7 +14,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: CatalogByConvention: 5df5831e48b8083b18570dcb804f20fd1c90694f - MotionAnimator: fe012f4b344f091f95a621b0d0a97c4e2ea1c525 + MotionAnimator: 8af077dac084b7880a4d2ddae31a26171087bd87 MotionInterchange: ead0e3ae1f3a5fb539e289debbc7ae036160a10d PODFILE CHECKSUM: 3537bf01c11174928ac008c20fec4738722e96f3