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

Commit 1793979

Browse files
authored
Add new APIs for implicit animations. (#30)
* Add new APIs for implicit animations. * Docs. * Add docs. * Docs. * Cleanup. * Remove strong. * Return copy. * Docs and rework. * Add missing header. * More tests.
1 parent 87c7a5c commit 1793979

File tree

6 files changed

+398
-0
lines changed

6 files changed

+398
-0
lines changed

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

+4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
667A3F4C1DEE269400CB3A99 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 667A3F4A1DEE269400CB3A99 /* LaunchScreen.storyboard */; };
1818
667A3F541DEE273000CB3A99 /* TableOfContents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 667A3F531DEE273000CB3A99 /* TableOfContents.swift */; };
1919
6687264A1EF04B4C00113675 /* MotionAnimatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 668726491EF04B4C00113675 /* MotionAnimatorTests.swift */; };
20+
66BF5A8F1FB0E4CB00E864F6 /* ImplicitAnimationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66BF5A8E1FB0E4CB00E864F6 /* ImplicitAnimationTests.swift */; };
2021
66DD4BF51EEF0ECB00207119 /* CalendarCardExpansionExample.m in Sources */ = {isa = PBXBuildFile; fileRef = 66DD4BF41EEF0ECB00207119 /* CalendarCardExpansionExample.m */; };
2122
66DD4BF81EEF1C4B00207119 /* CalendarChipMotionSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 66DD4BF71EEF1C4B00207119 /* CalendarChipMotionSpec.m */; };
2223
66FD99FA1EE9FBBE00C53A82 /* MotionAnimatorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 66FD99F91EE9FBBE00C53A82 /* MotionAnimatorTests.m */; };
@@ -59,6 +60,7 @@
5960
667A3F4D1DEE269400CB3A99 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
6061
667A3F531DEE273000CB3A99 /* TableOfContents.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableOfContents.swift; sourceTree = "<group>"; };
6162
668726491EF04B4C00113675 /* MotionAnimatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MotionAnimatorTests.swift; sourceTree = "<group>"; };
63+
66BF5A8E1FB0E4CB00E864F6 /* ImplicitAnimationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImplicitAnimationTests.swift; sourceTree = "<group>"; };
6264
66DD4BF31EEF0ECB00207119 /* CalendarCardExpansionExample.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CalendarCardExpansionExample.h; sourceTree = "<group>"; };
6365
66DD4BF41EEF0ECB00207119 /* CalendarCardExpansionExample.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CalendarCardExpansionExample.m; sourceTree = "<group>"; };
6466
66DD4BF61EEF1C4B00207119 /* CalendarChipMotionSpec.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CalendarChipMotionSpec.h; sourceTree = "<group>"; };
@@ -206,6 +208,7 @@
206208
children = (
207209
66FD99F91EE9FBBE00C53A82 /* MotionAnimatorTests.m */,
208210
668726491EF04B4C00113675 /* MotionAnimatorTests.swift */,
211+
66BF5A8E1FB0E4CB00E864F6 /* ImplicitAnimationTests.swift */,
209212
660636011FACC24300C3DFB8 /* TimeScaleFactorTests.swift */,
210213
);
211214
path = unit;
@@ -483,6 +486,7 @@
483486
buildActionMask = 2147483647;
484487
files = (
485488
660636021FACC24300C3DFB8 /* TimeScaleFactorTests.swift in Sources */,
489+
66BF5A8F1FB0E4CB00E864F6 /* ImplicitAnimationTests.swift in Sources */,
486490
6687264A1EF04B4C00113675 /* MotionAnimatorTests.swift in Sources */,
487491
66FD99FA1EE9FBBE00C53A82 /* MotionAnimatorTests.m in Sources */,
488492
);

src/MDMMotionAnimator.h

+25
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,29 @@ NS_SWIFT_NAME(MotionAnimator)
103103
keyPath:(nonnull MDMAnimatableKeyPath)keyPath
104104
completion:(nullable void(^)(void))completion;
105105

