现有圆上的曲线文本

发布于 2024-09-25 10:08:08 字数 6017 浏览 5 评论 0原文

对于我正在构建的应用程序,我画了 2 个圆圈。一个比另一个大一点。我想在这些线之间弯曲文本,以用于我正在构建的圆形菜单。

我读过大多数有关弯曲文本的内容,您必须将文本分成字符,并以正确的角度单独绘制每个字符(通过旋转您正在绘制的上下文)。

我就是不知道如何为我的角色找到正确的角度和位置。

我附上了一张屏幕截图,显示菜单目前的样子。只有我添加的文本是从 UIImageView 中的图像加载的。

alt text

我希望有人能给我一些关于如何在某些点在白色圆圈中绘制文本的起点。

编辑: 好的,我目前处于这一点:

alt text

我通过使用以下代码完成:

- (UIImage*) createMenuRingWithFrame:(CGRect)frame
{
    CGRect imageSize = CGRectMake(0,0,300,300);
    float perSectionDegrees = 360 / [sections count];
    float totalRotation = 90;
    char* fontName = (char*)[self.menuItemsFont.fontName cStringUsingEncoding:NSASCIIStringEncoding];

    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGContextRef context = CGBitmapContextCreate(NULL, imageSize.size.width, imageSize.size.height, 8, 4 * imageSize.size.width, colorSpace, kCGImageAlphaPremultipliedFirst);

    CGContextSetTextMatrix(context, CGAffineTransformIdentity);
    CGContextSelectFont(context, fontName, 18, kCGEncodingMacRoman);

    CGContextSetRGBFillColor(context, 0, 0, 0, 1);

    CGPoint centerPoint = CGPointMake(imageSize.size.width / 2, imageSize.size.height / 2);
    double radius = (frame.size.width / 2);

    CGContextStrokeEllipseInRect(context, CGRectMake(centerPoint.x - (frame.size.width / 2), centerPoint.y - (frame.size.height / 2), frame.size.width, frame.size.height));

    for (int index = 0; index < [sections count]; index++)
    {
        NSString* menuItemText = [sections objectAtIndex:index];
        CGSize textSize = [menuItemText sizeWithFont:self.menuItemsFont];
        char* menuItemTextChar = (char*)[menuItemText cStringUsingEncoding:NSASCIIStringEncoding];

        float x = centerPoint.x + radius * cos(degreesToRadians(totalRotation));
        float y = centerPoint.y + radius * sin(degreesToRadians(totalRotation));

        CGContextSaveGState(context);

        CGContextTranslateCTM(context, x, y);
        CGContextRotateCTM(context, degreesToRadians(totalRotation - 90));
        CGContextShowTextAtPoint(context, 0 - (textSize.width / 2), 0 - (textSize.height / 2), menuItemTextChar, strlen(menuItemTextChar));

        CGContextRestoreGState(context);

        totalRotation += perSectionDegrees;
    }

    CGImageRef contextImage = CGBitmapContextCreateImage(context);

    CGContextRelease(context);
    CGColorSpaceRelease(colorSpace);

    return [UIImage imageWithCGImage:contextImage];
}

这些是我使用的变量那里:

NSArray* sections = [[NSArray alloc] initWithObjects:@"settings", @"test", @"stats", @"nog iets", @"woei", @"woei2", nil];
self.menuItemsFont = [UIFont fontWithName:@"VAGRounded-Bold" size:18];

单词的旋转似乎是正确的,位置也正确。现在我需要以某种方式找出字母(及其坐标)应该旋转的位置。我需要一些帮助。

编辑:已修复!看看下面的代码!

- (void) drawStringAtContext:(CGContextRef) context string:(NSString*) text atAngle:(float) angle withRadius:(float) radius
{
    CGSize textSize = [text sizeWithFont:self.menuItemsFont];

    float perimeter = 2 * M_PI * radius;
    float textAngle = textSize.width / perimeter * 2 * M_PI;

    angle += textAngle / 2;

    for (int index = 0; index < [text length]; index++)
    {
        NSRange range = {index, 1};
        NSString* letter = [text substringWithRange:range];     
        char* c = (char*)[letter cStringUsingEncoding:NSASCIIStringEncoding];
        CGSize charSize = [letter sizeWithFont:self.menuItemsFont];

        NSLog(@"Char %@ with size: %f x %f", letter, charSize.width, charSize.height);

        float x = radius * cos(angle);
        float y = radius * sin(angle);

        float letterAngle = (charSize.width / perimeter * -2 * M_PI);

        CGContextSaveGState(context);
        CGContextTranslateCTM(context, x, y);
        CGContextRotateCTM(context, (angle - 0.5 * M_PI));
        CGContextShowTextAtPoint(context, 0, 0, c, strlen(c));
        CGContextRestoreGState(context);

        angle += letterAngle;
    }
}

- (UIImage*) createMenuRingWithFrame:(CGRect)frame
{
    CGPoint centerPoint = CGPointMake(frame.size.width / 2, frame.size.height / 2);
    char* fontName = (char*)[self.menuItemsFont.fontName cStringUsingEncoding:NSASCIIStringEncoding];

    CGFloat* ringColorComponents = (float*)CGColorGetComponents(ringColor.CGColor);
    CGFloat* textColorComponents = (float*)CGColorGetComponents(textColor.CGColor);

    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGContextRef context = CGBitmapContextCreate(NULL, frame.size.width, frame.size.height, 8, 4 * frame.size.width, colorSpace, kCGImageAlphaPremultipliedFirst);

    CGContextSetTextMatrix(context, CGAffineTransformIdentity);

    CGContextSelectFont(context, fontName, 18, kCGEncodingMacRoman);
    CGContextSetRGBStrokeColor(context, ringColorComponents[0], ringColorComponents[1], ringColorComponents[2], ringAlpha);
    CGContextSetLineWidth(context, ringWidth);  

    CGContextStrokeEllipseInRect(context, CGRectMake(ringWidth, ringWidth, frame.size.width - (ringWidth * 2), frame.size.height - (ringWidth * 2)));
    CGContextSetRGBFillColor(context, textColorComponents[0], textColorComponents[1], textColorComponents[2], textAlpha);

    CGContextSaveGState(context);
    CGContextTranslateCTM(context, centerPoint.x, centerPoint.y);

    float angleStep = 2 * M_PI / [sections count];
    float angle = degreesToRadians(90);

    textRadius = textRadius - 12;

    for (NSString* text in sections)
    {
        [self drawStringAtContext:context string:text atAngle:angle withRadius:textRadius];
        angle -= angleStep;
    }

    CGContextRestoreGState(context);

    CGImageRef contextImage = CGBitmapContextCreateImage(context);

    CGContextRelease(context);
    CGColorSpaceRelease(colorSpace);

    [self saveImage:[UIImage imageWithCGImage:contextImage] withName:@"test.png"];
    return [UIImage imageWithCGImage:contextImage];

}

For an application I am building I have drawn 2 circles. One a bit bigger than the other. I want to curve text between those lines, for a circular menu I am building.

I read most stuff about curving a text that you have to split up your text in characters and draw each character on it's own with the right angle in mind (by rotating the context you are drawing on).

I just can't wrap my head around on how to get the right angles and positions for my characters.

I included a screenshot on what the menu, at the moment, look like. Only the texts I added by are loaded from an image in an UIImageView.

alt text

I hope someone can get me some starting points on how I can draw the text in the white circle, at certain points.

EDIT:
Ok, I am currently at this point:

alt text

I accomplish by using the following code:

- (UIImage*) createMenuRingWithFrame:(CGRect)frame
{
    CGRect imageSize = CGRectMake(0,0,300,300);
    float perSectionDegrees = 360 / [sections count];
    float totalRotation = 90;
    char* fontName = (char*)[self.menuItemsFont.fontName cStringUsingEncoding:NSASCIIStringEncoding];

    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGContextRef context = CGBitmapContextCreate(NULL, imageSize.size.width, imageSize.size.height, 8, 4 * imageSize.size.width, colorSpace, kCGImageAlphaPremultipliedFirst);

    CGContextSetTextMatrix(context, CGAffineTransformIdentity);
    CGContextSelectFont(context, fontName, 18, kCGEncodingMacRoman);

    CGContextSetRGBFillColor(context, 0, 0, 0, 1);

    CGPoint centerPoint = CGPointMake(imageSize.size.width / 2, imageSize.size.height / 2);
    double radius = (frame.size.width / 2);

    CGContextStrokeEllipseInRect(context, CGRectMake(centerPoint.x - (frame.size.width / 2), centerPoint.y - (frame.size.height / 2), frame.size.width, frame.size.height));

    for (int index = 0; index < [sections count]; index++)
    {
        NSString* menuItemText = [sections objectAtIndex:index];
        CGSize textSize = [menuItemText sizeWithFont:self.menuItemsFont];
        char* menuItemTextChar = (char*)[menuItemText cStringUsingEncoding:NSASCIIStringEncoding];

        float x = centerPoint.x + radius * cos(degreesToRadians(totalRotation));
        float y = centerPoint.y + radius * sin(degreesToRadians(totalRotation));

        CGContextSaveGState(context);

        CGContextTranslateCTM(context, x, y);
        CGContextRotateCTM(context, degreesToRadians(totalRotation - 90));
        CGContextShowTextAtPoint(context, 0 - (textSize.width / 2), 0 - (textSize.height / 2), menuItemTextChar, strlen(menuItemTextChar));

        CGContextRestoreGState(context);

        totalRotation += perSectionDegrees;
    }

    CGImageRef contextImage = CGBitmapContextCreateImage(context);

    CGContextRelease(context);
    CGColorSpaceRelease(colorSpace);

    return [UIImage imageWithCGImage:contextImage];
}

These are the variables I use in there:

NSArray* sections = [[NSArray alloc] initWithObjects:@"settings", @"test", @"stats", @"nog iets", @"woei", @"woei2", nil];
self.menuItemsFont = [UIFont fontWithName:@"VAGRounded-Bold" size:18];

The rotation of the words seem correct, the placement also. Now I need somehow figure out at which rotation the letters (and their coordinates) should be. I could use some help with that.

Edit: Fixed! Check out the following code!

- (void) drawStringAtContext:(CGContextRef) context string:(NSString*) text atAngle:(float) angle withRadius:(float) radius
{
    CGSize textSize = [text sizeWithFont:self.menuItemsFont];

    float perimeter = 2 * M_PI * radius;
    float textAngle = textSize.width / perimeter * 2 * M_PI;

    angle += textAngle / 2;

    for (int index = 0; index < [text length]; index++)
    {
        NSRange range = {index, 1};
        NSString* letter = [text substringWithRange:range];     
        char* c = (char*)[letter cStringUsingEncoding:NSASCIIStringEncoding];
        CGSize charSize = [letter sizeWithFont:self.menuItemsFont];

        NSLog(@"Char %@ with size: %f x %f", letter, charSize.width, charSize.height);

        float x = radius * cos(angle);
        float y = radius * sin(angle);

        float letterAngle = (charSize.width / perimeter * -2 * M_PI);

        CGContextSaveGState(context);
        CGContextTranslateCTM(context, x, y);
        CGContextRotateCTM(context, (angle - 0.5 * M_PI));
        CGContextShowTextAtPoint(context, 0, 0, c, strlen(c));
        CGContextRestoreGState(context);

        angle += letterAngle;
    }
}

