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

Commit

Permalink
Add new APIs for implicit animations. (#30)
Browse files Browse the repository at this point in the history
* Add new APIs for implicit animations.

* Docs.

* Add docs.

* Docs.

* Cleanup.

* Remove strong.

* Return copy.

* Docs and rework.

* Add missing header.

* More tests.
  • Loading branch information
jverkoey authored Nov 6, 2017
1 parent 87c7a5c commit 1793979
Show file tree
Hide file tree
Showing 6 changed files with 398 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,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 */; };
66BF5A8F1FB0E4CB00E864F6 /* ImplicitAnimationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66BF5A8E1FB0E4CB00E864F6 /* ImplicitAnimationTests.swift */; };
66DD4BF51EEF0ECB00207119 /* CalendarCardExpansionExample.m in Sources */ = {isa = PBXBuildFile; fileRef = 66DD4BF41EEF0ECB00207119 /* CalendarCardExpansionExample.m */; };
66DD4BF81EEF1C4B00207119 /* CalendarChipMotionSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 66DD4BF71EEF1C4B00207119 /* CalendarChipMotionSpec.m */; };
66FD99FA1EE9FBBE00C53A82 /* MotionAnimatorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 66FD99F91EE9FBBE00C53A82 /* MotionAnimatorTests.m */; };
Expand Down Expand Up @@ -59,6 +60,7 @@
667A3F4D1DEE269400CB3A99 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
667A3F531DEE273000CB3A99 /* TableOfContents.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableOfContents.swift; sourceTree = "<group>"; };
668726491EF04B4C00113675 /* MotionAnimatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MotionAnimatorTests.swift; sourceTree = "<group>"; };
66BF5A8E1FB0E4CB00E864F6 /* ImplicitAnimationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImplicitAnimationTests.swift; sourceTree = "<group>"; };
66DD4BF31EEF0ECB00207119 /* CalendarCardExpansionExample.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CalendarCardExpansionExample.h; sourceTree = "<group>"; };
66DD4BF41EEF0ECB00207119 /* CalendarCardExpansionExample.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CalendarCardExpansionExample.m; sourceTree = "<group>"; };
66DD4BF61EEF1C4B00207119 /* CalendarChipMotionSpec.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CalendarChipMotionSpec.h; sourceTree = "<group>"; };
Expand Down Expand Up @@ -206,6 +208,7 @@
children = (
66FD99F91EE9FBBE00C53A82 /* MotionAnimatorTests.m */,
668726491EF04B4C00113675 /* MotionAnimatorTests.swift */,
66BF5A8E1FB0E4CB00E864F6 /* ImplicitAnimationTests.swift */,
660636011FACC24300C3DFB8 /* TimeScaleFactorTests.swift */,
);
path = unit;
Expand Down Expand Up @@ -483,6 +486,7 @@
buildActionMask = 2147483647;
files = (
660636021FACC24300C3DFB8 /* TimeScaleFactorTests.swift in Sources */,
66BF5A8F1FB0E4CB00E864F6 /* ImplicitAnimationTests.swift in Sources */,
6687264A1EF04B4C00113675 /* MotionAnimatorTests.swift in Sources */,
66FD99FA1EE9FBBE00C53A82 /* MotionAnimatorTests.m in Sources */,
);
Expand Down
25 changes: 25 additions & 0 deletions src/MDMMotionAnimator.h
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,29 @@ NS_SWIFT_NAME(MotionAnimator)
keyPath:(nonnull MDMAnimatableKeyPath)keyPath
completion:(nullable void(^)(void))completion;

/**
Performs `animations` using the timing provided.
@param timing The timing 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 timing. The block is
non-escaping.
*/
- (void)animateWithTiming:(MDMMotionTiming)timing animations:(nonnull void(^)(void))animations;

/**
Performs `animations` using the timing provided and executes the completion handler once all added
animations have completed.
@param timing The timing 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 timing. The block is
non-escaping.
@param completion The completion handler will be executed once all added animations have come to
rest. The block is escaping and will be released once the animations have completed.
*/
- (void)animateWithTiming:(MDMMotionTiming)timing
animations:(nonnull void (^)(void))animations
completion:(nullable void(^)(void))completion;

@end
24 changes: 24 additions & 0 deletions src/MDMMotionAnimator.m
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#import "CATransaction+MotionAnimator.h"
#import "private/CABasicAnimation+MotionAnimator.h"
#import "private/MDMUIKitValueCoercion.h"
#import "private/MDMBlockAnimations.h"
#import "private/MDMDragCoefficient.h"

@implementation MDMMotionAnimator {
Expand Down Expand Up @@ -120,6 +121,29 @@ - (void)animateWithTiming:(MDMMotionTiming)timing
[CATransaction commit];
}

- (void)animateWithTiming:(MDMMotionTiming)timing animations:(void (^)(void))animations {
[self animateWithTiming:timing animations:animations completion:nil];
}