106+
/**
107+
Performs `animations` using the timing provided.
108+
109+
@param timing The timing to be used for the animation.
110+
@param animations The block to be executed. Any animatable properties changed within this block
111+
will result in animations being added to the view's layer with the provided timing. The block is
112+
non-escaping.
113+
*/
114+
- (void)animateWithTiming:(MDMMotionTiming)timing animations:(nonnull void(^)(void))animations;
115+
116+
/**
117+
Performs `animations` using the timing provided and executes the completion handler once all added
118+
animations have completed.
119+
120+
@param timing The timing to be used for the animation.
121+
@param animations The block to be executed. Any animatable properties changed within this block
122+
will result in animations being added to the view's layer with the provided timing. The block is
123+
non-escaping.
124+
@param completion The completion handler will be executed once all added animations have come to
125+
rest. The block is escaping and will be released once the animations have completed.
126+
*/
127+
- (void)animateWithTiming:(MDMMotionTiming)timing
128+
animations:(nonnull void (^)(void))animations
129+
completion:(nullable void(^)(void))completion;
130+
106131
@end

src/MDMMotionAnimator.m

+24
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#import "CATransaction+MotionAnimator.h"
2222
#import "private/CABasicAnimation+MotionAnimator.h"
2323
#import "private/MDMUIKitValueCoercion.h"
24+
#import "private/MDMBlockAnimations.h"
2425
#import "private/MDMDragCoefficient.h"
2526

2627
@implementation MDMMotionAnimator {
@@ -120,6 +121,29 @@ - (void)animateWithTiming:(MDMMotionTiming)timing
120121
[CATransaction commit];
121122
}
122123