- (UIImage*) createMenuRingWithFrame:(CGRect)frame
{
    CGPoint centerPoint = CGPointMake(frame.size.width / 2, frame.size.height / 2);
    char* fontName = (char*)[self.menuItemsFont.fontName cStringUsingEncoding:NSASCIIStringEncoding];

    CGFloat* ringColorComponents = (float*)CGColorGetComponents(ringColor.CGColor);
    CGFloat* textColorComponents = (float*)CGColorGetComponents(textColor.CGColor);

    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGContextRef context = CGBitmapContextCreate(NULL, frame.size.width, frame.size.height, 8, 4 * frame.size.width, colorSpace, kCGImageAlphaPremultipliedFirst);

    CGContextSetTextMatrix(context, CGAffineTransformIdentity);

    CGContextSelectFont(context, fontName, 18, kCGEncodingMacRoman);
    CGContextSetRGBStrokeColor(context, ringColorComponents[0], ringColorComponents[1], ringColorComponents[2], ringAlpha);
    CGContextSetLineWidth(context, ringWidth);  

    CGContextStrokeEllipseInRect(context, CGRectMake(ringWidth, ringWidth, frame.size.width - (ringWidth * 2), frame.size.height - (ringWidth * 2)));
    CGContextSetRGBFillColor(context, textColorComponents[0], textColorComponents[1], textColorComponents[2], textAlpha);

    CGContextSaveGState(context);
    CGContextTranslateCTM(context, centerPoint.x, centerPoint.y);

    float angleStep = 2 * M_PI / [sections count];
    float angle = degreesToRadians(90);

    textRadius = textRadius - 12;

    for (NSString* text in sections)
    {
        [self drawStringAtContext:context string:text atAngle:angle withRadius:textRadius];
        angle -= angleStep;
    }

    CGContextRestoreGState(context);

    CGImageRef contextImage = CGBitmapContextCreateImage(context);

    CGContextRelease(context);
    CGColorSpaceRelease(colorSpace);

    [self saveImage:[UIImage imageWithCGImage:contextImage] withName:@"test.png"];
    return [UIImage imageWithCGImage:contextImage];

}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(12

寄居者 2024-10-02 10:08:08

我改编了Apple的CoreTextArcCocoa示例项目(Tom H在此回复中提到过) )并想我会在这里分享。

我还添加了一些其他功能,例如能够将圆弧大小设置为小于 180 的值,以及将文本颜色和偏移量作为属性(这样您就不必使用巨大的框架来显示整个内容)文本)。

 /*

 File: CoreTextArcView.m (iOS version)

 Abstract: Defines and implements the CoreTextArcView custom UIView subclass to
 draw text on a curve and illustrate best practices with CoreText.

 Based on CoreTextArcView provided by Apple for Mac OS X https://developer.apple.com/library/mac/#samplecode/CoreTextArcCocoa/Introduction/Intro.html

 Ported to iOS (& added color, arcsize features) August 2011 by Alec Vance, Juggleware LLC http://juggleware.com/

 */ 

#import <UIKit/UIKit.h>
#import <CoreText/CoreText.h>


@interface CoreTextArcView : UIView {
@private
    UIFont *            _font;
    NSString *          _string;
    CGFloat             _radius;
    UIColor *           _color;
    CGFloat             _arcSize;
    CGFloat             _shiftH, _shiftV; // horiz & vertical shift

    struct {
        unsigned int    showsGlyphBounds:1;
        unsigned int    showsLineMetrics:1;
        unsigned int    dimsSubstitutedGlyphs:1;
        unsigned int    reserved:29;
    }                   _flags;
}

@property(retain, nonatomic) UIFont *font;
@property(retain, nonatomic) NSString *text;
@property(readonly, nonatomic) NSAttributedString *attributedString;
@property(assign, nonatomic) CGFloat radius;
@property(nonatomic) BOOL showsGlyphBounds;
@property(nonatomic) BOOL showsLineMetrics;
@property(nonatomic) BOOL dimsSubstitutedGlyphs;
@property(retain, nonatomic) UIColor *color;
@property(nonatomic) CGFloat arcSize;
@property(nonatomic) CGFloat shiftH, shiftV;
@end


/*

 File: CoreTextArcView.m (iOS version)

 */ 

#import "CoreTextArcView.h"
#import <AssertMacros.h>
#import <QuartzCore/QuartzCore.h>

#define ARCVIEW_DEBUG_MODE          NO

#define ARCVIEW_DEFAULT_FONT_NAME   @"Helvetica"
#define ARCVIEW_DEFAULT_FONT_SIZE   64.0
#define ARCVIEW_DEFAULT_RADIUS      150.0
#define ARCVIEW_DEFAULT_ARC_SIZE    180.0



@implementation CoreTextArcView

- (id)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        self.font = [UIFont fontWithName:ARCVIEW_DEFAULT_FONT_NAME size:ARCVIEW_DEFAULT_FONT_SIZE];
        self.text = @"Curvaceous Type";
        self.radius = ARCVIEW_DEFAULT_RADIUS;
        self.showsGlyphBounds = NO;
        self.showsLineMetrics = NO;
        self.dimsSubstitutedGlyphs = NO;
        self.color = [UIColor whiteColor];
        self.arcSize = ARCVIEW_DEFAULT_ARC_SIZE;
        self.shiftH = self.shiftV = 0.0f;
    }
    return self;
}

typedef struct GlyphArcInfo {
    CGFloat         width;
    CGFloat         angle;  // in radians
} GlyphArcInfo;

static void PrepareGlyphArcInfo(CTLineRef line, CFIndex glyphCount, GlyphArcInfo *glyphArcInfo, CGFloat arcSizeRad)
{
    NSArray *runArray = (NSArray *)CTLineGetGlyphRuns(line);

    // Examine each run in the line, updating glyphOffset to track how far along the run is in terms of glyphCount.
    CFIndex glyphOffset = 0;
    for (id run in runArray) {
        CFIndex runGlyphCount = CTRunGetGlyphCount((CTRunRef)run);

        // Ask for the width of each glyph in turn.
        CFIndex runGlyphIndex = 0;
        for (; runGlyphIndex < runGlyphCount; runGlyphIndex++) {
            glyphArcInfo[runGlyphIndex + glyphOffset].width = CTRunGetTypographicBounds((CTRunRef)run, CFRangeMake(runGlyphIndex, 1), NULL, NULL, NULL);
        }

        glyphOffset += runGlyphCount;
    }

    double lineLength = CTLineGetTypographicBounds(line, NULL, NULL, NULL);

    CGFloat prevHalfWidth = glyphArcInfo[0].width / 2.0;
    glyphArcInfo[0].angle = (prevHalfWidth / lineLength) * arcSizeRad;

    // Divide the arc into slices such that each one covers the distance from one glyph's center to the next.
    CFIndex lineGlyphIndex = 1;
    for (; lineGlyphIndex < glyphCount; lineGlyphIndex++) {
        CGFloat halfWidth = glyphArcInfo[lineGlyphIndex].width / 2.0;
        CGFloat prevCenterToCenter = prevHalfWidth + halfWidth;

        glyphArcInfo[lineGlyphIndex].angle = (prevCenterToCenter / lineLength) * arcSizeRad;

        prevHalfWidth = halfWidth;
    }
}


// ensure that redraw occurs.
-(void)setText:(NSString *)text{
    [_string release];
    _string = [text retain];

    [self setNeedsDisplay];
}

//set arc size in degrees (180 = half circle)
-(void)setArcSize:(CGFloat)degrees{
    _arcSize = degrees * M_PI/180.0;
}

//get arc size in degrees
-(CGFloat)arcSize{
    return _arcSize * 180.0/M_PI;
}

- (void)drawRect:(CGRect)rect {
    // Don't draw if we don't have a font or string
    if (self.font == NULL || self.text == NULL) 
        return;

    // Initialize the text matrix to a known value
    CGContextRef context = UIGraphicsGetCurrentContext();


    //Reset the transformation
    //Doing this means you have to reset the contentScaleFactor to 1.0
    CGAffineTransform t0 = CGContextGetCTM(context);


    CGFloat xScaleFactor = t0.a > 0 ? t0.a : -t0.a;
    CGFloat yScaleFactor = t0.d > 0 ? t0.d : -t0.d;
    t0 = CGAffineTransformInvert(t0);
    if (xScaleFactor != 1.0 || yScaleFactor != 1.0)
        t0 = CGAffineTransformScale(t0, xScaleFactor, yScaleFactor);

    CGContextConcatCTM(context, t0);

    CGContextSetTextMatrix(context, CGAffineTransformIdentity);

    if(ARCVIEW_DEBUG_MODE){
        // Draw a black background (debug)
        CGContextSetFillColorWithColor(context, [UIColor blackColor].CGColor);
        CGContextFillRect(context, self.layer.bounds);
    }

    NSAttributedString *attStr = self.attributedString;
    CFAttributedStringRef asr = (CFAttributedStringRef)attStr;
    CTLineRef line = CTLineCreateWithAttributedString(asr);
    assert(line != NULL);

    CFIndex glyphCount = CTLineGetGlyphCount(line);
    if (glyphCount == 0) {
        CFRelease(line);
        return;
    }

    GlyphArcInfo *  glyphArcInfo = (GlyphArcInfo*)calloc(glyphCount, sizeof(GlyphArcInfo));
    PrepareGlyphArcInfo(line, glyphCount, glyphArcInfo, _arcSize);

    // Move the origin from the lower left of the view nearer to its center.
    CGContextSaveGState(context);

    CGContextTranslateCTM(context, CGRectGetMidX(rect)+_shiftH, CGRectGetMidY(rect)+_shiftV - self.radius / 2.0);

    if(ARCVIEW_DEBUG_MODE){
        // Stroke the arc in red for verification.
        CGContextBeginPath(context);
        CGContextAddArc(context, 0.0, 0.0, self.radius, M_PI_2+_arcSize/2.0, M_PI_2-_arcSize/2.0, 1);
        CGContextSetRGBStrokeColor(context, 1.0, 0.0, 0.0, 1.0);
        CGContextStrokePath(context);
    }

    // Rotate the context 90 degrees counterclockwise (per 180 degrees)
    CGContextRotateCTM(context, _arcSize/2.0);

    // Now for the actual drawing. The angle offset for each glyph relative to the previous glyph has already been calculated; with that information in hand, draw those glyphs overstruck and centered over one another, making sure to rotate the context after each glyph so the glyphs are spread along a semicircular path.

    CGPoint textPosition = CGPointMake(0.0, self.radius);
    CGContextSetTextPosition(context, textPosition.x, textPosition.y);

    CFArrayRef runArray = CTLineGetGlyphRuns(line);
    CFIndex runCount = CFArrayGetCount(runArray);

    CFIndex glyphOffset = 0;
    CFIndex runIndex = 0;
    for (; runIndex < runCount; runIndex++) {
        CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runArray, runIndex);
        CFIndex runGlyphCount = CTRunGetGlyphCount(run);
        Boolean drawSubstitutedGlyphsManually = false;
        CTFontRef runFont = CFDictionaryGetValue(CTRunGetAttributes(run), kCTFontAttributeName);

        // Determine if we need to draw substituted glyphs manually. Do so if the runFont is not the same as the overall font.
        if (self.dimsSubstitutedGlyphs && ![self.font isEqual:(UIFont *)runFont]) {
            drawSubstitutedGlyphsManually = true;
        }

        CFIndex runGlyphIndex = 0;
        for (; runGlyphIndex < runGlyphCount; runGlyphIndex++) {
            CFRange glyphRange = CFRangeMake(runGlyphIndex, 1);
            CGContextRotateCTM(context, -(glyphArcInfo[runGlyphIndex + glyphOffset].angle));

            // Center this glyph by moving left by half its width.
            CGFloat glyphWidth = glyphArcInfo[runGlyphIndex + glyphOffset].width;
            CGFloat halfGlyphWidth = glyphWidth / 2.0;
            CGPoint positionForThisGlyph = CGPointMake(textPosition.x - halfGlyphWidth, textPosition.y);

            // Glyphs are positioned relative to the text position for the line, so offset text position leftwards by this glyph's width in preparation for the next glyph.
            textPosition.x -= glyphWidth;

            CGAffineTransform textMatrix = CTRunGetTextMatrix(run);
            textMatrix.tx = positionForThisGlyph.x;
            textMatrix.ty = positionForThisGlyph.y;
            CGContextSetTextMatrix(context, textMatrix);

            if (!drawSubstitutedGlyphsManually) {
                CTRunDraw(run, context, glyphRange);
            } 
            else {
                // We need to draw the glyphs manually in this case because we are effectively applying a graphics operation by setting the context fill color. Normally we would use kCTForegroundColorAttributeName, but this does not apply as we don't know the ranges for the colors in advance, and we wanted demonstrate how to manually draw.
                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.25, 0.25, 0.25, 0.5);
                CGContextShowGlyphsAtPositions(context, &glyph, &position, 1);

                CFRelease(cgFont);
            }

            // Draw the glyph bounds 
            if ((self.showsGlyphBounds) != 0) {
                CGRect glyphBounds = CTRunGetImageBounds(run, context, glyphRange);

                CGContextSetRGBStrokeColor(context, 0.0, 0.0, 1.0, 1.0);
                CGContextStrokeRect(context, glyphBounds);
            }
            // Draw the bounding boxes defined by the line metrics
            if ((self.showsLineMetrics) != 0) {
                CGRect lineMetrics;
                CGFloat ascent, descent;

                CTRunGetTypographicBounds(run, glyphRange, &ascent, &descent, NULL);

                // The glyph is centered around the y-axis
                lineMetrics.origin.x = -halfGlyphWidth;
                lineMetrics.origin.y = positionForThisGlyph.y - descent;
                lineMetrics.size.width = glyphWidth; 
                lineMetrics.size.height = ascent + descent;

                CGContextSetRGBStrokeColor(context, 0.0, 1.0, 0.0, 1.0);
                CGContextStrokeRect(context, lineMetrics);
            }
        }

        glyphOffset += runGlyphCount;
    }

    CGContextRestoreGState(context);

    free(glyphArcInfo);
    CFRelease(line);    



}

