Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@warpling
Last active March 21, 2017 23:26
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save warpling/fae69d61986c6b7b38f33b83d65de0ed to your computer and use it in GitHub Desktop.
Save warpling/fae69d61986c6b7b38f33b83d65de0ed to your computer and use it in GitHub Desktop.
CircularTextView (as seen in the iOS app Blackbox)
//
// 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
//
// 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