124+
- (void)animateWithTiming:(MDMMotionTiming)timing animations:(void (^)(void))animations {
125+
[self animateWithTiming:timing animations:animations completion:nil];
126+
}
127+
128+
- (void)animateWithTiming:(MDMMotionTiming)timing
129+
animations:(void (^)(void))animations
130+
completion:(void(^)(void))completion {
131+
NSArray<MDMImplicitAction *> *actions = MDMAnimateImplicitly(animations);
132+
133+
[CATransaction begin];
134+
[CATransaction setCompletionBlock:completion];
135+
136+
for (MDMImplicitAction *action in actions) {
137+
id currentValue = [action.layer valueForKeyPath:action.keyPath];
138+
[self animateWithTiming:timing
139+
toLayer:action.layer
140+
withValues:@[action.initialValue, currentValue]
141+
keyPath:action.keyPath];
142+
}
143+
144+
[CATransaction commit];
145+
}
146+
123147
- (void)addCoreAnimationTracer:(void (^)(CALayer *, CAAnimation *))tracer {
124148
if (!_tracers) {
125149
_tracers = [NSMutableArray array];

src/private/MDMBlockAnimations.h

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
Copyright 2017-present The Material Motion Authors. All Rights Reserved.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
#import <Foundation/Foundation.h>
18+
#import <QuartzCore/QuartzCore.h>
19+
20+
@interface MDMImplicitAction: NSObject
21+
@property(nonatomic, strong, readonly) id initialValue;
22+
@property(nonatomic, copy, readonly) NSString *keyPath;
23+
@property(nonatomic, strong, readonly) CALayer *layer;
24+
@end
25+
26+
NSArray<MDMImplicitAction *> *MDMAnimateImplicitly(void (^animations)(void));

src/private/MDMBlockAnimations.m

+132
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/*
2+
Copyright 2017-present The Material Motion Authors. All Rights Reserved.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
#import "MDMBlockAnimations.h"
18+
19+
#import <UIKit/UIKit.h>
20+
#import <objc/runtime.h>
21+
22+
static IMP sOriginalActionForLayerImp = NULL;
23+
24+
@interface MDMActionContext: NSObject
25+
@property(nonatomic, readonly) NSArray<MDMImplicitAction *> *interceptedActions;
26+
@end
27+
28+
@implementation MDMImplicitAction
29+
30+
- (instancetype)initWithLayer:(CALayer *)layer
31+
keyPath:(NSString *)keyPath
32+
initialValue:(id)initialValue {
33+
self = [super init];
34+
if (self) {
35+
_layer = layer;
36+
_keyPath = [keyPath copy];
37+
_initialValue = initialValue;
38+
}
39+
return self;
40+
}
41+
42+
@end
43+
44+
@implementation MDMActionContext {
45+
NSMutableArray<MDMImplicitAction *> *_interceptedActions;
46+
}
47+
48+
- (instancetype)init {
49+
self = [super init];
50+
if (self) {
51+
_interceptedActions = [NSMutableArray array];
52+
}
53+
return self;
54+
}
55+
56+
- (void)addActionForLayer:(CALayer *)layer
57+
keyPath:(NSString *)keyPath
58+
withInitialValue:(id)initialValue {
59+
[_interceptedActions addObject:[[MDMImplicitAction alloc] initWithLayer:layer
60+
keyPath:keyPath
61+
initialValue:initialValue]];
62+
}
63+
64+
- (NSArray<MDMImplicitAction *> *)interceptedActions {
65+
return [_interceptedActions copy];
66+
}
67+
68+
@end
69+
70+
static NSMutableArray *sActionContext = nil;
71+
72+
static id<CAAction> ActionForLayer(id self, SEL _cmd, CALayer *layer, NSString *event) {
73+
NSCAssert([NSStringFromSelector(_cmd) isEqualToString:
74+
NSStringFromSelector(@selector(actionForLayer:forKey:))],
75+
@"Invalid method signature.");
76+
77+
MDMActionContext *context = [sActionContext lastObject];
78+
NSCAssert(context != nil, @"MotionAnimator action method invoked out of implicit scope.");
79+
80+
if (context == nil) {
81+
// Graceful handling of invalid state on non-debug builds for if our context is nil invokes our
82+
// original implementation:
83+
return ((id<CAAction>(*)(id, SEL, CALayer *, NSString *))sOriginalActionForLayerImp)
84+
(self, _cmd, layer, event);
85+
}
86+
87+
// We don't have access to the "to" value of our animation here, so we unfortunately can't
88+
// calculate additive values if the animator is configured as such. So, to support additive
89+
// animations, we queue up the modified actions and then add them all at the end of our
90+
// MDMAnimateBlock invocation.
91+
id initialValue = [layer valueForKeyPath:event];
92+
[context addActionForLayer:layer keyPath:event withInitialValue:initialValue];
93+
return [NSNull null];
94+
}
95+
96+
NSArray<MDMImplicitAction *> *MDMAnimateImplicitly(void (^work)(void)) {
97+
if (!work) {
98+
return nil;
99+
}
100+
101+
// This method can be called recursively, so we maintain a recursive context stack in the scope of
102+
// this method. Note that this is absolutely not thread safe, but neither is Core Animation.
103+
if (!sActionContext) {
104+
sActionContext = [NSMutableArray array];
105+
}
106+
[sActionContext addObject:[[MDMActionContext alloc] init]];
107+
108+
SEL selector = @selector(actionForLayer:forKey:);
109+
Method method = class_getInstanceMethod([UIView class], selector);
110+
111+
if (sOriginalActionForLayerImp == nil) {
112+
// Swap the original UIView implementation with our own so that we can intercept all
113+
// actionForLayer:forKey: events. All events will be
114+
sOriginalActionForLayerImp = method_setImplementation(method, (IMP)ActionForLayer);
115+
}
116+
117+
work();
118+
119+
// Return any intercepted actions we received during the invocation of work.
120+
MDMActionContext *context = [sActionContext lastObject];
121+
[sActionContext removeLastObject];
122+
123+
if ([sActionContext count] == 0) {
124+
// Restore our original method if we've emptied the stack:
125+
method_setImplementation(method, sOriginalActionForLayerImp);
126+
127+
sOriginalActionForLayerImp = nil;
128+
sActionContext = nil;
129+
}
130+
131+
return context.interceptedActions;
132+
}

0 commit comments

Comments
 (0)