-(void)dealloc
{
    [_font release];
    [_string release];
    [_color release];
    [super dealloc]
}

@synthesize font = _font;
@synthesize text = _string;
@synthesize radius = _radius;
@synthesize color = _color;
@synthesize arcSize = _arcSize;
@synthesize shiftH = _shiftH;
@synthesize shiftV = _shiftV;

@dynamic attributedString;
- (NSAttributedString *)attributedString {
    // Create an attributed string with the current font and string.
    assert(self.font != nil);
    assert(self.text != nil);

    // Create our attributes...

    // font
    CTFontRef fontRef = CTFontCreateWithName((CFStringRef)self.font.fontName, self.font.pointSize, NULL);

    // color
    CGColorRef colorRef = self.color.CGColor;

    // pack it into attributes dictionary

    NSDictionary *attributesDict = [NSDictionary dictionaryWithObjectsAndKeys:
                                    (id)fontRef, (id)kCTFontAttributeName,
                                    colorRef, (id)kCTForegroundColorAttributeName,
                                    nil];
    assert(attributesDict != nil);


    // Create the attributed string
    NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:self.text attributes:attributesDict];

    CFRelease(fontRef);

    return [attrString autorelease];
}

@dynamic showsGlyphBounds;
- (BOOL)showsGlyphBounds {
    return _flags.showsGlyphBounds;
}

- (void)setShowsGlyphBounds:(BOOL)show {
    _flags.showsGlyphBounds = show ? 1 : 0;
}

@dynamic showsLineMetrics;
- (BOOL)showsLineMetrics {
    return _flags.showsLineMetrics;
}

- (void)setShowsLineMetrics:(BOOL)show {
    _flags.showsLineMetrics = show ? 1 : 0;
}

@dynamic dimsSubstitutedGlyphs;
- (BOOL)dimsSubstitutedGlyphs {
    return _flags.dimsSubstitutedGlyphs;
}

- (void)setDimsSubstitutedGlyphs:(BOOL)dim {
    _flags.dimsSubstitutedGlyphs = dim ? 1 : 0;
}

@end

I adapted Apple's CoreTextArcCocoa sample project (mentioned by Tom H in this reply) and thought I'd share it here.

I added a few other features as well, such as the ability to set the arc size to something smaller than 180, and the text color and offset shift as properties (so that you don't have to have a huge frame to show the whole text).

 /*

 File: CoreTextArcView.m (iOS version)

 Abstract: Defines and implements the CoreTextArcView custom UIView subclass to
 draw text on a curve and illustrate best practices with CoreText.

 Based on CoreTextArcView provided by Apple for Mac OS X https://developer.apple.com/library/mac/#samplecode/CoreTextArcCocoa/Introduction/Intro.html

 Ported to iOS (& added color, arcsize features) August 2011 by Alec Vance, Juggleware LLC http://juggleware.com/

 */ 

#import <UIKit/UIKit.h>
#import <CoreText/CoreText.h>


@interface CoreTextArcView : UIView {
@private
    UIFont *            _font;
    NSString *          _string;
    CGFloat             _radius;
    UIColor *           _color;
    CGFloat             _arcSize;
    CGFloat             _shiftH, _shiftV; // horiz & vertical shift

    struct {
        unsigned int    showsGlyphBounds:1;
        unsigned int    showsLineMetrics:1;
        unsigned int    dimsSubstitutedGlyphs:1;
        unsigned int    reserved:29;
    }                   _flags;
}

@property(retain, nonatomic) UIFont *font;
@property(retain, nonatomic) NSString *text;
@property(readonly, nonatomic) NSAttributedString *attributedString;
@property(assign, nonatomic) CGFloat radius;
@property(nonatomic) BOOL showsGlyphBounds;
@property(nonatomic) BOOL showsLineMetrics;
@property(nonatomic) BOOL dimsSubstitutedGlyphs;
@property(retain, nonatomic) UIColor *color;
@property(nonatomic) CGFloat arcSize;
@property(nonatomic) CGFloat shiftH, shiftV;
@end


/*

 File: CoreTextArcView.m (iOS version)

 */ 

#import "CoreTextArcView.h"
#import <AssertMacros.h>
#import <QuartzCore/QuartzCore.h>

#define ARCVIEW_DEBUG_MODE          NO

#define ARCVIEW_DEFAULT_FONT_NAME   @"Helvetica"
#define ARCVIEW_DEFAULT_FONT_SIZE   64.0
#define ARCVIEW_DEFAULT_RADIUS      150.0
#define ARCVIEW_DEFAULT_ARC_SIZE    180.0



@implementation CoreTextArcView

- (id)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        self.font = [UIFont fontWithName:ARCVIEW_DEFAULT_FONT_NAME size:ARCVIEW_DEFAULT_FONT_SIZE];
        self.text = @"Curvaceous Type";
        self.radius = ARCVIEW_DEFAULT_RADIUS;
        self.showsGlyphBounds = NO;
        self.showsLineMetrics = NO;
        self.dimsSubstitutedGlyphs = NO;
        self.color = [UIColor whiteColor];
        self.arcSize = ARCVIEW_DEFAULT_ARC_SIZE;
        self.shiftH = self.shiftV = 0.0f;
    }
    return self;
}

typedef struct GlyphArcInfo {
    CGFloat         width;
    CGFloat         angle;  // in radians
} GlyphArcInfo;

static void PrepareGlyphArcInfo(CTLineRef line, CFIndex glyphCount, GlyphArcInfo *glyphArcInfo, CGFloat arcSizeRad)
{
    NSArray *runArray = (NSArray *)CTLineGetGlyphRuns(line);

    // Examine each run in the line, updating glyphOffset to track how far along the run is in terms of glyphCount.
    CFIndex glyphOffset = 0;
    for (id run in runArray) {
        CFIndex runGlyphCount = CTRunGetGlyphCount((CTRunRef)run);

        // Ask for the width of each glyph in turn.
        CFIndex runGlyphIndex = 0;
        for (; runGlyphIndex < runGlyphCount; runGlyphIndex++) {
            glyphArcInfo[runGlyphIndex + glyphOffset].width = CTRunGetTypographicBounds((CTRunRef)run, CFRangeMake(runGlyphIndex, 1), NULL, NULL, NULL);
        }

        glyphOffset += runGlyphCount;
    }

    double lineLength = CTLineGetTypographicBounds(line, NULL, NULL, NULL);

    CGFloat prevHalfWidth = glyphArcInfo[0].width / 2.0;
    glyphArcInfo[0].angle = (prevHalfWidth / lineLength) * arcSizeRad;

    // Divide the arc into slices such that each one covers the distance from one glyph's center to the next.
    CFIndex lineGlyphIndex = 1;
    for (; lineGlyphIndex < glyphCount; lineGlyphIndex++) {
        CGFloat halfWidth = glyphArcInfo[lineGlyphIndex].width / 2.0;
        CGFloat prevCenterToCenter = prevHalfWidth + halfWidth;

        glyphArcInfo[lineGlyphIndex].angle = (prevCenterToCenter / lineLength) * arcSizeRad;

        prevHalfWidth = halfWidth;
    }
}


// ensure that redraw occurs.
-(void)setText:(NSString *)text{
    [_string release];
    _string = [text retain];

    [self setNeedsDisplay];
}

//set arc size in degrees (180 = half circle)
-(void)setArcSize:(CGFloat)degrees{
    _arcSize = degrees * M_PI/180.0;
}

//get arc size in degrees
-(CGFloat)arcSize{
    return _arcSize * 180.0/M_PI;
}

- (void)drawRect:(CGRect)rect {
    // Don't draw if we don't have a font or string
    if (self.font == NULL || self.text == NULL) 
        return;

    // Initialize the text matrix to a known value
    CGContextRef context = UIGraphicsGetCurrentContext();


    //Reset the transformation
    //Doing this means you have to reset the contentScaleFactor to 1.0
    CGAffineTransform t0 = CGContextGetCTM(context);


    CGFloat xScaleFactor = t0.a > 0 ? t0.a : -t0.a;
    CGFloat yScaleFactor = t0.d > 0 ? t0.d : -t0.d;
    t0 = CGAffineTransformInvert(t0);
    if (xScaleFactor != 1.0 || yScaleFactor != 1.0)
        t0 = CGAffineTransformScale(t0, xScaleFactor, yScaleFactor);

    CGContextConcatCTM(context, t0);

    CGContextSetTextMatrix(context, CGAffineTransformIdentity);

    if(ARCVIEW_DEBUG_MODE){
        // Draw a black background (debug)
        CGContextSetFillColorWithColor(context, [UIColor blackColor].CGColor);
        CGContextFillRect(context, self.layer.bounds);
    }

    NSAttributedString *attStr = self.attributedString;
    CFAttributedStringRef asr = (CFAttributedStringRef)attStr;
    CTLineRef line = CTLineCreateWithAttributedString(asr);
    assert(line != NULL);

    CFIndex glyphCount = CTLineGetGlyphCount(line);
    if (glyphCount == 0) {
        CFRelease(line);
        return;
    }

    GlyphArcInfo *  glyphArcInfo = (GlyphArcInfo*)calloc(glyphCount, sizeof(GlyphArcInfo));
    PrepareGlyphArcInfo(line, glyphCount, glyphArcInfo, _arcSize);

    // Move the origin from the lower left of the view nearer to its center.
    CGContextSaveGState(context);

    CGContextTranslateCTM(context, CGRectGetMidX(rect)+_shiftH, CGRectGetMidY(rect)+_shiftV - self.radius / 2.0);

    if(ARCVIEW_DEBUG_MODE){
        // Stroke the arc in red for verification.
        CGContextBeginPath(context);
        CGContextAddArc(context, 0.0, 0.0, self.radius, M_PI_2+_arcSize/2.0, M_PI_2-_arcSize/2.0, 1);
        CGContextSetRGBStrokeColor(context, 1.0, 0.0, 0.0, 1.0);
        CGContextStrokePath(context);
    }

    // Rotate the context 90 degrees counterclockwise (per 180 degrees)
    CGContextRotateCTM(context, _arcSize/2.0);

    // Now for the actual drawing. The angle offset for each glyph relative to the previous glyph has already been calculated; with that information in hand, draw those glyphs overstruck and centered over one another, making sure to rotate the context after each glyph so the glyphs are spread along a semicircular path.

    CGPoint textPosition = CGPointMake(0.0, self.radius);
    CGContextSetTextPosition(context, textPosition.x, textPosition.y);

    CFArrayRef runArray = CTLineGetGlyphRuns(line);
    CFIndex runCount = CFArrayGetCount(runArray);

    CFIndex glyphOffset = 0;
    CFIndex runIndex = 0;
    for (; runIndex < runCount; runIndex++) {
        CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runArray, runIndex);
        CFIndex runGlyphCount = CTRunGetGlyphCount(run);
        Boolean drawSubstitutedGlyphsManually = false;
        CTFontRef runFont = CFDictionaryGetValue(CTRunGetAttributes(run), kCTFontAttributeName);

        // Determine if we need to draw substituted glyphs manually. Do so if the runFont is not the same as the overall font.
        if (self.dimsSubstitutedGlyphs && ![self.font isEqual:(UIFont *)runFont]) {
            drawSubstitutedGlyphsManually = true;
        }

        CFIndex runGlyphIndex = 0;
        for (; runGlyphIndex < runGlyphCount; runGlyphIndex++) {
            CFRange glyphRange = CFRangeMake(runGlyphIndex, 1);
            CGContextRotateCTM(context, -(glyphArcInfo[runGlyphIndex + glyphOffset].angle));

            // Center this glyph by moving left by half its width.
            CGFloat glyphWidth = glyphArcInfo[runGlyphIndex + glyphOffset].width;
            CGFloat halfGlyphWidth = glyphWidth / 2.0;
            CGPoint positionForThisGlyph = CGPointMake(textPosition.x - halfGlyphWidth, textPosition.y);

            // Glyphs are positioned relative to the text position for the line, so offset text position leftwards by this glyph's width in preparation for the next glyph.
            textPosition.x -= glyphWidth;

            CGAffineTransform textMatrix = CTRunGetTextMatrix(run);
            textMatrix.tx = positionForThisGlyph.x;
            textMatrix.ty = positionForThisGlyph.y;
            CGContextSetTextMatrix(context, textMatrix);

            if (!drawSubstitutedGlyphsManually) {
                CTRunDraw(run, context, glyphRange);
            } 
            else {
                // We need to draw the glyphs manually in this case because we are effectively applying a graphics operation by setting the context fill color. Normally we would use kCTForegroundColorAttributeName, but this does not apply as we don't know the ranges for the colors in advance, and we wanted demonstrate how to manually draw.
                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.25, 0.25, 0.25, 0.5);
                CGContextShowGlyphsAtPositions(context, &glyph, &position, 1);

                CFRelease(cgFont);
            }

            // Draw the glyph bounds 
            if ((self.showsGlyphBounds) != 0) {
                CGRect glyphBounds = CTRunGetImageBounds(run, context, glyphRange);

                CGContextSetRGBStrokeColor(context, 0.0, 0.0, 1.0, 1.0);
                CGContextStrokeRect(context, glyphBounds);
            }
            // Draw the bounding boxes defined by the line metrics
            if ((self.showsLineMetrics) != 0) {
                CGRect lineMetrics;
                CGFloat ascent, descent;

                CTRunGetTypographicBounds(run, glyphRange, &ascent, &descent, NULL);

                // The glyph is centered around the y-axis
                lineMetrics.origin.x = -halfGlyphWidth;
                lineMetrics.origin.y = positionForThisGlyph.y - descent;
                lineMetrics.size.width = glyphWidth; 
                lineMetrics.size.height = ascent + descent;

                CGContextSetRGBStrokeColor(context, 0.0, 1.0, 0.0, 1.0);
                CGContextStrokeRect(context, lineMetrics);
            }
        }

        glyphOffset += runGlyphCount;
    }

    CGContextRestoreGState(context);

    free(glyphArcInfo);
    CFRelease(line);    



}

-(void)dealloc
{
    [_font release];
    [_string release];
    [_color release];
    [super dealloc]
}

@synthesize font = _font;
@synthesize text = _string;
@synthesize radius = _radius;
@synthesize color = _color;
@synthesize arcSize = _arcSize;
@synthesize shiftH = _shiftH;
@synthesize shiftV = _shiftV;

@dynamic attributedString;
- (NSAttributedString *)attributedString {
    // Create an attributed string with the current font and string.
    assert(self.font != nil);
    assert(self.text != nil);

    // Create our attributes...

    // font
    CTFontRef fontRef = CTFontCreateWithName((CFStringRef)self.font.fontName, self.font.pointSize, NULL);

    // color
    CGColorRef colorRef = self.color.CGColor;

    // pack it into attributes dictionary

    NSDictionary *attributesDict = [NSDictionary dictionaryWithObjectsAndKeys:
                                    (id)fontRef, (id)kCTFontAttributeName,
                                    colorRef, (id)kCTForegroundColorAttributeName,
                                    nil];
    assert(attributesDict != nil);


    // Create the attributed string
    NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:self.text attributes:attributesDict];

    CFRelease(fontRef);

    return [attrString autorelease];
}

@dynamic showsGlyphBounds;
- (BOOL)showsGlyphBounds {
    return _flags.showsGlyphBounds;
}

- (void)setShowsGlyphBounds:(BOOL)show {
    _flags.showsGlyphBounds = show ? 1 : 0;
}

@dynamic showsLineMetrics;
- (BOOL)showsLineMetrics {
    return _flags.showsLineMetrics;
}

- (void)setShowsLineMetrics:(BOOL)show {
    _flags.showsLineMetrics = show ? 1 : 0;
}

@dynamic dimsSubstitutedGlyphs;
- (BOOL)dimsSubstitutedGlyphs {
    return _flags.dimsSubstitutedGlyphs;
}

- (void)setDimsSubstitutedGlyphs:(BOOL)dim {
    _flags.dimsSubstitutedGlyphs = dim ? 1 : 0;
}

@end
意犹 2024-10-02 10:08:08

我试图在纸上快速计算出来,所以我可能是错的:)

将字符串的长度转换为 单位圆。因此(字符串长度/圆周长)*2Pi。现在您就得到了整个字符串的角度(以弧度为单位)。 (这是字符串开头和结尾之间的角度)

对于单独的字母,您可以执行相同的操作来获取各个字母的角度(以弧度为单位)(使用字母宽度)

一旦获得以弧度为单位的角度,您就可以计算出字母的 x 和 y 位置(和旋转)。

奖励:对于均匀的间距,您甚至可以计算出所有字符串的总长度与整个周长之间的比率。并在字符串之间平均分配剩余空间。

更新
我使用 html5/canvas 制作了概念证明,因此请使用合适的浏览器查看它:) 你应该能够移植它。 (请注意,代码没有注释)
wtf:代码在 Chrome 调试控制台打开时运行良好,在关闭时运行失败。 (解决方法:打开 chrome 控制台:ctrl-shift-j 并重新加载页面:f5); FF3.6.8 似乎表现不错,但字母“跳舞”。

I tried to work it out quickly on paper, so i may be wrong :)

Convert the length of the string into units on the UnitCircle. Thus (string.lenght/ circle perimeter)*2Pi. You now have the angle in radians for the whole string. (That is the angle between start and end of the string)

For the separate letters you could do the same to get the angle (in radians) for individual letters (using letter widths)

Once you have the angle in radians you can work out the x and y position (and rotation) of the letters.

Bonus: for even spacing you could even work out the ratio between the total length of all strings and the whole perimeter. And divide the remaining space equally between the string.

Update
I made a proof of concept using html5/canvas, so view it with a decent browser :) You should be able to port it. (mind you, the code isn't commented)
wtf: the code runs fine with the chrome debug console open, and fails when it is closed. (workaround: open chrome console: ctrl-shift-j and reload the page: f5); FF3.6.8 seems to do fine, but the letters 'dance'.

疏忽 2024-10-02 10:08:08

为了节省您一些时间,
这是我在 CoreTextArcView 中发现的内容,它公开了

- (id)initWithFrame:(CGRect)frame font:(UIFont *)font text:(NSString *)text radius:(float)radius arcSize:(float)arcSize color:(UIColor *)color;
  (x,y)<---------------     w             --------------->
      +--------------------------------------------------+
     ^|                                                  |  <--
     ||                                                  |  frame
     ||                                                  |
     ||                 VED L A BEL                      |
     ||             CU R            HE                   |
     ||           xx                   RE  x             |
      |          xx                        xxx           |
      |        xxx xx                     x   xxx        |
    h |      xxx    xx                  xxx     xx       |
      |      x       xxx         <-----------------------------
      |     xx         xx   xxxxxxx   xx           x     |  arcSize :
     ||    xx            xxx       xxx             xx    |  opening angle
     ||    x              xxx      xx               x    |  in degrees
     ||   xx                xx  xxx                 x    |
     ||   x  <---- r  ----->   x                    x    |
     ||   x                      (xc,yc)            x    |
     ||   x                             <-----------------------
     ||   x                                        xx    |  xc = x + w /2
     v+---xx--------------------------------------xx-----+  yc = y + h /2 + r /2
           xx                                    xx
            x                                   xx
            xxx                                xx
              xxx                            xxx
                xxxx                      xxxx
                   xxxxx              xxxxx
                        xxxxxxxxxxxxxxx

这对 r > 有效。 0 且 arcsize > 0。

To save you some time,
here is what i've found for the CoreTextArcView that exposes

- (id)initWithFrame:(CGRect)frame font:(UIFont *)font text:(NSString *)text radius:(float)radius arcSize:(float)arcSize color:(UIColor *)color;
  (x,y)<---------------     w             --------------->
      +--------------------------------------------------+
     ^|                                                  |  <--
     ||                                                  |  frame
     ||                                                  |
     ||                 VED L A BEL                      |
     ||             CU R            HE                   |
     ||           xx                   RE  x             |
      |          xx                        xxx           |
      |        xxx xx                     x   xxx        |
    h |      xxx    xx                  xxx     xx       |
      |      x       xxx         <-----------------------------
      |     xx         xx   xxxxxxx   xx           x     |  arcSize :
     ||    xx            xxx       xxx             xx    |  opening angle
     ||    x              xxx      xx               x    |  in degrees
     ||   xx                xx  xxx                 x    |
     ||   x  <---- r  ----->   x                    x    |
     ||   x                      (xc,yc)            x    |
     ||   x                             <-----------------------
     ||   x                                        xx    |  xc = x + w /2
     v+---xx--------------------------------------xx-----+  yc = y + h /2 + r /2
           xx                                    xx
            x                                   xx
            xxx                                xx
              xxx                            xxx
                xxxx                      xxxx
                   xxxxx              xxxxx
                        xxxxxxxxxxxxxxx

this is valid for r > 0 and arcsize > 0.

蓝眸 2024-10-02 10:08:08

这是我以预定义角度(以弧度为单位)在图层上绘制弯曲属性字符串的方法:

[self drawCurvedStringOnLayer:self.layer withAttributedText:incident atAngle:angle withRadius:300];

字符串也会在弧的底部区域自动反转。

输入图片此处描述

- (void)drawCurvedStringOnLayer:(CALayer *)layer
             withAttributedText:(NSAttributedString *)text
                        atAngle:(float)angle
                     withRadius:(float)radius {

    // angle in radians

    CGSize textSize = CGRectIntegral([text boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)
                                                        options:(NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading)
                                                        context:nil]).size;

    float perimeter = 2 * M_PI * radius;
    float textAngle = (textSize.width / perimeter * 2 * M_PI); 

    float textRotation;
    float textDirection;
    if (angle > degreesToRadians(10) && angle < degreesToRadians(170)) {
        //bottom string
        textRotation = 0.5 * M_PI ;
        textDirection = - 2 * M_PI;
        angle += textAngle / 2;
    } else {
        //top string
        textRotation = 1.5 * M_PI ;
        textDirection = 2 * M_PI;
        angle -= textAngle / 2;
    }

    for (int c = 0; c < text.length; c++) {
        NSRange range = {c, 1};
        NSAttributedString* letter = [text attributedSubstringFromRange:range];
        CGSize charSize = CGRectIntegral([letter boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)
                                                              options:(NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading)
                                                              context:nil]).size;

        float letterAngle = ( (charSize.width / perimeter) * textDirection );

        float x = radius * cos(angle + (letterAngle/2));
        float y = radius * sin(angle + (letterAngle/2));

        CATextLayer *singleChar = [self drawTextOnLayer:layer
                                           withText:letter
                                              frame:CGRectMake(layer.frame.size.width/2 - charSize.width/2 + x,
                                                               layer.frame.size.height/2 - charSize.height/2 + y,
                                                               charSize.width, charSize.height)
                                            bgColor:nil
                                            opacity:1];

        singleChar.transform = CATransform3DMakeAffineTransform( CGAffineTransformMakeRotation(angle - textRotation) );

        angle += letterAngle;
    }
}


