Last active
March 21, 2017 23:26
-
-
Save warpling/fae69d61986c6b7b38f33b83d65de0ed to your computer and use it in GitHub Desktop.
CircularTextView (as seen in the iOS app Blackbox)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// CircularTextView.h | |
// Wormhole | |
// | |
// Created by Ryan McLeod on 5/5/15. | |
// Copyright (c) 2015 Ryan McLeod. All rights reserved. | |
// | |
#import <UIKit/UIKit.h> | |
@interface CircularTextView : UIView | |
@property (strong, nonatomic) NSAttributedString *attributedText; | |
@property CGFloat inset; | |
@end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// CircularTextView.m | |
// Wormhole | |
// | |
// Created by Ryan McLeod on 5/5/15. | |
// Copyright (c) 2015 Ryan McLeod. All rights reserved. | |
// | |
#import "CircularTextView.h" | |
#import <CoreText/CoreText.h> | |
@implementation CircularTextView | |
@synthesize attributedText = _attributedText; | |
- (instancetype)initWithFrame:(CGRect)frame | |
{ | |
self = [super initWithFrame:frame]; | |
if (self) { | |
NSAssert((self.bounds.size.width == self.bounds.size.height), @"%@ can only draw using a square frame!", [self class]); | |
} | |
return self; | |
} | |
- (NSAttributedString*) attributedText { | |
return _attributedText; | |
} | |
- (void) setAttributedText:(NSAttributedString *)attributedText { | |
_attributedText = attributedText; | |
[self setNeedsDisplay]; | |
} | |
// Modified from: https://invasivecode.com/weblog/core-text | |
- (void) drawRect:(CGRect)rect { | |
[super drawRect:rect]; | |
if (!self.attributedText) { | |
return; | |
} | |
CGFloat radius = self.bounds.size.width/2.f; | |
CGContextRef context = UIGraphicsGetCurrentContext(); | |
CGContextSetTextMatrix(context, CGAffineTransformIdentity); | |
CGContextTranslateCTM(context, radius, radius); | |
CGContextScaleCTM(context, 1.0, -1.0); | |
CGContextRotateCTM(context, M_PI_2); | |
CTLineRef line = CTLineCreateWithAttributedString((CFAttributedStringRef)self.attributedText); | |
CFIndex glyphCount = CTLineGetGlyphCount(line); | |
CFArrayRef runArray = CTLineGetGlyphRuns(line); | |
CFIndex runCount = CFArrayGetCount(runArray); | |
NSMutableArray *widthArray = [[NSMutableArray alloc] init]; | |
CFIndex glyphOffset = 0; | |
for (CFIndex i = 0; i < runCount; i++) { | |
CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runArray, i); | |
CFIndex runGlyphCount = CTRunGetGlyphCount((CTRunRef)run); | |
for (CFIndex runGlyphIndex = 0; runGlyphIndex < runGlyphCount; runGlyphIndex++) { | |
NSNumber *widthValue = [NSNumber numberWithDouble:CTRunGetTypographicBounds((CTRunRef)run, CFRangeMake(runGlyphIndex, 1), NULL, NULL, NULL)]; | |
NSAssert(widthValue, @"widthValue was nil"); | |
[widthArray insertObject:widthValue atIndex:(runGlyphIndex + glyphOffset)]; | |
} | |
glyphOffset = runGlyphCount + 1; | |
} | |
CGFloat lineLength = CTLineGetTypographicBounds(line, NULL, NULL, NULL); | |
NSMutableArray *angleArray = [[NSMutableArray alloc] init]; | |
CGFloat prevHalfWidth = [[widthArray objectAtIndex:0] floatValue] / 2.0; | |
NSNumber *angleValue = [NSNumber numberWithDouble:(prevHalfWidth / lineLength) * 2 * M_PI]; | |
NSAssert(angleValue, @"angleValue was nil"); | |
[angleArray insertObject:angleValue atIndex:0]; | |
for (CFIndex lineGlyphIndex = 1; lineGlyphIndex < glyphCount; lineGlyphIndex++) { | |
CGFloat halfWidth = [[widthArray objectAtIndex:lineGlyphIndex] floatValue] / 2.0; | |
CGFloat prevCenterToCenter = prevHalfWidth + halfWidth; | |
// spread over whole circle: | |
// NSNumber *angleValue = [NSNumber numberWithDouble:(prevCenterToCenter / lineLength) * 2 * M_PI]; | |
// actually spaced in a way that makes sense: | |
NSNumber *angleValue = [NSNumber numberWithDouble:(atan2(prevCenterToCenter/2.f, radius) * 2)]; | |
NSAssert(angleValue, @"angleValue was nil"); | |
[angleArray insertObject:angleValue atIndex:lineGlyphIndex]; // 15-4 | |
prevHalfWidth = halfWidth; | |
} | |
// Warning: This will not work as expected for strings with mixed fonts/sizes! | |
// Calculate line height from the first run in the string | |
CTFontRef fontRef = CFAttributedStringGetAttribute((CFAttributedStringRef)self.attributedText, 0, kCTFontAttributeName, NULL); | |
CGFloat lineHeight = 0.0; | |
lineHeight = CTFontGetAscent(fontRef) + CTFontGetDescent(fontRef) + CTFontGetLeading(fontRef); | |
// TODO: use actual height of font | |
CGPoint textPosition = CGPointMake(0.0, radius - lineHeight - self.inset); | |
CGContextSetTextPosition(context, textPosition.x, textPosition.y); | |
glyphOffset = 0; | |
for (CFIndex runIndex = 0; runIndex < runCount; runIndex++) { | |
CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runArray, runIndex); | |
CFIndex runGlyphCount = CTRunGetGlyphCount(run); | |
CTFontRef runFont = CFDictionaryGetValue(CTRunGetAttributes(run), kCTFontAttributeName); | |
for (CFIndex runGlyphIndex = 0; runGlyphIndex < runGlyphCount; runGlyphIndex++) { | |
CFRange glyphRange = CFRangeMake(runGlyphIndex, 1); | |
CGContextRotateCTM(context, -[[angleArray objectAtIndex:(runGlyphIndex + glyphOffset)] floatValue]); // 16-4 | |
CGFloat glyphWidth = [[widthArray objectAtIndex:(runGlyphIndex + glyphOffset)] floatValue]; | |
CGFloat halfGlyphWidth = glyphWidth / 2.0; | |
CGPoint positionForThisGlyph = CGPointMake(textPosition.x - halfGlyphWidth, textPosition.y); // 17-4 | |
textPosition.x -= glyphWidth; | |
CGAffineTransform textMatrix = CTRunGetTextMatrix(run); | |
textMatrix.tx = positionForThisGlyph.x; textMatrix.ty = positionForThisGlyph.y; | |
CGContextSetTextMatrix(context, textMatrix); | |
CGFontRef cgFont = CTFontCopyGraphicsFont(runFont, NULL); | |
CGGlyph glyph; CGPoint position; | |
CTRunGetGlyphs(run, glyphRange, &glyph); | |
CTRunGetPositions(run, glyphRange, &position); | |
CGContextSetFont(context, cgFont); | |
CGContextSetFontSize(context, CTFontGetSize(runFont)); | |
CGContextSetRGBFillColor(context, 0.9, 0.9, 0.9, 1.0); | |
CGContextShowGlyphsAtPositions(context, &glyph, &position, 1); | |
CGFontRelease(cgFont); | |
} | |
glyphOffset += runGlyphCount; | |
} | |
CFRelease(line); | |
} | |
@end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment