Permalink
Browse files

Added TUICAAction, which will handle animation of NSViews nested with…

…in TwUI
  • Loading branch information...
1 parent 66c39d5 commit ec0b4b40b360ab5fec7f67630230d4f442b03c0a @jspahrsummers jspahrsummers committed Jul 18, 2012
Showing with 306 additions and 0 deletions.
  1. +10 −0 TwUI.xcodeproj/project.pbxproj
  2. +36 −0 lib/Support/TUICAAction.h
  3. +260 −0 lib/Support/TUICAAction.m
@@ -317,6 +317,9 @@
D0C7656615B6297300E7AC2C /* NSScrollView+TUIExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = D0C7655C15B6297300E7AC2C /* NSScrollView+TUIExtensions.m */; };
D0C7656715B6297300E7AC2C /* NSScrollView+TUIExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = D0C7655C15B6297300E7AC2C /* NSScrollView+TUIExtensions.m */; };
D0C7656815B6297300E7AC2C /* NSScrollView+TUIExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = D0C7655C15B6297300E7AC2C /* NSScrollView+TUIExtensions.m */; };
+ D0C7657415B6341800E7AC2C /* TUICAAction.m in Sources */ = {isa = PBXBuildFile; fileRef = D0C7657015B6341800E7AC2C /* TUICAAction.m */; };
+ D0C7657515B6341800E7AC2C /* TUICAAction.m in Sources */ = {isa = PBXBuildFile; fileRef = D0C7657015B6341800E7AC2C /* TUICAAction.m */; };
+ D0C7657615B6341800E7AC2C /* TUICAAction.m in Sources */ = {isa = PBXBuildFile; fileRef = D0C7657015B6341800E7AC2C /* TUICAAction.m */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -482,6 +485,8 @@
D0C7655C15B6297300E7AC2C /* NSScrollView+TUIExtensions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSScrollView+TUIExtensions.m"; sourceTree = "<group>"; };
D0C7656915B62EFA00E7AC2C /* TUINSView+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "TUINSView+Private.h"; sourceTree = "<group>"; };
D0C7656D15B6322A00E7AC2C /* TUIViewNSViewContainer+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "TUIViewNSViewContainer+Private.h"; sourceTree = "<group>"; };
+ D0C7656F15B6341800E7AC2C /* TUICAAction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TUICAAction.h; sourceTree = "<group>"; };
+ D0C7657015B6341800E7AC2C /* TUICAAction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TUICAAction.m; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -632,6 +637,8 @@
D0C7652115B6232100E7AC2C /* CALayer+TUIExtensions.m */,
D0C7652215B6232100E7AC2C /* CATransaction+TUIExtensions.h */,
D0C7652315B6232100E7AC2C /* CATransaction+TUIExtensions.m */,
+ D0C7656F15B6341800E7AC2C /* TUICAAction.h */,
+ D0C7657015B6341800E7AC2C /* TUICAAction.m */,
);
name = Support;
path = lib/Support;
@@ -1097,6 +1104,7 @@
D0C7655715B6294400E7AC2C /* TUIScrollView+TUIBridgedScrollView.m in Sources */,
D0C7656215B6297300E7AC2C /* NSClipView+TUIExtensions.m in Sources */,
D0C7656815B6297300E7AC2C /* NSScrollView+TUIExtensions.m in Sources */,
+ D0C7657615B6341800E7AC2C /* TUICAAction.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1171,6 +1179,7 @@
D0C7655515B6294400E7AC2C /* TUIScrollView+TUIBridgedScrollView.m in Sources */,
D0C7656015B6297300E7AC2C /* NSClipView+TUIExtensions.m in Sources */,
D0C7656615B6297300E7AC2C /* NSScrollView+TUIExtensions.m in Sources */,
+ D0C7657415B6341800E7AC2C /* TUICAAction.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1251,6 +1260,7 @@
D0C7655615B6294400E7AC2C /* TUIScrollView+TUIBridgedScrollView.m in Sources */,
D0C7656115B6297300E7AC2C /* NSClipView+TUIExtensions.m in Sources */,
D0C7656715B6297300E7AC2C /* NSScrollView+TUIExtensions.m in Sources */,
+ D0C7657515B6341800E7AC2C /* TUICAAction.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -0,0 +1,36 @@
+//
+// TUICAAction.h
+//
+// Created by James Lawton on 11/21/11.
+// Copyright (c) 2011 Bitswift. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+#import <QuartzCore/QuartzCore.h>
+
+/*
+ * A CAAction which finds AppKit views contained in Velvet and animates them
+ * alongside. We pass through to the default animation to animate the Velvet
+ * views.
+ */
+@interface TUICAAction : NSObject <CAAction>
+
+/*
+ * Initializes an action which proxies for the given action, and handles animation
+ * of all descendent TUIViewNSViewContainer instances along with the layer being acted upon.
+ *
+ * This is the designated initializer.
+ */
+- (id)initWithAction:(id<CAAction>)innerAction;
+
+/*
+ * Returns an action initialized with <initWithAction:>.
+ */
++ (id)actionWithAction:(id<CAAction>)innerAction;
+
+/*
+ * Whether objects of this class add features to actions for the given key.
+ */
++ (BOOL)interceptsActionForKey:(NSString *)key;
+
+@end
@@ -0,0 +1,260 @@
+//
+// TUICAAction.m
+//
+// Created by James Lawton on 11/21/11.
+// Copyright (c) 2011 Bitswift. All rights reserved.
+//
+
+#import "TUICAAction.h"
+#import "TUINSWindow.h"
+#import "TUIView.h"
+#import "TUIViewNSViewContainer+Private.h"
+#import "CATransaction+TUIExtensions.h"
+#import <objc/runtime.h>
+
+@interface TUICAAction ()
+/*
+ * The action that this action is proxying, as specified at the time of
+ * initialization.
+ */
+@property (nonatomic, strong, readonly) id<CAAction> innerAction;
+
+/*
+ * The first responder when the animation began, to be restored when the
+ * animation completes.
+ */
+@property (nonatomic, strong) NSResponder *originalFirstResponder;
+
+/*
+ * Invoked whenever the geometry property `key` of `layer` has changed.
+ */
+- (void)geometryChangedForKey:(NSString *)key layer:(CALayer *)layer;
+
+/*
+ * Invoked whenever the opacity of `layer` has changed.
+ */
+- (void)opacityChangedForLayer:(CALayer *)layer;
+
+/*
+ * Renders the `NSView` of the given view into its layer and hides the `NSView`.
+ *
+ * This can be used to capture a static rendering of the `NSView` for animation
+ * or other purposes.
+ *
+ * @param view The <TUIViewNSViewContainer> hosting the `NSView`.
+ *
+ * @note <stopRenderingNSViewOfView:> should be invoked to restore the normal
+ * rendering of the `NSView`. Calls to these methods cannot be nested.
+ */
+- (void)startRenderingNSViewOfView:(TUIViewNSViewContainer *)view;
+
+/*
+ * Restores the normal rendering of the given view's `NSView` after a previous
+ * call to <startRenderingNSViewOfView:>.
+ *
+ * @param view The <TUIViewNSViewContainer> hosting the `NSView`.
+ *
+ * @note Calls to this method cannot be nested.
+ */
+- (void)stopRenderingNSViewOfView:(TUIViewNSViewContainer *)view;
+
+/*
+ * Schedules the given block to execute when the animation represented by the
+ * receiver completes.
+ *
+ * @param block A block to be run when the animation completes.
+ * @param layer The layer that the receiver is attached to.
+ */
+- (void)runWhenAnimationCompletes:(void (^)(void))block forLayer:(CALayer *)layer;
+
+/*
+ * Returns `YES` if objects of this class add features to actions for the given
+ * geometry property.
+ */
++ (BOOL)interceptsGeometryActionForKey:(NSString *)key;
+
+@end
+
+@implementation TUICAAction
+
+#pragma mark Properties
+
+@synthesize innerAction = m_innerAction;
+@synthesize originalFirstResponder = m_originalFirstResponder;
+
+#pragma mark Lifecycle
+
+- (id)initWithAction:(id<CAAction>)innerAction {
+ self = [super init];
+ if (!self)
+ return nil;
+
+ m_innerAction = innerAction;
+ return self;
+}
+
++ (id)actionWithAction:(id<CAAction>)innerAction {
+ return [[self alloc] initWithAction:innerAction];
+}
+
+#pragma mark TUIViewNSViewContainer support
+
+- (void)enumerateTUIViewNSViewContainersInLayer:(CALayer *)layer block:(void(^)(TUIViewNSViewContainer *))block {
+ if ([layer.delegate isKindOfClass:[TUIViewNSViewContainer class]]) {
+ block(layer.delegate);
+ } else {
+ for (CALayer *sublayer in [layer sublayers]) {
+ [self enumerateTUIViewNSViewContainersInLayer:sublayer block:block];
+ }
+ }
+}
+
+- (void)startRenderingNSViewOfView:(TUIViewNSViewContainer *)view; {
+ // Resign first responder on an animating NSView
+ // to disable focus ring.
+ id responder = view.nsWindow.firstResponder;
+ if ([responder isKindOfClass:[NSView class]] && [responder isDescendantOf:view.rootView]) {
+ // find the next responder which is not in this NSView hierarchy, and
+ // make that the first responder for the duration of the animation
+ id nextResponder = [responder nextResponder];
+
+ BOOL (^validResponder)(id) = ^(id obj){
+ if (!obj)
+ return YES;
+
+ if ([obj isKindOfClass:[NSView class]] && [responder isDescendantOf:view.rootView])
+ return NO;
+
+ if (![obj acceptsFirstResponder])
+ return NO;
+
+ return YES;
+ };
+
+ while (!validResponder(nextResponder)) {
+ nextResponder = [nextResponder nextResponder];
+ }
+
+ if ([view.nsWindow makeFirstResponder:nextResponder]) {
+ self.originalFirstResponder = responder;
+ }
+ }
+
+ BOOL wasAlreadyRendering = view.renderingContainedView;
+
+ [view startRenderingContainedView];
+ if (!wasAlreadyRendering) {
+ view.rootView.alphaValue = 0.0;
+ }
+}
+
+- (void)stopRenderingNSViewOfView:(TUIViewNSViewContainer *)view; {
+ [view stopRenderingContainedView];
+
+ if (!view.renderingContainedView) {
+ [view synchronizeNSViewGeometry];
+ view.rootView.alphaValue = 1.0;
+ }
+
+ if (self.originalFirstResponder) {
+ [view.nsWindow makeFirstResponder:self.originalFirstResponder];
+ self.originalFirstResponder = nil;
+ }
+}
+
+#pragma mark Action interception
+
+- (void)runActionForKey:(NSString *)key object:(id)anObject arguments:(NSDictionary *)dict {
+ [self.innerAction runActionForKey:key object:anObject arguments:dict];
+
+ CAAnimation *animation = [anObject animationForKey:key];
+ if (!animation)
+ return;
+
+ if ([key isEqualToString:@"opacity"]) {
+ [self opacityChangedForLayer:anObject];
+ } else if ([[self class] interceptsGeometryActionForKey:key]) {
+ [self geometryChangedForKey:key layer:anObject];
+ }
+}
+
++ (BOOL)interceptsGeometryActionForKey:(NSString *)key {
+ return [key isEqualToString:@"position"] || [key isEqualToString:@"bounds"] || [key isEqualToString:@"transform"];
+}
+
++ (BOOL)interceptsActionForKey:(NSString *)key {
+ return [key isEqualToString:@"opacity"] || [self interceptsGeometryActionForKey:key];
+}
+
+#pragma mark Action handlers
+
+- (void)geometryChangedForKey:(NSString *)key layer:(CALayer *)layer {
+ // For all contained TUIViewNSViewContainers, render their NSView into their layer
+ // and hide the NSView. Now the visual element is part of the layer
+ // hierarchy we're animating.
+ NSMutableArray *cachedViews = [NSMutableArray array];
+ [self enumerateTUIViewNSViewContainersInLayer:layer block:^(TUIViewNSViewContainer *view) {
+ [self startRenderingNSViewOfView:view];
+ [cachedViews addObject:view];
+ }];
+
+ // If we did nothing, there's no cleanup needed.
+ if (![cachedViews count])
+ return;
+
+ // return NSViews to rendering themselves after this animation completes
+ [self runWhenAnimationCompletes:^{
+ [cachedViews enumerateObjectsUsingBlock:^(TUIViewNSViewContainer *view, NSUInteger idx, BOOL *stop) {
+ [self stopRenderingNSViewOfView:view];
+ }];
+ } forLayer:layer];
+}
+
+- (void)opacityChangedForLayer:(CALayer *)layer; {
+ float newOpacity = layer.opacity;
+
+ if (fabs(1 - newOpacity) < 0.001) {
+ // return NSViews to rendering themselves after this animation completes
+ [self runWhenAnimationCompletes:^{
+ [self enumerateTUIViewNSViewContainersInLayer:layer block:^(TUIViewNSViewContainer *view) {
+ [self stopRenderingNSViewOfView:view];
+ }];
+ } forLayer:layer];
+ } else {
+ // For all contained TUIViewNSViewContainers, render their NSView into their layer
+ // and hide the NSView. Now the visual element is part of the layer
+ // hierarchy we're animating.
+ [self enumerateTUIViewNSViewContainersInLayer:layer block:^(TUIViewNSViewContainer *view) {
+ [self startRenderingNSViewOfView:view];
+ }];
+ }
+}
+
+- (void)runWhenAnimationCompletes:(void (^)(void))block forLayer:(CALayer *)layer; {
+ NSParameterAssert(block != nil);
+ NSParameterAssert(layer != nil);
+
+ CFTimeInterval duration = [CATransaction animationDuration];
+
+ if ([(id)self.innerAction isKindOfClass:[CAAnimation class]]) {
+ CAAnimation *animation = (id)self.innerAction;
+ if (animation.duration)
+ duration = animation.duration;
+
+ duration += animation.beginTime;
+ duration /= animation.speed;
+
+ do {
+ duration += layer.beginTime - layer.timeOffset;
+ duration /= layer.speed;
+
+ layer = layer.superlayer;
+ } while (layer);
+ }
+
+ dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(duration * NSEC_PER_SEC));
+ dispatch_after(popTime, dispatch_get_main_queue(), block);
+}
+
+@end
+

0 comments on commit ec0b4b4

Please sign in to comment.