- (CATextLayer *)drawTextOnLayer:(CALayer *)layer
                        withText:(NSAttributedString *)text
                           frame:(CGRect)frame
                         bgColor:(UIColor *)bgColor
                         opacity:(float)opacity {

    CATextLayer *textLayer = [[CATextLayer alloc] init];
    [textLayer setFrame:frame];
    [textLayer setString:text];
    [textLayer setAlignmentMode:kCAAlignmentCenter];
    [textLayer setBackgroundColor:bgColor.CGColor];
    [textLayer setContentsScale:[UIScreen mainScreen].scale];
    [textLayer setOpacity:opacity];
    [layer addSublayer:textLayer];
    return textLayer;
}


/** Degrees to Radian **/
#define degreesToRadians(degrees) (( degrees ) / 180.0 * M_PI )

/** Radians to Degrees **/
#define radiansToDegrees(radians) (( radians ) * ( 180.0 / M_PI ) )

This is my method to draw curved attributed strings on layers, at a predefined angle (in radians):

[self drawCurvedStringOnLayer:self.layer withAttributedText:incident atAngle:angle withRadius:300];

The string is also automatically reversed on the bottom area of the arc.

enter image description here

- (void)drawCurvedStringOnLayer:(CALayer *)layer
             withAttributedText:(NSAttributedString *)text
                        atAngle:(float)angle
                     withRadius:(float)radius {

    // angle in radians

    CGSize textSize = CGRectIntegral([text boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)
                                                        options:(NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading)
                                                        context:nil]).size;

    float perimeter = 2 * M_PI * radius;
    float textAngle = (textSize.width / perimeter * 2 * M_PI); 

    float textRotation;
    float textDirection;
    if (angle > degreesToRadians(10) && angle < degreesToRadians(170)) {
        //bottom string
        textRotation = 0.5 * M_PI ;
        textDirection = - 2 * M_PI;
        angle += textAngle / 2;
    } else {
        //top string
        textRotation = 1.5 * M_PI ;
        textDirection = 2 * M_PI;
        angle -= textAngle / 2;
    }

    for (int c = 0; c < text.length; c++) {
        NSRange range = {c, 1};
        NSAttributedString* letter = [text attributedSubstringFromRange:range];
        CGSize charSize = CGRectIntegral([letter boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)
                                                              options:(NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading)
                                                              context:nil]).size;

        float letterAngle = ( (charSize.width / perimeter) * textDirection );

        float x = radius * cos(angle + (letterAngle/2));
        float y = radius * sin(angle + (letterAngle/2));

        CATextLayer *singleChar = [self drawTextOnLayer:layer
                                           withText:letter
                                              frame:CGRectMake(layer.frame.size.width/2 - charSize.width/2 + x,
                                                               layer.frame.size.height/2 - charSize.height/2 + y,
                                                               charSize.width, charSize.height)
                                            bgColor:nil
                                            opacity:1];

        singleChar.transform = CATransform3DMakeAffineTransform( CGAffineTransformMakeRotation(angle - textRotation) );

        angle += letterAngle;
    }
}


- (CATextLayer *)drawTextOnLayer:(CALayer *)layer
                        withText:(NSAttributedString *)text
                           frame:(CGRect)frame
                         bgColor:(UIColor *)bgColor
                         opacity:(float)opacity {

    CATextLayer *textLayer = [[CATextLayer alloc] init];
    [textLayer setFrame:frame];
    [textLayer setString:text];
    [textLayer setAlignmentMode:kCAAlignmentCenter];
    [textLayer setBackgroundColor:bgColor.CGColor];
    [textLayer setContentsScale:[UIScreen mainScreen].scale];
    [textLayer setOpacity:opacity];
    [layer addSublayer:textLayer];
    return textLayer;
}


/** Degrees to Radian **/
#define degreesToRadians(degrees) (( degrees ) / 180.0 * M_PI )

/** Radians to Degrees **/
#define radiansToDegrees(radians) (( radians ) * ( 180.0 / M_PI ) )
转角预定愛 2024-10-02 10:08:08

查看此 Apple 示例项目:CoreTextArcCocoa

演示使用Core Text进行绘图
Cocoa 中沿着弧线的文本
应用。同样,这个样本
说明了如何使用可可
字体面板用于接收字体设置
Core Text 可以使用它来
选择用于绘图的字体。

CoreText 在 iOS 中也可用,因此您应该能够实现类似的功能。

Check out this Apple sample project: CoreTextArcCocoa

Demonstrates using Core Text to draw
text along an arc in a Cocoa
application. As well, this sample
illustrates how you can use the Cocoa
font panel to receive font settings
that can be used by Core Text to
select the font used for drawing.

CoreText is also available in iOS so you should be able to implement something similar.

淡淡绿茶香 2024-10-02 10:08:08

我尝试了上面提到的 git 项目,正如 ZpaceZombor 所说,有一个错误的偏移量,

CGContextTranslateCTM(context, CGRectGetMidX(rect)+_shiftH, CGRectGetMidY(rect)+_shiftV - self.radius / 2.0);

我只是将

CGContextTranslateCTM(context, CGRectGetMidX(rect)+_shiftH, CGRectGetMidY(rect)+_shiftV);

半径设置为容器视图的宽度和高度之间的最小值,所以我将弧大小设置为

我任意更改了该行

CGContextRotateCTM(context, _arcSize/2.0);

CGContextRotateCTM(context, M_PI_2);

我已将 init 方法更改为

- (id)initWithFrame:(CGRect)frame font:(UIFont *)font text:(NSString *)text color:(UIColor *)color{

    self = [super initWithFrame:frame];
    if (self) {
        self.font = font;
        self.text = text;
        self.radius = -1 * (frame.size.width > frame.size.height ? frame.size.height / 2 : frame.size.width / 2);
        _arcSize = 2* M_PI;
        self.showsGlyphBounds = NO;
        self.showsLineMetrics = NO;
        self.dimsSubstitutedGlyphs = NO;
        self.color = color;
        self.shiftH = self.shiftV = 0.0f;

    }
    return self;
}

经过多次尝试,我对函数 PrepareGlyphArcInfo 进行了此修改

// this constants come from a single case ( fontSize = 22 | circle diameter = 250px | lower circle diameter 50px | 0.12f is a proportional acceptable value of 250px diameter | 0.18f is a proportional acceptable value of 50px | 0.035f is a proportional acceptable value of "big" chars
#define kReferredCharSpacing 0.12f
#define kReferredFontSize 22.f
#define kReferredMajorDiameter 250.f
#define kReferredMinorDiameter 50.f
#define kReferredMinorSpacingFix 0.18f
#define kReferredBigCharSpacingFix  0.035f

static void PrepareGlyphArcInfo(UIFont* font,CGFloat containerRadius,CTLineRef line, CFIndex glyphCount, GlyphArcInfo *glyphArcInfo, CGFloat arcSizeRad)
{
    NSArray *runArray = (NSArray *)CTLineGetGlyphRuns(line);

    CGFloat curMaxTypoWidth = 0.f;
    CGFloat curMinTypoWidth = 0.f;

    // Examine each run in the line, updating glyphOffset to track how far along the run is in terms of glyphCount.
    CFIndex glyphOffset = 0;
    for (id run in runArray) {
        CFIndex runGlyphCount = CTRunGetGlyphCount((CTRunRef)run);

            // Ask for the width of each glyph in turn.
        CFIndex runGlyphIndex = 0;
        for (; runGlyphIndex < runGlyphCount; runGlyphIndex++) {
            glyphArcInfo[runGlyphIndex + glyphOffset].width = CTRunGetTypographicBounds((CTRunRef)run, CFRangeMake(runGlyphIndex, 1), NULL, NULL, NULL);

            if (curMaxTypoWidth < glyphArcInfo[runGlyphIndex + glyphOffset].width)
                curMaxTypoWidth = glyphArcInfo[runGlyphIndex + glyphOffset].width;

            if (curMinTypoWidth > glyphArcInfo[runGlyphIndex + glyphOffset].width || curMinTypoWidth == 0)
                curMinTypoWidth = glyphArcInfo[runGlyphIndex + glyphOffset].width;

        }

        glyphOffset += runGlyphCount;
    }

    //double lineLength = CTLineGetTypographicBounds(line, NULL, NULL, NULL);

    glyphArcInfo[0].angle = M_PI_2; // start at the bottom circle

    CFIndex lineGlyphIndex = 1;

    // based on font size. (supposing that with fontSize = 22 we could use 0.12)
    CGFloat maxCharSpacing = font.pointSize * kReferredCharSpacing / kReferredFontSize;

    // for diameter minor than referred 250
    if ((fabsf(containerRadius)*2) < kReferredMajorDiameter)
        maxCharSpacing = maxCharSpacing + kReferredMinorSpacingFix * kReferredMinorDiameter / (fabsf(containerRadius)*2);

    CGFloat startAngle = fabsf(glyphArcInfo[0].angle);
    CGFloat endAngle = startAngle;

    for (; lineGlyphIndex < glyphCount; lineGlyphIndex++) {

        CGFloat deltaWidth = curMaxTypoWidth - glyphArcInfo[lineGlyphIndex].width;

        // fix applied to large characters like uppercase letters or symbols
        CGFloat bigCharFix = (glyphArcInfo[lineGlyphIndex-1].width == curMaxTypoWidth || (glyphArcInfo[lineGlyphIndex-1].width+2) >= curMaxTypoWidth ? kReferredBigCharSpacingFix : 0 );

        glyphArcInfo[lineGlyphIndex].angle = - (maxCharSpacing * (glyphArcInfo[lineGlyphIndex].width + deltaWidth ) / curMaxTypoWidth) - bigCharFix;

        endAngle += fabsf(glyphArcInfo[lineGlyphIndex].angle);
    }

    // center text to bottom
    glyphArcInfo[0].angle = glyphArcInfo[0].angle + (endAngle - startAngle ) / 2;

}

,并更改了 drawRect: 方法

- (void)drawRect:(CGRect)rect {
    // Don't draw if we don't have a font or string
    if (self.font == NULL || self.text == NULL) 
        return;

    // Initialize the text matrix to a known value
    CGContextRef context = UIGraphicsGetCurrentContext();

    //Reset the transformation
    //Doing this means you have to reset the contentScaleFactor to 1.0
    CGAffineTransform t0 = CGContextGetCTM(context);

    CGFloat xScaleFactor = t0.a > 0 ? t0.a : -t0.a;
    CGFloat yScaleFactor = t0.d > 0 ? t0.d : -t0.d;
    t0 = CGAffineTransformInvert(t0);
    if (xScaleFactor != 1.0 || yScaleFactor != 1.0)
        t0 = CGAffineTransformScale(t0, xScaleFactor, yScaleFactor);

    CGContextConcatCTM(context, t0);

    CGContextSetTextMatrix(context, CGAffineTransformIdentity);

    NSAttributedString *attStr = self.attributedString;
    CFAttributedStringRef asr = (CFAttributedStringRef)attStr;
    CTLineRef line = CTLineCreateWithAttributedString(asr);
    assert(line != NULL);

    CFIndex glyphCount = CTLineGetGlyphCount(line);
    if (glyphCount == 0) {
        CFRelease(line);
        return;
    }

    GlyphArcInfo *  glyphArcInfo = (GlyphArcInfo*)calloc(glyphCount, sizeof(GlyphArcInfo));
    PrepareGlyphArcInfo(self.font, self.radius, line, glyphCount, glyphArcInfo, _arcSize);

    // Move the origin from the lower left of the view nearer to its center.
    CGContextSaveGState(context);

    CGContextTranslateCTM(context, CGRectGetMidX(rect)+_shiftH, CGRectGetMidY(rect)+_shiftV);

    if(ARCVIEW_DEBUG_MODE){
        // Stroke the arc in red for verification.
        CGContextBeginPath(context);
        CGContextAddArc(context, 0.0, 0.0, self.radius, M_PI_2+_arcSize/2.0, M_PI_2-_arcSize/2.0, 1);
        CGContextSetRGBStrokeColor(context, 1.0, 0.0, 0.0, 1.0);
        CGContextStrokePath(context);
    }

    // Rotate the context 90 degrees counterclockwise (per 180 degrees)
    CGContextRotateCTM(context, M_PI_2);

    // Now for the actual drawing. The angle offset for each glyph relative to the previous glyph has already been calculated; with that information in hand, draw those glyphs overstruck and centered over one another, making sure to rotate the context after each glyph so the glyphs are spread along a semicircular path.

    CGPoint textPosition = CGPointMake(0.0, self.radius);
    CGContextSetTextPosition(context, textPosition.x, textPosition.y);

    CFArrayRef runArray = CTLineGetGlyphRuns(line);
    CFIndex runCount = CFArrayGetCount(runArray);

    CFIndex glyphOffset = 0;
    CFIndex runIndex = 0;
    for (; runIndex < runCount; runIndex++) {
        CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runArray, runIndex);
        CFIndex runGlyphCount = CTRunGetGlyphCount(run);
        Boolean drawSubstitutedGlyphsManually = false;
        CTFontRef runFont = CFDictionaryGetValue(CTRunGetAttributes(run), kCTFontAttributeName);

        // Determine if we need to draw substituted glyphs manually. Do so if the runFont is not the same as the overall font.
        if (self.dimsSubstitutedGlyphs && ![self.font isEqual:(UIFont *)runFont]) {
            drawSubstitutedGlyphsManually = true;
        }

        CFIndex runGlyphIndex = 0;
        for (; runGlyphIndex < runGlyphCount; runGlyphIndex++) {
            CFRange glyphRange = CFRangeMake(runGlyphIndex, 1);
            CGContextRotateCTM(context, -(glyphArcInfo[runGlyphIndex + glyphOffset].angle));

            // Center this glyph by moving left by half its width.
            CGFloat glyphWidth = glyphArcInfo[runGlyphIndex + glyphOffset].width;
            CGFloat halfGlyphWidth = glyphWidth / 2.0;
            CGPoint positionForThisGlyph = CGPointMake(textPosition.x - halfGlyphWidth, textPosition.y);

            // Glyphs are positioned relative to the text position for the line, so offset text position leftwards by this glyph's width in preparation for the next glyph.
            textPosition.x -= glyphWidth;

            CGAffineTransform textMatrix = CTRunGetTextMatrix(run);
            textMatrix.tx = positionForThisGlyph.x;
            textMatrix.ty = positionForThisGlyph.y;
            CGContextSetTextMatrix(context, textMatrix);

            CTRunDraw(run, context, glyphRange);
        }

        glyphOffset += runGlyphCount;
    }

    CGContextSetFillColorWithColor(context, [UIColor clearColor].CGColor);
    CGContextSetAlpha(context,0.0);
    CGContextFillRect(context, rect);

    CGContextRestoreGState(context);

    free(glyphArcInfo);
    CFRelease(line);    

}

如您所见,我使用了一种非常不好的方法来计算每个字符之间的间距(在原始示例中,字符之间的间距也基于弧的大小)。无论如何,这似乎工作得很好。

最好的解决方案可能是弯曲一个矩形(即线性文本),需要图形工作和较少奇怪的计算。

这就是我所获得的
sample result

希望有帮助

I tried the git project mentioned above, and as ZpaceZombor said , there is a wrong offset

CGContextTranslateCTM(context, CGRectGetMidX(rect)+_shiftH, CGRectGetMidY(rect)+_shiftV - self.radius / 2.0);

I've changed simply to

CGContextTranslateCTM(context, CGRectGetMidX(rect)+_shiftH, CGRectGetMidY(rect)+_shiftV);

I've set the radius to the Min value between width and height of the container view, so i've set the arc size to .

I've arbitrary changed the line

CGContextRotateCTM(context, _arcSize/2.0);

with

CGContextRotateCTM(context, M_PI_2);

I've changed the init method to

- (id)initWithFrame:(CGRect)frame font:(UIFont *)font text:(NSString *)text color:(UIColor *)color{

    self = [super initWithFrame:frame];
    if (self) {
        self.font = font;
        self.text = text;
        self.radius = -1 * (frame.size.width > frame.size.height ? frame.size.height / 2 : frame.size.width / 2);
        _arcSize = 2* M_PI;
        self.showsGlyphBounds = NO;
        self.showsLineMetrics = NO;
        self.dimsSubstitutedGlyphs = NO;
        self.color = color;
        self.shiftH = self.shiftV = 0.0f;

    }
    return self;
}

After a lot of attempts, i've produce this modification to the function PrepareGlyphArcInfo

// this constants come from a single case ( fontSize = 22 | circle diameter = 250px | lower circle diameter 50px | 0.12f is a proportional acceptable value of 250px diameter | 0.18f is a proportional acceptable value of 50px | 0.035f is a proportional acceptable value of "big" chars
#define kReferredCharSpacing 0.12f
#define kReferredFontSize 22.f
#define kReferredMajorDiameter 250.f
#define kReferredMinorDiameter 50.f
#define kReferredMinorSpacingFix 0.18f
#define kReferredBigCharSpacingFix  0.035f

static void PrepareGlyphArcInfo(UIFont* font,CGFloat containerRadius,CTLineRef line, CFIndex glyphCount, GlyphArcInfo *glyphArcInfo, CGFloat arcSizeRad)
{
    NSArray *runArray = (NSArray *)CTLineGetGlyphRuns(line);

    CGFloat curMaxTypoWidth = 0.f;
    CGFloat curMinTypoWidth = 0.f;

    // Examine each run in the line, updating glyphOffset to track how far along the run is in terms of glyphCount.
    CFIndex glyphOffset = 0;
    for (id run in runArray) {
        CFIndex runGlyphCount = CTRunGetGlyphCount((CTRunRef)run);

            // Ask for the width of each glyph in turn.
        CFIndex runGlyphIndex = 0;
        for (; runGlyphIndex < runGlyphCount; runGlyphIndex++) {
            glyphArcInfo[runGlyphIndex + glyphOffset].width = CTRunGetTypographicBounds((CTRunRef)run, CFRangeMake(runGlyphIndex, 1), NULL, NULL, NULL);

            if (curMaxTypoWidth < glyphArcInfo[runGlyphIndex + glyphOffset].width)
                curMaxTypoWidth = glyphArcInfo[runGlyphIndex + glyphOffset].width;

            if (curMinTypoWidth > glyphArcInfo[runGlyphIndex + glyphOffset].width || curMinTypoWidth == 0)
                curMinTypoWidth = glyphArcInfo[runGlyphIndex + glyphOffset].width;

        }

        glyphOffset += runGlyphCount;
    }

    //double lineLength = CTLineGetTypographicBounds(line, NULL, NULL, NULL);

    glyphArcInfo[0].angle = M_PI_2; // start at the bottom circle

    CFIndex lineGlyphIndex = 1;

    // based on font size. (supposing that with fontSize = 22 we could use 0.12)
    CGFloat maxCharSpacing = font.pointSize * kReferredCharSpacing / kReferredFontSize;

    // for diameter minor than referred 250
    if ((fabsf(containerRadius)*2) < kReferredMajorDiameter)
        maxCharSpacing = maxCharSpacing + kReferredMinorSpacingFix * kReferredMinorDiameter / (fabsf(containerRadius)*2);

    CGFloat startAngle = fabsf(glyphArcInfo[0].angle);
    CGFloat endAngle = startAngle;

    for (; lineGlyphIndex < glyphCount; lineGlyphIndex++) {

        CGFloat deltaWidth = curMaxTypoWidth - glyphArcInfo[lineGlyphIndex].width;

        // fix applied to large characters like uppercase letters or symbols
        CGFloat bigCharFix = (glyphArcInfo[lineGlyphIndex-1].width == curMaxTypoWidth || (glyphArcInfo[lineGlyphIndex-1].width+2) >= curMaxTypoWidth ? kReferredBigCharSpacingFix : 0 );

        glyphArcInfo[lineGlyphIndex].angle = - (maxCharSpacing * (glyphArcInfo[lineGlyphIndex].width + deltaWidth ) / curMaxTypoWidth) - bigCharFix;

        endAngle += fabsf(glyphArcInfo[lineGlyphIndex].angle);
    }

    // center text to bottom
    glyphArcInfo[0].angle = glyphArcInfo[0].angle + (endAngle - startAngle ) / 2;

}

And changed the drawRect: method to