- (void)animateWithTiming:(MDMMotionTiming)timing
animations:(void (^)(void))animations
completion:(void(^)(void))completion {
NSArray<MDMImplicitAction *> *actions = MDMAnimateImplicitly(animations);

[CATransaction begin];
[CATransaction setCompletionBlock:completion];

for (MDMImplicitAction *action in actions) {
id currentValue = [action.layer valueForKeyPath:action.keyPath];
[self animateWithTiming:timing
toLayer:action.layer
withValues:@[action.initialValue, currentValue]
keyPath:action.keyPath];
}

[CATransaction commit];
}

- (void)addCoreAnimationTracer:(void (^)(CALayer *, CAAnimation *))tracer {
if (!_tracers) {
_tracers = [NSMutableArray array];
Expand Down
26 changes: 26 additions & 0 deletions src/private/MDMBlockAnimations.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
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 <Foundation/Foundation.h>
#import <QuartzCore/QuartzCore.h>

@interface MDMImplicitAction: NSObject
@property(nonatomic, strong, readonly) id initialValue;
@property(nonatomic, copy, readonly) NSString *keyPath;
@property(nonatomic, strong, readonly) CALayer *layer;
@end

NSArray<MDMImplicitAction *> *MDMAnimateImplicitly(void (^animations)(void));
132 changes: 132 additions & 0 deletions src/private/MDMBlockAnimations.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*
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 "MDMBlockAnimations.h"

#import <UIKit/UIKit.h>
#import <objc/runtime.h>

static IMP sOriginalActionForLayerImp = NULL;

@interface MDMActionContext: NSObject
@property(nonatomic, readonly) NSArray<MDMImplicitAction *> *interceptedActions;
@end

@implementation MDMImplicitAction

- (instancetype)initWithLayer:(CALayer *)layer
keyPath:(NSString *)keyPath
initialValue:(id)initialValue {
self = [super init];
if (self) {
_layer = layer;
_keyPath = [keyPath copy];
_initialValue = initialValue;
}
return self;
}

@end

@implementation MDMActionContext {
NSMutableArray<MDMImplicitAction *> *_interceptedActions;
}

- (instancetype)init {
self = [super init];
if (self) {
_interceptedActions = [NSMutableArray array];
}
return self;
}

- (void)addActionForLayer:(CALayer *)layer
keyPath:(NSString *)keyPath
withInitialValue:(id)initialValue {
[_interceptedActions addObject:[[MDMImplicitAction alloc] initWithLayer:layer
keyPath:keyPath
initialValue:initialValue]];
}

- (NSArray<MDMImplicitAction *> *)interceptedActions {
return [_interceptedActions copy];
}

@end

static NSMutableArray *sActionContext = nil;

static id<CAAction> ActionForLayer(id self, SEL _cmd, CALayer *layer, NSString *event) {
NSCAssert([NSStringFromSelector(_cmd) isEqualToString:
NSStringFromSelector(@selector(actionForLayer:forKey:))],
@"Invalid method signature.");

MDMActionContext *context = [sActionContext lastObject];
NSCAssert(context != nil, @"MotionAnimator action method invoked out of implicit scope.");

if (context == nil) {
// Graceful handling of invalid state on non-debug builds for if our context is nil invokes our
// original implementation:
return ((id<CAAction>(*)(id, SEL, CALayer *, NSString *))sOriginalActionForLayerImp)
(self, _cmd, layer, event);
}

// We don't have access to the "to" value of our animation here, so we unfortunately can't
// calculate additive values if the animator is configured as such. So, to support additive
// animations, we queue up the modified actions and then add them all at the end of our
// MDMAnimateBlock invocation.
id initialValue = [layer valueForKeyPath:event];
[context addActionForLayer:layer keyPath:event withInitialValue:initialValue];
return [NSNull null];
}

NSArray<MDMImplicitAction *> *MDMAnimateImplicitly(void (^work)(void)) {
if (!work) {
return nil;
}

// This method can be called recursively, so we maintain a recursive context stack in the scope of
// this method. Note that this is absolutely not thread safe, but neither is Core Animation.
if (!sActionContext) {
sActionContext = [NSMutableArray array];
}
[sActionContext addObject:[[MDMActionContext alloc] init]];

SEL selector = @selector(actionForLayer:forKey:);
Method method = class_getInstanceMethod([UIView class], selector);

if (sOriginalActionForLayerImp == nil) {
// Swap the original UIView implementation with our own so that we can intercept all
// actionForLayer:forKey: events. All events will be
sOriginalActionForLayerImp = method_setImplementation(method, (IMP)ActionForLayer);
}

work();

// Return any intercepted actions we received during the invocation of work.
MDMActionContext *context = [sActionContext lastObject];
[sActionContext removeLastObject];

if ([sActionContext count] == 0) {
// Restore our original method if we've emptied the stack:
method_setImplementation(method, sOriginalActionForLayerImp);

sOriginalActionForLayerImp = nil;
sActionContext = nil;
}

return context.interceptedActions;
}
Loading

0 comments on commit 1793979

Please sign in to comment.