Use Core Animation for find indicator

This commit is contained in:
Jacob Bandes-Storch
2016-08-28 14:12:40 -07:00
committed by Allan Odgaard
parent ba8b6d52f4
commit dd3ebefe3f

View File

@@ -1,134 +1,136 @@
#import "OakPopOutAnimation.h"
#import <oak/algorithm.h>
#import <oak/debug.h>
static CGFloat const kExtendWidth = 6;
static CGFloat const kExtendWidth = 4;
static CGFloat const kExtendHeight = 1;
static CGFloat const kRectXRadius = 6;
static CGFloat const kRectYRadius = 6;
static double const kGrowDuration = 0.10;
static double const kFadeDuration = 0.60;
static CGFloat const kRectXRadius = 2;
static CGFloat const kRectYRadius = 2;
static CGFloat const kMaxScale = 1.5;
static CGFloat const kShadowRadius = 2;
static double const kGrowDuration = 0.05;
static double const kFadeDuration = 0.50;
#if !defined(MAC_OS_X_VERSION_10_12) || (MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_12)
@interface OakPopOutView : NSView
#else
@interface OakPopOutView : NSView <CAAnimationDelegate>
#endif
{
OBJC_WATCH_LEAKS(OakPopOutView);
NSRect baseFrame;
double growDuration;
double fadeDuration;
CALayer* imageLayer;
CAShapeLayer* shapeLayer;
}
@property (nonatomic) NSDate* animationStartTime;
@property (nonatomic) NSImage* contentImage;
@property (nonatomic) NSWindow* retainedWindow;
- (void)startAnimation:(id)sender;
@end
void OakShowPopOutAnimation (NSRect aRect, NSImage* anImage)
void OakShowPopOutAnimation (NSRect viewRect, NSImage* anImage)
{
if(aRect.size.width == 0 || aRect.size.height == 0)
if(viewRect.size.width == 0 || viewRect.size.height == 0)
return;
aRect = NSInsetRect(aRect, -kExtendWidth, -kExtendHeight);
NSRect contentRect = NSMakeRect(0, 0, NSWidth(aRect), NSHeight(aRect));
NSWindow* window = [[NSWindow alloc] initWithContentRect:contentRect styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:NO];
[window setAlphaValue:1];
viewRect = NSInsetRect(viewRect, -kExtendWidth, -kExtendHeight);
NSRect windowRect = viewRect;
CGFloat extraWidth = ceil((kMaxScale - 1) * (viewRect.size.width + 4 * kShadowRadius)/2);
CGFloat extraHeight = ceil((kMaxScale - 1) * (viewRect.size.height + 4 * kShadowRadius)/2);
windowRect.origin.x -= extraWidth; viewRect.origin.x = extraWidth;
windowRect.origin.y -= extraHeight; viewRect.origin.y = extraHeight;
windowRect.size.width += 2 * extraWidth;
windowRect.size.height += 2 * extraHeight;
NSWindow* window = [[NSWindow alloc] initWithContentRect:windowRect styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:NO];
[window setBackgroundColor:[NSColor clearColor]];
[window setExcludedFromWindowsMenu:YES];
[window setHasShadow:YES];
[window setIgnoresMouseEvents:YES];
[window setLevel:NSStatusWindowLevel];
[window setOpaque:NO];
[window setReleasedWhenClosed:NO];
[window useOptimizedDrawing:YES];
[[window contentView] setWantsLayer:YES];
OakPopOutView* aView = [[OakPopOutView alloc] initWithFrame:contentRect];
OakPopOutView* aView = [[OakPopOutView alloc] initWithFrame:viewRect];
[aView setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
aView.contentImage = anImage;
aView.retainedWindow = window;
[[window contentView] addSubview:aView];
[window setFrame:aRect display:YES];
[window setFrame:[window frameRectForContentRect:windowRect] display:YES];
[window orderFront:nil];
[aView startAnimation:nil];
}
static double bounce_curve (double t)
{
return 1 - sqrt( 1 - pow((1 - fabs(2*t - 1)), 2) );
// return 1 - fabs( 2 * (1 - (1.0 / (1.0 + exp(-12*t + 6)))) - 1);
}
@implementation OakPopOutView
- (id)initWithFrame:(NSRect)aRect
{
if(self = [super initWithFrame:aRect])
{
double const slowDownFactor = 1; // ([[NSApp currentEvent] modifierFlags] & (NSCommandKeyMask|NSAlternateKeyMask|NSControlKeyMask|NSShiftKeyMask)) == NSShiftKeyMask ? 6 : 1;
growDuration = kGrowDuration * slowDownFactor;
fadeDuration = kFadeDuration * slowDownFactor;
[self setWantsLayer:YES];
self.layer.masksToBounds = NO;
shapeLayer = [CAShapeLayer layer];
shapeLayer.frame = self.bounds;
shapeLayer.fillColor = [[NSColor yellowColor] CGColor];
shapeLayer.strokeColor = [[NSColor colorWithWhite:0 alpha:0.1] CGColor];
shapeLayer.lineWidth = 0.5;
shapeLayer.path = CGPathCreateWithRoundedRect(CGRectInset([self bounds], 0.25, 0.25), kRectXRadius, kRectYRadius, NULL);
shapeLayer.shadowOpacity = 0.25;
shapeLayer.shadowRadius = kShadowRadius;
shapeLayer.shadowOffset = CGSizeMake(0, -1);
[self.layer addSublayer:shapeLayer];
imageLayer = [CALayer layer];
[shapeLayer addSublayer:imageLayer];
}
return self;
}
- (void)viewDidMoveToWindow
{
CGFloat scaleFactor = self.window.screen.backingScaleFactor;
if(scaleFactor)
{
imageLayer.contents = [_contentImage layerContentsForContentsScale:scaleFactor];
imageLayer.bounds = CGRectMake(0, 0, _contentImage.size.width, _contentImage.size.height);
imageLayer.position = CGPointMake(CGRectGetMidX(shapeLayer.bounds), CGRectGetMidY(shapeLayer.bounds));
}
}
- (void)startAnimation:(id)sender
{
baseFrame = [[self window] frame];
self.animationStartTime = [NSDate date];
[NSTimer scheduledTimerWithTimeInterval:0.02 target:self selector:@selector(animationTick:) userInfo:nil repeats:YES];
static CAAnimationGroup* animationGroup; // Animations are copied; we'll reuse one
static dispatch_once_t onceToken = 0;
dispatch_once(&onceToken, ^{
CABasicAnimation* grow = [CABasicAnimation animationWithKeyPath:@"transform.scale"];
grow.duration = kGrowDuration;
grow.fromValue = @1;
grow.toValue = @(kMaxScale);
grow.autoreverses = YES;
grow.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
CABasicAnimation* fade = [CABasicAnimation animationWithKeyPath:@"opacity"];
fade.beginTime = 2 * kGrowDuration;
fade.duration = kFadeDuration;
fade.fromValue = @1;
fade.toValue = @0;
fade.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
animationGroup = [CAAnimationGroup new];
animationGroup.animations = @[grow, fade];
animationGroup.duration = 2 * kGrowDuration + kFadeDuration;
animationGroup.fillMode = kCAFillModeForwards;
animationGroup.removedOnCompletion = NO;
});
animationGroup.delegate = self; // Listen for animationDidStop:finished:
animationGroup.speed = 1;
[shapeLayer addAnimation:animationGroup forKey:nil];
animationGroup.delegate = nil;
}
- (void)animationTick:(NSTimer*)aTimer
- (void)animationDidStop:(CAAnimation*)theAnimation finished:(BOOL)flag
{
double const totalDuration = 2*growDuration + fadeDuration;
CGFloat alpha = 1.0;
CGFloat grow = 0.0;
double t = [[NSDate date] timeIntervalSinceDate:self.animationStartTime];
if(t > totalDuration)
{
[aTimer invalidate];
[[self window] orderOut:self];
self.retainedWindow = nil;
return;
}
else if(t > 2*growDuration)
{
t = (t - 2*growDuration) * 1.0/fadeDuration;
alpha = 0.97 * (1 - oak::slow_in_out(t));
}
else
{
t = t * 1.0/(2*growDuration);
grow = bounce_curve(t);
}
grow = 1 + grow / 2;
CGFloat w = NSWidth(baseFrame) * grow;
CGFloat h = NSHeight(baseFrame) * grow;
CGFloat x = round(NSMidX(baseFrame) - w/2);
CGFloat y = round(NSMidY(baseFrame) - h/2);
[[self window] setFrame:NSMakeRect(x, y, round(w), round(h)) display:YES];
[[self window] setAlphaValue:alpha];
}
- (BOOL)isFlipped { return YES; }
- (BOOL)isOpaque { return NO; }
- (void)drawRect:(NSRect)aRect
{
[[NSColor clearColor] set];
NSRectFill(aRect);
NSBezierPath* roundedRect = [NSBezierPath bezierPathWithRoundedRect:[self bounds] xRadius:kRectXRadius yRadius:kRectYRadius];
[[NSColor yellowColor] set];
[roundedRect fill];
[[NSColor whiteColor] set];
[roundedRect stroke];
[self.contentImage drawInRect:NSInsetRect([self bounds], kExtendWidth, kExtendHeight) fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1 respectFlipped:YES hints:nil];
[super drawRect:aRect];
[[self window] orderOut:self];
self.retainedWindow = nil;
}
@end