- (void)drawRect:(CGRect)rect {
    // Don't draw if we don't have a font or string
    if (self.font == NULL || self.text == NULL) 
        return;

    // Initialize the text matrix to a known value
    CGContextRef context = UIGraphicsGetCurrentContext();

    //Reset the transformation
    //Doing this means you have to reset the contentScaleFactor to 1.0
    CGAffineTransform t0 = CGContextGetCTM(context);

    CGFloat xScaleFactor = t0.a > 0 ? t0.a : -t0.a;
    CGFloat yScaleFactor = t0.d > 0 ? t0.d : -t0.d;
    t0 = CGAffineTransformInvert(t0);
    if (xScaleFactor != 1.0 || yScaleFactor != 1.0)
        t0 = CGAffineTransformScale(t0, xScaleFactor, yScaleFactor);

    CGContextConcatCTM(context, t0);

    CGContextSetTextMatrix(context, CGAffineTransformIdentity);

    NSAttributedString *attStr = self.attributedString;
    CFAttributedStringRef asr = (CFAttributedStringRef)attStr;
    CTLineRef line = CTLineCreateWithAttributedString(asr);
    assert(line != NULL);

    CFIndex glyphCount = CTLineGetGlyphCount(line);
    if (glyphCount == 0) {
        CFRelease(line);
        return;
    }

    GlyphArcInfo *  glyphArcInfo = (GlyphArcInfo*)calloc(glyphCount, sizeof(GlyphArcInfo));
    PrepareGlyphArcInfo(self.font, self.radius, line, glyphCount, glyphArcInfo, _arcSize);

    // Move the origin from the lower left of the view nearer to its center.
    CGContextSaveGState(context);

    CGContextTranslateCTM(context, CGRectGetMidX(rect)+_shiftH, CGRectGetMidY(rect)+_shiftV);

    if(ARCVIEW_DEBUG_MODE){
        // Stroke the arc in red for verification.
        CGContextBeginPath(context);
        CGContextAddArc(context, 0.0, 0.0, self.radius, M_PI_2+_arcSize/2.0, M_PI_2-_arcSize/2.0, 1);
        CGContextSetRGBStrokeColor(context, 1.0, 0.0, 0.0, 1.0);
        CGContextStrokePath(context);
    }

    // Rotate the context 90 degrees counterclockwise (per 180 degrees)
    CGContextRotateCTM(context, M_PI_2);

    // Now for the actual drawing. The angle offset for each glyph relative to the previous glyph has already been calculated; with that information in hand, draw those glyphs overstruck and centered over one another, making sure to rotate the context after each glyph so the glyphs are spread along a semicircular path.

    CGPoint textPosition = CGPointMake(0.0, self.radius);
    CGContextSetTextPosition(context, textPosition.x, textPosition.y);

    CFArrayRef runArray = CTLineGetGlyphRuns(line);
    CFIndex runCount = CFArrayGetCount(runArray);

    CFIndex glyphOffset = 0;
    CFIndex runIndex = 0;
    for (; runIndex < runCount; runIndex++) {
        CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runArray, runIndex);
        CFIndex runGlyphCount = CTRunGetGlyphCount(run);
        Boolean drawSubstitutedGlyphsManually = false;
        CTFontRef runFont = CFDictionaryGetValue(CTRunGetAttributes(run), kCTFontAttributeName);

        // Determine if we need to draw substituted glyphs manually. Do so if the runFont is not the same as the overall font.
        if (self.dimsSubstitutedGlyphs && ![self.font isEqual:(UIFont *)runFont]) {
            drawSubstitutedGlyphsManually = true;
        }

        CFIndex runGlyphIndex = 0;
        for (; runGlyphIndex < runGlyphCount; runGlyphIndex++) {
            CFRange glyphRange = CFRangeMake(runGlyphIndex, 1);
            CGContextRotateCTM(context, -(glyphArcInfo[runGlyphIndex + glyphOffset].angle));

            // Center this glyph by moving left by half its width.
            CGFloat glyphWidth = glyphArcInfo[runGlyphIndex + glyphOffset].width;
            CGFloat halfGlyphWidth = glyphWidth / 2.0;
            CGPoint positionForThisGlyph = CGPointMake(textPosition.x - halfGlyphWidth, textPosition.y);

            // Glyphs are positioned relative to the text position for the line, so offset text position leftwards by this glyph's width in preparation for the next glyph.
            textPosition.x -= glyphWidth;

            CGAffineTransform textMatrix = CTRunGetTextMatrix(run);
            textMatrix.tx = positionForThisGlyph.x;
            textMatrix.ty = positionForThisGlyph.y;
            CGContextSetTextMatrix(context, textMatrix);

            CTRunDraw(run, context, glyphRange);
        }

        glyphOffset += runGlyphCount;
    }

    CGContextSetFillColorWithColor(context, [UIColor clearColor].CGColor);
    CGContextSetAlpha(context,0.0);
    CGContextFillRect(context, rect);

    CGContextRestoreGState(context);

    free(glyphArcInfo);
    CFRelease(line);    

}

As you can see i've use a really not good method for calculate the space between each caracter ( in the original example the space between characters are based also on the arc size). Anyway this seems to work almost fine.

The best solution could be to curve a rectangle ( so the linear text ), with graphics effort and less strange calculations.

This is what i've obtained
sample result

Hope it helps

情域 2024-10-02 10:08:08

Juggleware 的解决方案效果很好,但我似乎找不到改变方向的方法,即我将如何将弧从顺时针移动到逆时针?

更新:在该示例中的过于复杂的代码中挣扎了几天之后,我决定推出自己的代码。我采用了使用 CATextLayers 的声明性方法,将其放置在圆上并单独旋转。这样,结果就更容易实现。这是适合您的核心代码:

-(void)layoutSublayersOfLayer:(CALayer*)layer
{
    if ( layer != self.layer )
    {
        return;
    }

    self.layer.sublayers = nil;

    LOG( @"Laying out sublayers..." );

    CGFloat xcenter = self.frame.size.width / 2;
    CGFloat ycenter = self.frame.size.height / 2;

    float angle = arcStart;
    float angleStep = arcSize / [self.text length];

    for ( NSUInteger i = 0; i < [self.text length]; ++i )
    {
        NSRange range = { .location = i, .length = 1 };
        NSString* c = [self.text substringWithRange:range];

        CGFloat yoffset = sin( DEGREES_TO_RADIANS(angle) ) * radius;
        CGFloat xoffset = cos( DEGREES_TO_RADIANS(angle) ) * radius;

        CGFloat rotAngle = 90 - angle;

        if ( clockwise )
        {
            yoffset = -yoffset;
            rotAngle = -90 + angle;
        }

        CATextLayer* tl = [[CATextLayer alloc] init];
        if ( debugMode )
        {
            tl.borderWidth = 1;
            tl.cornerRadius = 3;
            tl.borderColor = [UIColor whiteColor].CGColor;
        }
        tl.frame = CGRectMake( shiftH + xcenter - xoffset, shiftV + ycenter + yoffset, 20, 20 );
        tl.font = self.font.fontName;
        tl.fontSize = self.font.pointSize;
        tl.foregroundColor = self.color.CGColor;
        tl.string = c;
        tl.alignmentMode = @"center";

        tl.transform = CATransform3DMakeAffineTransform( CGAffineTransformMakeRotation( DEGREES_TO_RADIANS(rotAngle) ) );

        if ( debugMode )
        {
            CATextLayer* debugLayer = [self debugLayerWithText:[NSString stringWithFormat:@"%u: %.0f°", i, angle]];
            debugLayer.transform = CATransform3DMakeAffineTransform( CGAffineTransformMakeRotation( DEGREES_TO_RADIANS(-rotAngle) ) );
            [tl addSublayer:debugLayer];
        }        
        [self.layer addSublayer:tl];

        angle += angleStep;
    }
}

Juggleware's solution works great, I can't seem to find a way to change the direction though, i.e. how would I go to move the arc from clockwise to counter-clockwise?

Update: After struggling for days with the overcomplicated code in that example, I decided to roll my own. I went for a declarative approach using CATextLayers which are placed on the circle and rotated individually. This way the results were much more simple to achieve. Here's the core code for you:

-(void)layoutSublayersOfLayer:(CALayer*)layer
{
    if ( layer != self.layer )
    {
        return;
    }

    self.layer.sublayers = nil;

    LOG( @"Laying out sublayers..." );

    CGFloat xcenter = self.frame.size.width / 2;
    CGFloat ycenter = self.frame.size.height / 2;

    float angle = arcStart;
    float angleStep = arcSize / [self.text length];

    for ( NSUInteger i = 0; i < [self.text length]; ++i )
    {
        NSRange range = { .location = i, .length = 1 };
        NSString* c = [self.text substringWithRange:range];

        CGFloat yoffset = sin( DEGREES_TO_RADIANS(angle) ) * radius;
        CGFloat xoffset = cos( DEGREES_TO_RADIANS(angle) ) * radius;

        CGFloat rotAngle = 90 - angle;

        if ( clockwise )
        {
            yoffset = -yoffset;
            rotAngle = -90 + angle;
        }

        CATextLayer* tl = [[CATextLayer alloc] init];
        if ( debugMode )
        {
            tl.borderWidth = 1;
            tl.cornerRadius = 3;
            tl.borderColor = [UIColor whiteColor].CGColor;
        }
        tl.frame = CGRectMake( shiftH + xcenter - xoffset, shiftV + ycenter + yoffset, 20, 20 );
        tl.font = self.font.fontName;
        tl.fontSize = self.font.pointSize;
        tl.foregroundColor = self.color.CGColor;
        tl.string = c;
        tl.alignmentMode = @"center";

        tl.transform = CATransform3DMakeAffineTransform( CGAffineTransformMakeRotation( DEGREES_TO_RADIANS(rotAngle) ) );

        if ( debugMode )
        {
            CATextLayer* debugLayer = [self debugLayerWithText:[NSString stringWithFormat:@"%u: %.0f°", i, angle]];
            debugLayer.transform = CATransform3DMakeAffineTransform( CGAffineTransformMakeRotation( DEGREES_TO_RADIANS(-rotAngle) ) );
            [tl addSublayer:debugLayer];
        }        
        [self.layer addSublayer:tl];

        angle += angleStep;
    }
}
睫毛溺水了 2024-10-02 10:08:08

您可以下载使用 CoreTextArcView 的示例项目: https://github.com/javenisme/CurvaView

You can download a sample project that use CoreTextArcView: https://github.com/javenisme/CurvaView

寒尘 2024-10-02 10:08:08

求内圆的周长。这是您想要将角色的底部渲染到的圆圈。我们将这个周长称为totalLength

我假设您有一个要在 textItems 中围绕圆圈渲染的字符串列表。

将每个字符串的宽度放入一个 textWidths 数组中,并将它们均匀分布在 totalLength 上,也许像这个伪(pythonish)代码:

block = max(textWidths)
assert(block * len(textWidths) <= totalLength)
offsets = [(block * i) + ((block-width) / 2) for i, width in enumerate(textWidths)]

尽管毫无疑问可以在在断言触发的情况下,真正重要的是我们知道单个单词在已知区域中的开始和结束位置。要在长度为 totalLength 的直线上渲染,我们只需在 offsets[i] 处开始渲染每个文本块。

为了将其放到圆上,我们将把这条直线映射回圆周上。为此,我们需要将沿该线的每个像素映射到圆上的位置和角度上。此函数将沿该线的偏移量转换为角度(它采用 0 到 totalLength 范围内的值),

def offsetToAngle(pixel):
    ratio = pixel / totalLength
    angle = math.pi * 2 * ratio # cool kids use radians.
    return angle

即您的角度。获得职位:

def angleToPosition(angle, characterWidth):
    xNorm = math.sin(angle + circleRotation)
    yNorm = math.cos(angle + circleRotation)

    halfCWidth = characterWidth / 2
    x = xNorm * radius + yNorm * halfCWidth # +y = tangent
    y = yNorm * radius - xNorm * halfCWidth # -x = tangent again.

    # translate to the circle centre
    x += circleCentre.x
    y += circleCentre.y

    return x,y

这有点棘手。我想这几乎是你问题的症结所在。重要的是,您需要沿着圆的切线向后偏移以计算出开始渲染的点,以便角色的中间碰到圆的半径。 “后”的构成取决于您的坐标系。如果 0,0 位于左下角,则交换切线分量的符号。我假设左上角。

这很重要:我还做出了一个很大的假设,即文本旋转发生在字形的左下角周围。如果没有的话,事情看起来就会有点奇怪。当字体较大时,它会更加明显。总有一种方法可以补偿它旋转的位置,并且通常有一种方法可以告诉系统您希望旋转原点在哪里(这将与代码中的 CGContextTranslateCTM 调用相关)你可以想象)你需要做一个小实验来让角色在围绕左下角旋转的单个点上绘制。

circleRotation 只是一个偏移量,因此您可以旋转整个圆,而不是让事物始终处于相同的方向。这也是弧度。

现在对于每个文本块中的每个字符:

for text, offset in zip(textItems, offsets):
    pix = offset # start each block at the offset we calculated earlier.
    for c in text:
        cWidth = measureGlyph(c)
        # choose the circumference location of the middle of the character
        # this is to match with the tangent calculation of tangentToOffset
        angle = offsetToAngle(pix + cWidth / 2)
        x,y = angleToPosition(angle, cWidth)
        drawGlyph(c, x, y, angle)

        pix += cWidth # start of next character in circumference space

无论如何,这就是概念。

Take the circumference of the inner circle. This is the circle you want the base of the characters to be rendered onto. We'll call this circumference totalLength.

I assume you have a list of strings to render around the circle in textItems.

Take the width of each string into a textWidths array and distribute them evenly across totalLength, perhaps like this pseudo(pythonish) code:

block = max(textWidths)
assert(block * len(textWidths) <= totalLength)
offsets = [(block * i) + ((block-width) / 2) for i, width in enumerate(textWidths)]

Although better layouts can no doubt be done in the cases where the assert would trigger, all that really matters is that we know where individual words start and end in a known area. To render on a straight line of length totalLength we simply start rendering each block of text at offsets[i].

To get it onto the circle, we'll map that straight line back onto the circumference. To do that we need to map each pixel along that line onto a position on the circle and an angle. This function converts the offset along that line into an angle (it takes values in the range 0 to totalLength)

def offsetToAngle(pixel):
    ratio = pixel / totalLength
    angle = math.pi * 2 * ratio # cool kids use radians.
    return angle

that's your angle. To get a position:

def angleToPosition(angle, characterWidth):
    xNorm = math.sin(angle + circleRotation)
    yNorm = math.cos(angle + circleRotation)

    halfCWidth = characterWidth / 2
    x = xNorm * radius + yNorm * halfCWidth # +y = tangent
    y = yNorm * radius - xNorm * halfCWidth # -x = tangent again.

    # translate to the circle centre
    x += circleCentre.x
    y += circleCentre.y

    return x,y

That's a bit more tricky. This is pretty much the crux of your issues, I'd have thought. The big deal is that you need to offset back along the tangent of the circle to work out the point to start rendering so that the middle of the character hits the radius of the circle. What constitues 'back' depends on your coordinate system. if 0,0 is in the bottom left, then the signs of the tangent components is swapped. I assumed top left.

This is important: I'm also making a big assumption that the text rotation occurs around the bottom left of the glyph. If it doesn't then things will look a bit weird. It will be more noticeable at larger font sizes. There is always a way to compensate for wherever it rotates around, and there's usually a way to tell the system where you want the rotation origin to be (that will be related to the CGContextTranslateCTM call in your code I'd imagine) you'll need to do a small experiment to get characters drawing at a single point rotating around their bottom left.

circleRotation is just an offset so you can rotate the whole circle, rather than having things always be in the same orientation. That's in radians too.

so now for each character in each block of text:

for text, offset in zip(textItems, offsets):
    pix = offset # start each block at the offset we calculated earlier.
    for c in text:
        cWidth = measureGlyph(c)
        # choose the circumference location of the middle of the character
        # this is to match with the tangent calculation of tangentToOffset
        angle = offsetToAngle(pix + cWidth / 2)
        x,y = angleToPosition(angle, cWidth)
        drawGlyph(c, x, y, angle)

        pix += cWidth # start of next character in circumference space

That's the concept, anyway.

木森分化 2024-10-02 10:08:08

输入图片此处描述

#import <Cocoa/Cocoa.h>

@interface CircleTextCell : NSCell {

}

@end

#import "CircleTextCell.h"

#define PI (3.141592653589793)

@implementation CircleTextCell

- (void)drawWithFrame: (NSRect)cellFrame inView: (NSView*)controlView
{
    NSAttributedString *str = [self attributedStringValue];
    NSSize stringSize = [str size];
    NSUInteger chars = [[str string] length];
    CGFloat radius = (stringSize.width + 5 * chars) / (2 * PI);
    CGFloat diameter = 2*radius;
    NSPoint scale = {1,1};
    if (diameter > cellFrame.size.width)
    {
        scale.x = cellFrame.size.width / diameter;
    }
    if (diameter > cellFrame.size.height)
    {
        scale.y = cellFrame.size.height / diameter;
    }
    NSAffineTransform *transform = [NSAffineTransform transform];
    NSAffineTransformStruct identity = [transform transformStruct];
    [transform scaleXBy: scale.x yBy: scale.y];
    [transform translateXBy: radius yBy: 0];
    [NSGraphicsContext saveGraphicsState];

    [transform concat];

    NSPoint origin = {0,0};
    CGFloat angleScale = 360 / (stringSize.width + (5 * chars));
    for (NSUInteger i=0 ; i<chars ; i++)
    {
        NSAttributedString *substr = 
            [str attributedSubstringFromRange: NSMakeRange(i, 1)];
        [substr drawAtPoint: origin];
        [transform setTransformStruct: identity];
        CGFloat displacement = [substr size].width + 5;
        [transform translateXBy: displacement yBy: 0];
        [transform rotateByDegrees: angleScale * displacement];
        [transform concat];
    }
    [NSGraphicsContext restoreGraphicsState];
}
@end

#import <Cocoa/Cocoa.h>

@class CircleTextCell;
@interface CircleTextView : NSView {
    CircleTextCell *cell;
}

@end

#import "CircleTextView.h"
#import "CircleTextCell.h"

@implementation CircleTextView
- (void)awakeFromNib
{
    NSDictionary *attributes = 
        [NSDictionary dictionaryWithObject: [NSFont fontWithName: @"Zapfino"
                                                            size:32]
                                    forKey: NSFontAttributeName];
    NSAttributedString *str =
        [[NSAttributedString alloc] initWithString: @"Hello World!  This is a very long text string that will be wrapped into a circle by a cell drawn in a custom view"
                                        attributes: attributes];
    cell = [[CircleTextCell alloc] init];
    [cell setAttributedStringValue: str];
}
- (void)drawRect:(NSRect)rect 
{
    [[NSColor whiteColor] setFill];
    [NSBezierPath fillRect: rect];
    [cell drawWithFrame: [self bounds] inView: self];
}

@end

enter image description here

#import <Cocoa/Cocoa.h>

@interface CircleTextCell : NSCell {

}

@end

#import "CircleTextCell.h"

#define PI (3.141592653589793)

@implementation CircleTextCell

- (void)drawWithFrame: (NSRect)cellFrame inView: (NSView*)controlView
{
    NSAttributedString *str = [self attributedStringValue];
    NSSize stringSize = [str size];
    NSUInteger chars = [[str string] length];
    CGFloat radius = (stringSize.width + 5 * chars) / (2 * PI);
    CGFloat diameter = 2*radius;
    NSPoint scale = {1,1};
    if (diameter > cellFrame.size.width)
    {
        scale.x = cellFrame.size.width / diameter;
    }
    if (diameter > cellFrame.size.height)
    {
        scale.y = cellFrame.size.height / diameter;
    }
    NSAffineTransform *transform = [NSAffineTransform transform];
    NSAffineTransformStruct identity = [transform transformStruct];
    [transform scaleXBy: scale.x yBy: scale.y];
    [transform translateXBy: radius yBy: 0];
    [NSGraphicsContext saveGraphicsState];

    [transform concat];

    NSPoint origin = {0,0};
    CGFloat angleScale = 360 / (stringSize.width + (5 * chars));
    for (NSUInteger i=0 ; i<chars ; i++)
    {
        NSAttributedString *substr = 
            [str attributedSubstringFromRange: NSMakeRange(i, 1)];
        [substr drawAtPoint: origin];
        [transform setTransformStruct: identity];
        CGFloat displacement = [substr size].width + 5;
        [transform translateXBy: displacement yBy: 0];
        [transform rotateByDegrees: angleScale * displacement];
        [transform concat];
    }
    [NSGraphicsContext restoreGraphicsState];
}
@end

#import <Cocoa/Cocoa.h>

@class CircleTextCell;
@interface CircleTextView : NSView {
    CircleTextCell *cell;
}

@end

#import "CircleTextView.h"
#import "CircleTextCell.h"

@implementation CircleTextView
- (void)awakeFromNib
{
    NSDictionary *attributes = 
        [NSDictionary dictionaryWithObject: [NSFont fontWithName: @"Zapfino"
                                                            size:32]
                                    forKey: NSFontAttributeName];
    NSAttributedString *str =
        [[NSAttributedString alloc] initWithString: @"Hello World!  This is a very long text string that will be wrapped into a circle by a cell drawn in a custom view"
                                        attributes: attributes];
    cell = [[CircleTextCell alloc] init];
    [cell setAttributedStringValue: str];
}
- (void)drawRect:(NSRect)rect 
{
    [[NSColor whiteColor] setFill];
    [NSBezierPath fillRect: rect];
    [cell drawWithFrame: [self bounds] inView: self];
}

@end
悲歌长辞 2024-10-02 10:08:08

输入图片这里的描述这是最好的网址https://github.com/javenisme/CurvaView 设置文本的曲线:

但是根据度数曲线,我只需更新一点代码,我们就可以将曲线设置为度数。比如 45,60,90 180, 360。

看看代码: https://github.com/tikamsingh/CurveTextWithAngle

你可以采取一些想法。

enter image description herethat is the best url https://github.com/javenisme/CurvaView to set curve your text:

But as per the the degree wise curve i just update a code little bit and we can set the curve as a degree wise . like 45,60,90 180, 360.

Look at the code : https://github.com/tikamsingh/CurveTextWithAngle

You can take some idea.

℉絮湮 2024-10-02 10:08:08

参考Ali Seyman的回答

您可以下载使用 CoreTextArcView 的示例项目:https://github.com/javenisme/CurvaView


添加使用此方法来减小视图框架大小,就像 UILabel 一样。

- (void)sizeToFit{
[super sizeToFit];

CGFloat width = ceilf( fabsf((self.radius*2)) + self.font.lineHeight) + 3.0;
CGRect f = self.frame;
f.size = CGSizeMake(width,width);
self.frame = f;
[self setNeedsDisplay];
}

如果有人也能在降低高度方面有所改进,欢迎补充。

Referring to Ali Seyman's answer:

You can download a sample project that use CoreTextArcView: https://github.com/javenisme/CurvaView

Add on this method to reduce the view frame size, just like UILabel.

- (void)sizeToFit{
[super sizeToFit];

CGFloat width = ceilf( fabsf((self.radius*2)) + self.font.lineHeight) + 3.0;
CGRect f = self.frame;
f.size = CGSizeMake(width,width);
self.frame = f;
[self setNeedsDisplay];
}

If anyone could improve on reducing the height as well, welcome to add on.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文