Skip to content
Browse files

First stab at retina-aware TUIImage. Make TUIImage/imageNamed: lazy, …

…will reload different variants if needed when drawing based on the current scale factor of the destination context. Track the scale factor of the current destination context with a thread-specific global (so drawsInBackground works), set in TUIView/displayLayer.
  • Loading branch information...
1 parent f354c6d commit 790d850137deb9001af4ef73f875119ae26394d1 @atebits atebits committed Jun 19, 2012
Showing with 195 additions and 78 deletions.
  1. +4 −0 ExampleProject/Example.xcodeproj/project.pbxproj
  2. +1 −3 lib/UIKit/TUIImage.h
  3. +157 −75 lib/UIKit/TUIImage.m
  4. +2 −0 lib/UIKit/TUIView+Private.h
  5. +31 −0 lib/UIKit/TUIView.m
View
4 ExampleProject/Example.xcodeproj/project.pbxproj
@@ -13,6 +13,7 @@
5C782FD013A54FFF00CF69EF /* ApplicationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C782FCF13A54FFD00CF69EF /* ApplicationServices.framework */; };
5C782FD713A556A900CF69EF /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C782FD613A556A800CF69EF /* QuartzCore.framework */; };
5C90DB9D13A7C08E00ECDD14 /* ExampleTabBar.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C90DB9C13A7C08D00ECDD14 /* ExampleTabBar.m */; };
+ 5E8A288B158FC349000D5D2B /* clock@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 5E8A288A158FC349000D5D2B /* clock@2x.png */; };
5ED56678139DC30300031CDF /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5ED56677139DC30300031CDF /* Cocoa.framework */; };
5ED56682139DC30300031CDF /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5ED56680139DC30300031CDF /* InfoPlist.strings */; };
5ED56685139DC30300031CDF /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 5ED56684139DC30300031CDF /* main.m */; };
@@ -73,6 +74,7 @@
5C782FD613A556A800CF69EF /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; };
5C90DB9B13A7C08D00ECDD14 /* ExampleTabBar.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ExampleTabBar.h; sourceTree = "<group>"; };
5C90DB9C13A7C08D00ECDD14 /* ExampleTabBar.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ExampleTabBar.m; sourceTree = "<group>"; };
+ 5E8A288A158FC349000D5D2B /* clock@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "clock@2x.png"; path = "../../../../Desktop/clock@2x.png"; sourceTree = "<group>"; };
5ED56673139DC30300031CDF /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
5ED56677139DC30300031CDF /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; };
5ED5667A139DC30300031CDF /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; };
@@ -172,6 +174,7 @@
5ED5667E139DC30300031CDF /* Supporting Files */ = {
isa = PBXGroup;
children = (
+ 5E8A288A158FC349000D5D2B /* clock@2x.png */,
D3EC0C481432325A003C162C /* large-image.jpeg */,
5C57CF3C13A7E7C00032AC1F /* clock.png */,
5ED5667F139DC30300031CDF /* Example-Info.plist */,
@@ -287,6 +290,7 @@
5ED5668E139DC30300031CDF /* MainMenu.xib in Resources */,
5C57CF3D13A7E7C00032AC1F /* clock.png in Resources */,
D3EC0C4A1432325A003C162C /* large-image.jpeg in Resources */,
+ 5E8A288B158FC349000D5D2B /* clock@2x.png in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
View
4 lib/UIKit/TUIImage.h
@@ -17,9 +17,6 @@
#import <Foundation/Foundation.h>
@interface TUIImage : NSObject
-{
- CGImageRef _imageRef;
-}
+ (TUIImage *)imageNamed:(NSString *)name;
+ (TUIImage *)imageNamed:(NSString *)name cache:(BOOL)shouldCache;
@@ -33,6 +30,7 @@
- (id)initWithCGImage:(CGImageRef)imageRef;
@property (nonatomic, readonly) CGSize size;
+@property (nonatomic, readonly) CGFloat scale;
@property (nonatomic, readonly) CGImageRef CGImage;
- (void)drawAtPoint:(CGPoint)point; // mode = kCGBlendModeNormal, alpha = 1.0
View
232 lib/UIKit/TUIImage.m
@@ -17,6 +17,78 @@
#import "TUIImage.h"
#import "TUIKit.h"
+static CGImageRef TUICreateImageRefWithData(NSData *data)
+{
+ if(!data)
+ return nil;
+
+ CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
+ if(!imageSource) {
+ return nil;
+ }
+
+ CGImageRef image = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);
+ if(!image) {
+ NSLog(@"could not create image at index 0");
+ }
+
+ CFRelease(imageSource);
+ return image;
+}
+
+static CGImageRef TUICreateImageRefForURL(NSURL *url, BOOL shouldCache)
+{
+ static NSMutableDictionary *cache = nil;
+ if(!cache)
+ cache = [NSMutableDictionary new];
+
+ if(url) {
+ CGImageRef image;
+
+ // look up in cache
+ image = (__bridge CGImageRef)[cache objectForKey:url];
+ if(image)
+ return CGImageRetain(image);
+
+ image = TUICreateImageRefWithData([NSData dataWithContentsOfURL:url]);
+ if(image && shouldCache)
+ [cache setObject:(__bridge id)image forKey:url];
+
+ return image;
+ }
+ return NULL;
+}
+
+static NSURL *TUIURLForNameAndScaleFactor(NSString *name, CGFloat scaleFactor)
+{
+ // simplest thing that works for now
+ // todo - understand the details of what UIKit does, mimic.
+ NSString *ext = [name pathExtension];
+ NSString *baseName = [name stringByDeletingPathExtension];
+ if(scaleFactor == 2.0) {
+ name = [[baseName stringByAppendingString:@"@2x"] stringByAppendingPathExtension:ext];
+ } else {
+ name = [baseName stringByAppendingPathExtension:ext];
+ }
+ return [[[NSBundle mainBundle] resourceURL] URLByAppendingPathComponent:name];
+}
+
+static CGImageRef TUICreateImageRefForNameAndScaleFactor(NSString *name, CGFloat scaleFactor, BOOL shouldCache)
+{
+ if(name) {
+ CGImageRef i = NULL;
+ if(scaleFactor == 2.0) {
+ i = TUICreateImageRefForURL(TUIURLForNameAndScaleFactor(name, scaleFactor), shouldCache);
+ if(i)
+ return i;
+ }
+ // fallback
+ return TUICreateImageRefForURL(TUIURLForNameAndScaleFactor(name, 1.0), shouldCache);
+ }
+
+ return NULL;
+}
+
@interface TUIStretchableImage : TUIImage
{
@public
@@ -32,6 +104,49 @@ @interface TUIStretchableImage : TUIImage
@implementation TUIImage
+{
+ CGFloat _lastScaleFactor;
+ NSString *_imageName;
+ CGImageRef _imageRef;
+ BOOL _shouldCache;
+}
+
+- (id)init
+{
+ if(self = [super init]) {
+ _lastScaleFactor = 1.0;
+ }
+ return self;
+}
+
+- (id)initWithCGImage:(CGImageRef)imageRef
+{
+ if(self = [self init]) {
+ if(imageRef)
+ _imageRef = CGImageRetain(imageRef);
+ }
+ return self;
+}
+
+- (id)initWithName:(NSString *)name cache:(BOOL)shouldCache
+{
+ if(self = [self init]) {
+ _imageName = name;
+ _shouldCache = shouldCache;
+ }
+ return self;
+}
+
+- (id)initWithData:(NSData *)data
+{
+ CGImageRef i = TUICreateImageRefWithData(data);
+ if(i) {
+ self = [self initWithCGImage:i];
+ CGImageRelease(i);
+ return self;
+ }
+ return nil;
+}
+ (TUIImage *)_imageWithABImage:(id)abimage
{
@@ -40,32 +155,7 @@ + (TUIImage *)_imageWithABImage:(id)abimage
+ (TUIImage *)imageNamed:(NSString *)name cache:(BOOL)shouldCache
{
- if(!name)
- return nil;
-
- static NSMutableDictionary *cache = nil;
- if(!cache) {
- cache = [[NSMutableDictionary alloc] init];
- }
-
- TUIImage *image = [cache objectForKey:name];
- if(image)
- return image;
-
- NSURL *url = [[[NSBundle mainBundle] resourceURL] URLByAppendingPathComponent:name];
- if(url) {
- NSData *data = [NSData dataWithContentsOfURL:url];
- if(data) {
- image = [self imageWithData:data];
- if(image) {
- if(shouldCache) {
- [cache setObject:image forKey:name];
- }
- }
- }
- }
-
- return image;
+ return [[self alloc] initWithName:name cache:shouldCache];
}
+ (TUIImage *)imageNamed:(NSString *)name
@@ -75,32 +165,7 @@ + (TUIImage *)imageNamed:(NSString *)name
+ (TUIImage *)imageWithData:(NSData *)data
{
- CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
- if(!imageSource) {
- return nil;
- }
-
- CGImageRef image = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);
- if(!image) {
- NSLog(@"could not create image at index 0");
- CFRelease(imageSource);
- return nil;
- }
-
- TUIImage *i = [TUIImage imageWithCGImage:image];
- CGImageRelease(image);
- CFRelease(imageSource);
- return i;
-}
-
-- (id)initWithCGImage:(CGImageRef)imageRef
-{
- if((self = [super init]))
- {
- if(imageRef)
- _imageRef = CGImageRetain(imageRef);
- }
- return self;
+ return [[self alloc] initWithData:data];
}
- (void)dealloc
@@ -120,7 +185,7 @@ + (TUIImage *)imageWithCGImage:(CGImageRef)imageRef
* @note Don't use this method in -drawRect: if you use a NSGraphicsContext. This method may
* change the current context in order to convert the image and will not restore any previous
* context.
- *
+ *
* @param image an NSImage
* @return TUIImage
*/
@@ -178,15 +243,35 @@ + (TUIImage *)imageWithNSImage:(NSImage *)image
- (CGSize)size
{
- return CGSizeMake(CGImageGetWidth(_imageRef), CGImageGetHeight(_imageRef));
+ CGImageRef cgImage = [self CGImage];
+ CGFloat inv = 1.0f / _lastScaleFactor; // must call -CGImage first (will update _lastScaleFactor)
+ if(cgImage)
+ return CGSizeMake(CGImageGetWidth(cgImage) * inv, CGImageGetHeight(cgImage) * inv);
+ return CGSizeZero;
+}
+
+- (CGFloat)scale
+{
+ [self CGImage]; // update _lastScaleFactor if needed
+ return _lastScaleFactor;
}
- (CGImageRef)CGImage
{
+ if(_imageName) { // lazy image
+ CGFloat currentScaleFactor = TUICurrentContextScaleFactor();
+ if(!_imageRef || (_lastScaleFactor != currentScaleFactor)) {
+ // if we haven't loaded an image yet, or the scale factor changed, load a new one
+ if(_imageRef)
+ CGImageRelease(_imageRef);
+ _imageRef = CGImageRetain(TUICreateImageRefForNameAndScaleFactor(_imageName, currentScaleFactor, _shouldCache));
+ _lastScaleFactor = currentScaleFactor;
+ }
+ }
return _imageRef;
}
-- (void)drawAtPoint:(CGPoint)point // mode = kCGBlendModeNormal, alpha = 1.0
+- (void)drawAtPoint:(CGPoint)point // mode = kCGBlendModeNormal, alpha = 1.0
{
[self drawAtPoint:point blendMode:kCGBlendModeNormal alpha:1.0];
}
@@ -199,19 +284,20 @@ - (void)drawAtPoint:(CGPoint)point blendMode:(CGBlendMode)blendMode alpha:(CGFlo
[self drawInRect:rect blendMode:blendMode alpha:alpha];
}
-- (void)drawInRect:(CGRect)rect // mode = kCGBlendModeNormal, alpha = 1.0
+- (void)drawInRect:(CGRect)rect // mode = kCGBlendModeNormal, alpha = 1.0
{
[self drawInRect:rect blendMode:kCGBlendModeNormal alpha:1.0];
}
- (void)drawInRect:(CGRect)rect blendMode:(CGBlendMode)blendMode alpha:(CGFloat)alpha
{
- if(_imageRef) {
+ CGImageRef cgImage = [self CGImage]; // -CGImage will automatically update itself with the correct image
+ if(cgImage) {
CGContextRef ctx = TUIGraphicsGetCurrentContext();
CGContextSaveGState(ctx);
CGContextSetAlpha(ctx, alpha);
CGContextSetBlendMode(ctx, blendMode);
- CGContextDrawImage(ctx, rect, _imageRef);
+ CGContextDrawImage(ctx, rect, cgImage);
CGContextRestoreGState(ctx);
}
}
@@ -228,20 +314,21 @@ - (NSInteger)topCapHeight
- (TUIImage *)stretchableImageWithLeftCapWidth:(NSInteger)leftCapWidth topCapHeight:(NSInteger)topCapHeight
{
- TUIStretchableImage *i = (TUIStretchableImage *)[TUIStretchableImage imageWithCGImage:_imageRef];
+ TUIStretchableImage *i = (TUIStretchableImage *)[TUIStretchableImage imageWithCGImage:[self CGImage]];
i->leftCapWidth = leftCapWidth;
i->topCapHeight = topCapHeight;
return i;
}
- (NSData *)dataRepresentationForType:(NSString *)type compression:(CGFloat)compressionQuality
{
- if(_imageRef) {
+ CGImageRef cgImage = [self CGImage];
+ if(cgImage) {
NSMutableData *mutableData = [NSMutableData data];
CGImageDestinationRef destination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)mutableData, (__bridge CFStringRef)type, 1, NULL);
NSDictionary *properties = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithFloat:compressionQuality], kCGImageDestinationLossyCompressionQuality, nil];
- CGImageDestinationAddImage(destination, _imageRef, (__bridge CFDictionaryRef)properties);
+ CGImageDestinationAddImage(destination, cgImage, (__bridge CFDictionaryRef)properties);
CGImageDestinationFinalize(destination);
CFRelease(destination);
@@ -257,12 +344,6 @@ - (NSData *)dataRepresentationForType:(NSString *)type compression:(CGFloat)comp
@implementation TUIStretchableImage
-- (void)dealloc
-{
- for(int i = 0; i < 9; ++i)
- ;
-}
-
- (NSInteger)leftCapWidth
{
return leftCapWidth;
@@ -314,14 +395,15 @@ - (NSInteger)topCapHeight
- (void)drawInRect:(CGRect)rect blendMode:(CGBlendMode)blendMode alpha:(CGFloat)alpha
{
- CGSize s = self.size;
- CGFloat t = topCapHeight;
- CGFloat l = leftCapWidth;
-
- if(t*2 > s.height-1) t -= 1;
- if(l*2 > s.width-1) l -= 1;
-
- if(_imageRef) {
+ CGImageRef cgImage = [self CGImage];
+ if(cgImage) {
+ CGSize s = self.size;
+ CGFloat t = topCapHeight;
+ CGFloat l = leftCapWidth;
+
+ if(t*2 > s.height-1) t -= 1;
+ if(l*2 > s.width-1) l -= 1;
+
if(!_flags.haveSlices) {
STRETCH_COORDS(0.0, 0.0, s.width, s.height, t, l, t, l)
#define X(I) slices[I] = [self upsideDownCrop:r[I]];
View
2 lib/UIKit/TUIView+Private.h
@@ -26,3 +26,5 @@
- (void)_updateLayerScaleFactor;
@end
+
+extern CGFloat TUICurrentContextScaleFactor(void);
View
31 lib/UIKit/TUIView.m
@@ -18,6 +18,7 @@
#import "TUIKit.h"
#import "TUIView+Private.h"
#import "TUIViewController.h"
+#import <pthread.h>
NSString * const TUIViewWillMoveToWindowNotification = @"TUIViewWillMoveToWindowNotification";
NSString * const TUIViewDidMoveToWindowNotification = @"TUIViewDidMoveToWindowNotification";
@@ -82,9 +83,13 @@ - (void)setSubviews:(NSArray *)s
}
}
+static pthread_key_t TUICurrentContextScaleFactorTLSKey;
+
+ (void)initialize
{
if(self == [TUIView class]) {
+ pthread_key_create(&TUICurrentContextScaleFactorTLSKey, free);
+
TUIViewCenteredLayout = [^(TUIView *v) {
TUIView *superview = v.superview;
CGRect b = superview.frame;
@@ -286,6 +291,31 @@ - (CGContextRef)_CGContext
return _context.context;
}
+CGFloat TUICurrentContextScaleFactor(void)
+{
+ /*
+ Key is set up in +initialize
+ Use TLS rather than a simple global so drawsInBackground should continue to work (views in the same process may be drawing destined for different windows on different screens with different scale factors).
+ */
+ CGFloat *v = pthread_getspecific(TUICurrentContextScaleFactorTLSKey);
+ if(v)
+ return *v;
+ return 1.0;
+}
+
+static void BogusFunctionToSilenceStaticAnalyzer(void *x) { }
+
+static void TUISetCurrentContextScaleFactor(CGFloat s)
+{
+ CGFloat *v = pthread_getspecific(TUICurrentContextScaleFactorTLSKey);
+ if(!v) {
+ v = malloc(sizeof(CGFloat));
+ pthread_setspecific(TUICurrentContextScaleFactorTLSKey, v);
+ }
+ *v = s;
+ BogusFunctionToSilenceStaticAnalyzer(v);
+}
+
- (void)displayLayer:(CALayer *)layer
{
if(_viewFlags.delegateWillDisplayLayer)
@@ -311,6 +341,7 @@ - (void)displayLayer:(CALayer *)layer
if(_viewFlags.clearsContextBeforeDrawing) \
CGContextClearRect(context, b); \
CGFloat scale = [self.layer respondsToSelector:@selector(contentsScale)] ? self.layer.contentsScale : 1.0f; \
+ TUISetCurrentContextScaleFactor(scale); \
CGContextScaleCTM(context, scale, scale); \
CGContextSetAllowsAntialiasing(context, true); \
CGContextSetShouldAntialias(context, true); \

0 comments on commit 790d850

Please sign in to comment.
Something went wrong with that request. Please try again.