Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
GIFPlayer for OSX by the guys at Byte Inc.
//
// GIFPlayer.h
// Byte
//
// Created by Dom Hofmann on 8/9/15.
// Copyright © 2015 Byte, Inc. All rights reserved.
//
#import "ByteObject.h"
#import <Cocoa/Cocoa.h>
typedef NS_ENUM(NSUInteger, GIFPlayerScaleType) {
GIFPlayerScaleTypeFit,
GIFPlayerScaleTypeFill
};
@interface GIFPlayer : NSView <ByteObjectPrimitive>
@property (nonatomic) ByteObject *parentObject;
@property (nonatomic) GIFPlayerScaleType scaleType;
@property (nonatomic) NSData *imageData;
@end
//
// GIFPlayer.m
// Byte
//
// Created by Dom Hofmann on 8/9/15.
// Copyright © 2015 Byte, Inc. All rights reserved.
//
#import "GIFPlayer.h"
#import <QuartzCore/QuartzCore.h>
@interface GIFPlayer ()
@property (nonatomic) NSImage *image;
@property (nonatomic) CALayer *animationLayer;
@end
@implementation GIFPlayer
- (void)dealloc {
NSLog(@"Deallocating %@ %p", [self class], self);
}
- (BOOL)isFlipped {
return YES;
}
- (id)initWithFrame:(NSRect)frameRect {
if (self = [super initWithFrame:frameRect]) {
self.wantsLayer = YES;
self.layer.anchorPoint = CGPointMake(0.5, 0.5);
self.layer.frame = frameRect;
self.animationLayer = [CALayer new];
[self.layer addSublayer:self.animationLayer];
}
return self;
}
- (void)resizeSubviewsWithOldSize:(NSSize)oldSize {
[super resizeSubviewsWithOldSize:oldSize];
if (self.image) {
NSRect targetFrame = NSZeroRect;
CGFloat containerAspect = self.layer.bounds.size.width / self.layer.bounds.size.height;
CGFloat imageAspect = self.image.size.width / self.image.size.height;
CGFloat ratio;
if (_scaleType == GIFPlayerScaleTypeFill) {
if (containerAspect > imageAspect) {
ratio = self.layer.bounds.size.width / self.image.size.width;
} else {
ratio = self.layer.bounds.size.height / self.image.size.height;
}
} else {
if (containerAspect > imageAspect) {
ratio = self.layer.bounds.size.height / self.image.size.height;
} else {
ratio = self.layer.bounds.size.width / self.image.size.width;
}
}
CGFloat targetW = self.image.size.width * ratio;
CGFloat targetH = self.image.size.height * ratio;
targetFrame = NSMakeRect(self.layer.bounds.size.width / 2 - targetW / 2, self.layer.bounds.size.height / 2 - targetH / 2, targetW, targetH);
[CATransaction begin];
[CATransaction setDisableActions:YES];
self.animationLayer.frame = targetFrame;
[CATransaction commit];
}
}
- (void)setImageData:(NSData *)imageData {
_imageData = imageData;
self.image = [[NSImage alloc] initWithData:imageData];
CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)imageData, NULL);
if (!imageSource) {
NSLog(@"Couldn't load image source for supplied data");
return;
}
CFStringRef imageSourceContainerType = CGImageSourceGetType(imageSource);
BOOL isGIFData = UTTypeConformsTo(imageSourceContainerType, kUTTypeGIF);
if (!isGIFData) {
NSLog(@"The provided data is not a valid GIF");
return;
}
NSArray * reps = [self.image representations];
// build GIF frames
// adapted from procedures in this blog post: http://blog.pcitron.fr/2010/12/14/play-an-animated-gif-with-an-ikimageview/
for (NSImageRep * rep in reps) {
if ([rep isKindOfClass:[NSBitmapImageRep class]] == YES) {
NSBitmapImageRep *bitmapRep = (NSBitmapImageRep *)rep;
// get the number of frames. If it's 0, it's not an animated gif, do nothing
int frameCount = [[bitmapRep valueForProperty:NSImageFrameCount] intValue];
if (frameCount == 0) {
break;
}
// create an animation array which will contain all the frames of the animation
NSMutableArray *values = [NSMutableArray arrayWithCapacity:frameCount];
// create an array of delay times which contain the time to delay between each frame
NSMutableArray *delayTimes = [NSMutableArray arrayWithCapacity:frameCount];
// create a secondary array of Key times, which contain the cumulative time (on the animation timeline) that each frame should "hit"
__unused NSMutableArray *keyTimes = [NSMutableArray arrayWithCapacity:frameCount];
// we will increment this in our loop below to set total animation length
NSTimeInterval animationDuration = 0.0;
// loop through the frames
for (int i = 0; i < frameCount; i++) {
// set the current frame we're operating on
[bitmapRep setProperty:NSImageCurrentFrame withValue:[NSNumber numberWithInt:i]];
NSDictionary *frameProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(imageSource, i, NULL);
NSDictionary *framePropertiesGIF = [frameProperties objectForKey:(id)kCGImagePropertyGIFDictionary];
// Try to use the unclamped delay time; fall back to the normal delay time.
NSNumber *delayTime = [framePropertiesGIF objectForKey:(id)kCGImagePropertyGIFUnclampedDelayTime];
if (!delayTime) {
delayTime = [framePropertiesGIF objectForKey:(id)kCGImagePropertyGIFDelayTime];
}
// If we don't get a delay time from the properties, fall back to `kDelayTimeIntervalDefault` or carry over the preceding frame's value.
const NSTimeInterval kDelayTimeIntervalDefault = 0.1;
if (!delayTime) {
if (i == 0) {
NSLog(@"Falling back to default delay time for first frame, because a delay time was not found in GIF properties %@", frameProperties);
delayTime = @(kDelayTimeIntervalDefault);
} else {
NSLog(@"Falling back to preceding delay time for frame #%d, because a delay time was not found in GIF properties %@", i, frameProperties);
delayTime = delayTimes.lastObject;
}
}
[delayTimes addObject:delayTime];
[values addObject:(id)[bitmapRep CGImage]];
// increment the duration of the animation
animationDuration += delayTime.floatValue;
}
// create the animation
CAKeyframeAnimation * animation = [CAKeyframeAnimation animationWithKeyPath:@"contents"];
[animation setValues:values];
[animation setCalculationMode:@"discrete"];
[animation setDuration:(animationDuration - [delayTimes.lastObject floatValue])];
[animation setRepeatCount:HUGE_VAL];
// add the animation to the layer
[self.animationLayer addAnimation:animation forKey:@"contents"];
// ignore any other representations
break;
}
}
[self resizeSubviewsWithOldSize:self.frame.size];
}
- (void)setData:(NSMutableDictionary *)data isNew:(BOOL)isNew {
}
- (void)load:(CompletionBlock)completion {
if (completion) {
completion(YES);
}
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment