我正在使用 NSMutableAttribtuedString 为了用格式构建字符串,然后我将其传递给核心文本以渲染到帧中。问题是,我需要使用上标和下标。除非这些字符在字体中可用(大多数字体不支持它),然后设置属性 kCTSuperscriptAttributeName 根本没有做任何事情。

因此,我想我剩下的唯一选择是通过更改字体大小并移动基线来伪造它。我可以做字体大小位,但不知道更改基线的代码。有人可以帮忙吗?

谢谢!

编辑:我在想,考虑到我有时间对此问题进行编辑字体的时间,以便给出一个下标“ 2” ...或找到可以使用的内置iPad字体。有谁知道我可以使用的下标“ 2”的衬线字体?

有帮助吗?

解决方案

CtParagraphStylesPecifier或定义的字符串属性名称常数之间没有基线设置。因此,我认为可以肯定的是,Coretext本身并不支持文本上的基线调整属性。在cttypesetter中提到了基线位置,但我无法将其与iPad Coretext的一条线路的任何能力联系在一起。

因此,您可能需要自己干预渲染过程。例如:

  • 创建一个ctframesetter,例如通过 CTFramesetterCreateWithAttributedString
  • 从中获得一个ctframe CTFramesetterCreateFrame
  • 采用 CTFrameGetLineOriginsCTFrameGetLines 要获取一系列ctlines和应绘制它们的位置(即,具有合适段落/线路断裂的文本以及您所有其他kerning/prade/其他定位文本属性)
  • 从中,对于没有上标或下标的行,只需使用 CTLineDraw 忘了
  • 对于那些具有上标或下标的人,请使用 CTLineGetGlyphRuns 要获取描述行上各种字形的CTRUN对象数组
  • 在每次运行中,使用 CTRunGetStringIndices 确定在运行中哪些源字符;如果不包括您要上标或下标,则只需使用 CTRunDraw 画东西
  • 否则,请使用 CTRunGetGlyphs 闯入单个字形, CTRunGetPositions 弄清楚在正常运行中将它们吸引到哪里
  • 采用 CGContextShowGlyphsAtPoint 适当地,对您想要的上标或下标进行了调整后的文本矩阵

我尚未找到一种方法来查询字体是否具有自动上标/下标的相关提示,这使事情有些棘手。如果您绝望并且没有解决方案,那么根本不使用Coretext的东西可能会更容易 - 在这种情况下,您可能应该定义自己的属性(这就是为什么[NS/CF]属性属性允许任意属性来应用,通过字符串名称识别),并使用普通的NSString搜索方法来识别需要在盲人中打印的区域或盲人的下标。

出于绩效原因,二进制搜索可能是搜索所有行,一行中的运行以及跑步中的字形为您感兴趣的人的方法更聪明地提前做到这一点,而不是在每次抽签上进行:(或等效方法,例如您使用的是catiledlayer)。

此外,CTRUN方法具有变体,可以要求指针指向C数组,其中包含您要求的内容的内容,可能会节省您的副本操作,但不一定成功。检查文档。我只是确保我要绘制一个可行的解决方案,而不是一定要绘制通过CoreText API的绝对最佳路线。

其他提示

这是基于汤米(Tommy)大纲的一些代码,可以很好地完成工作(虽然仅在单行进行了测试)。用 @"MDBaselineAdjust", ,此代码将行绘制为 offset, , 一种 CGPoint. 。要获得上标,也要降低字体尺寸A凹槽。预览可能: http://cloud.mochidev.com/ifpf (读为“ [xe] 4f的行14...")

希望这可以帮助 :)

NSAttributedString *string = ...;
CGPoint origin = ...;

CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)string);
CGSize suggestedSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0, string.length), NULL, CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX), NULL);
CGPathRef path = CGPathCreateWithRect(CGRectMake(origin.x, origin.y, suggestedSize.width, suggestedSize.height), NULL);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, string.length), path, NULL);
NSArray *lines = (NSArray *)CTFrameGetLines(frame);
if (lines.count) {
    CGPoint *lineOrigins = malloc(lines.count * sizeof(CGPoint));
    CTFrameGetLineOrigins(frame, CFRangeMake(0, lines.count), lineOrigins);

    int i = 0;
    for (id aLine in lines) {
        NSArray *glyphRuns = (NSArray *)CTLineGetGlyphRuns((CTLineRef)aLine);

        CGFloat width = origin.x+lineOrigins[i].x-lineOrigins[0].x;

        for (id run in glyphRuns) {
            CFRange range = CTRunGetStringRange((CTRunRef)run);
            NSDictionary *dict = [string attributesAtIndex:range.location effectiveRange:NULL];
            CGFloat baselineAdjust = [[dict objectForKey:@"MDBaselineAdjust"] doubleValue];

            CGContextSetTextPosition(context, width, origin.y+baselineAdjust);

            CTRunDraw((CTRunRef)run, context, CFRangeMake(0, 0));
        }

        i++;
    }

    free(lineOrigins);
}
CFRelease(frame);
CGPathRelease(path);
CFRelease(framesetter);

`

您现在可以使用ios7中的TextKit模仿下标。例子:

NSMutableAttributedString *carbonDioxide = [[NSMutableAttributedString alloc] initWithString:@"CO2"];
[carbonDioxide addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:8] range:NSMakeRange(2, 1)];
[carbonDioxide addAttribute:NSBaselineOffsetAttributeName value:@(-2) range:NSMakeRange(2, 1)];

Image of attributed string output

我本人遇到了麻烦。苹果的核心文本文档声称,自3.2版以来,iOS一直在支持,但由于某种原因,它仍然无法正常工作。即使在iOS 5中……多么令人沮丧>。

如果您仅真正关心上标或下标号码,我设法找到了解决方法。假设您有一个文本块可能包含一个“ sub2”标签,其中想要下标。2。使用nsregularexpression查找标签,然后在您的Regex对象上使用reportacementStringForresult方法将每个标签替换为Unicode字符:

if ([match isEqualToString:@"<sub2/>"])
{
   replacement = @"₂";
}

如果使用OSX字符查看器,则可以将Unicode字符直接放入代码中。其中有一组名为“数字”的字符,其中具有所有上标和下标字符。只需将光标留在代码窗口中的适当位置,然后双击角色查看器即可插入所需的字符。

使用正确的字体,您也可以使用任何字母来执行此操作,但是角色映射只有少数我看到的非数字可用。

另外,您可以将Unicode字符放入源内容中,但是在许多情况下(例如我的情况),这是不可能的。

Swift 4

非常宽松地基于格雷厄姆·珀克斯(Graham Perks)的回答。我无法像以前一样使他的代码工作,但是经过三个小时的工作,我创造了一些效果很好的东西!如果您希望对此进行完整的实现以及一堆其他的其他性能和功能附加组件(链接,异步图等),请查看我的单个文件库 Dylabel. 。如果没有,请继续阅读。

我解释了我在评论中所做的一切。这是绘制方法,可以从drawRect中调用:

/// Draw text on a given context. Supports superscript using NSBaselineOffsetAttributeName
///
/// This method works by drawing the text backwards (i.e. last line first). This is very very important because it's how we ensure superscripts don't overlap the text above it. In other words, we need to start from the bottom, get the height of the text we just drew, and then draw the next text above it. This could be done in a forward direction but you'd have to use lookahead which IMO is more work.
///
/// If you have to modify on this, remember that CT uses a mathmatical origin (i.e. 0,0 is bottom left like a cartisian plane)
/// - Parameters:
///   - context: A core graphics draw context
///   - attributedText: An attributed string
func drawText(context:CGContext, attributedText: NSAttributedString) {
    //Create our CT boiler plate
    let framesetter = CTFramesetterCreateWithAttributedString(attributedText)
    let textRect = bounds
    let path = CGPath(rect: textRect, transform: nil)
    let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, nil)

    //Fetch our lines, bridging to swift from CFArray
    let lines = CTFrameGetLines(frame) as [AnyObject]
    let lineCount = lines.count

    //Get the line origin coordinates. These are used for calculating stock line height (w/o baseline modifications)
    var lineOrigins = [CGPoint](repeating: CGPoint.zero, count: lineCount)
    CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), &lineOrigins);

    //Since we're starting from the bottom of the container we need get our bottom offset/padding (so text isn't slammed to the bottom or cut off)
    var ascent:CGFloat = 0
    var descent:CGFloat = 0
    var leading:CGFloat = 0
    if lineCount > 0 {
        CTLineGetTypographicBounds(lines.last as! CTLine, &ascent, &descent, &leading)
    }

    //This variable holds the current draw position, relative to CT origin of the bottom left
    //https://stackoverflow.com/a/27631737/1166266
    var drawYPositionFromOrigin:CGFloat = descent

    //Again, draw the lines in reverse so we don't need look ahead
    for lineIndex in (0..<lineCount).reversed()  {
        //Calculate the current line height so we can accurately move the position up later
        let lastLinePosition = lineIndex > 0 ? lineOrigins[lineIndex - 1].y: textRect.height
        let currentLineHeight = lastLinePosition - lineOrigins[lineIndex].y
        //Throughout the loop below this variable will be updated to the tallest value for the current line
        var maxLineHeight:CGFloat = currentLineHeight

        //Grab the current run glyph. This is used for attributed string interop
        let glyphRuns = CTLineGetGlyphRuns(lines[lineIndex] as! CTLine) as [AnyObject]

        for run in glyphRuns {
            let run = run as! CTRun
            //Convert the format range to something we can match to our string
            let runRange = CTRunGetStringRange(run)

            let attribuetsAtPosition = attributedText.attributes(at: runRange.location, effectiveRange: nil)
            var baselineAdjustment: CGFloat = 0.0
            if let adjust = attribuetsAtPosition[NSAttributedStringKey.baselineOffset] as? NSNumber {
                //We have a baseline offset!
                baselineAdjustment = CGFloat(adjust.floatValue)
            }

            //Check if this glyph run is tallest, and move it if it is
            maxLineHeight = max(currentLineHeight + baselineAdjustment, maxLineHeight)

            //Move the draw head. Note that we're drawing from the unupdated drawYPositionFromOrigin. This is again thanks to CT cartisian plane where we draw from the bottom left of text too.
            context.textPosition = CGPoint.init(x: lineOrigins[lineIndex].x, y: drawYPositionFromOrigin)
            //Draw!
            CTRunDraw(run, context, CFRangeMake(0, 0))

        }
        //Move our position because we've completed the drawing of the line which is at most `maxLineHeight`
        drawYPositionFromOrigin += maxLineHeight
    }
}

我还制作了一种计算给定宽度的文本所需高度的方法。它是完全相同的代码,除非没有绘制任何内容。

/// Calculate the height if it were drawn using `drawText`
/// Uses the same code as drawText except it doesn't draw.
///
/// - Parameters:
///   - attributedText: The text to calculate the height of
///   - width: The constraining width
///   - estimationHeight: Optional paramater, default 30,000px. This is the container height used to layout the text. DO NOT USE CGFLOATMAX AS IT CORE TEXT CANNOT CREATE A FRAME OF THAT SIZE.
/// - Returns: The size required to fit the text
static func size(of attributedText:NSAttributedString,width:CGFloat, estimationHeight:CGFloat?=30000) -> CGSize {
    let framesetter = CTFramesetterCreateWithAttributedString(attributedText)
    let textRect = CGRect.init(x: 0, y: 0, width: width, height: estimationHeight!)
    let path = CGPath(rect: textRect, transform: nil)
    let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, nil)

    //Fetch our lines, bridging to swift from CFArray
    let lines = CTFrameGetLines(frame) as [AnyObject]
    let lineCount = lines.count

    //Get the line origin coordinates. These are used for calculating stock line height (w/o baseline modifications)
    var lineOrigins = [CGPoint](repeating: CGPoint.zero, count: lineCount)
    CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), &lineOrigins);

    //Since we're starting from the bottom of the container we need get our bottom offset/padding (so text isn't slammed to the bottom or cut off)
    var ascent:CGFloat = 0
    var descent:CGFloat = 0
    var leading:CGFloat = 0
    if lineCount > 0 {
        CTLineGetTypographicBounds(lines.last as! CTLine, &ascent, &descent, &leading)
    }

    //This variable holds the current draw position, relative to CT origin of the bottom left
    var drawYPositionFromOrigin:CGFloat = descent

    //Again, draw the lines in reverse so we don't need look ahead
    for lineIndex in (0..<lineCount).reversed()  {
        //Calculate the current line height so we can accurately move the position up later
        let lastLinePosition = lineIndex > 0 ? lineOrigins[lineIndex - 1].y: textRect.height
        let currentLineHeight = lastLinePosition - lineOrigins[lineIndex].y
        //Throughout the loop below this variable will be updated to the tallest value for the current line
        var maxLineHeight:CGFloat = currentLineHeight

        //Grab the current run glyph. This is used for attributed string interop
        let glyphRuns = CTLineGetGlyphRuns(lines[lineIndex] as! CTLine) as [AnyObject]

        for run in glyphRuns {
            let run = run as! CTRun
            //Convert the format range to something we can match to our string
            let runRange = CTRunGetStringRange(run)

            let attribuetsAtPosition = attributedText.attributes(at: runRange.location, effectiveRange: nil)
            var baselineAdjustment: CGFloat = 0.0
            if let adjust = attribuetsAtPosition[NSAttributedStringKey.baselineOffset] as? NSNumber {
                //We have a baseline offset!
                baselineAdjustment = CGFloat(adjust.floatValue)
            }

            //Check if this glyph run is tallest, and move it if it is
            maxLineHeight = max(currentLineHeight + baselineAdjustment, maxLineHeight)

            //Skip drawing since this is a height calculation
        }
        //Move our position because we've completed the drawing of the line which is at most `maxLineHeight`
        drawYPositionFromOrigin += maxLineHeight
    }
    return CGSize.init(width: width, height: drawYPositionFromOrigin)
}

就像我写的所有内容一样,我还针对某些公共图书馆和系统功能进行了一些基准(即使它们在这里不起作用)。我在这里使用了一个巨大的复杂弦,以防止任何人采取不公平的快捷方式。

---HEIGHT CALCULATION---
Runtime for 1000 iterations (ms) BoundsForRect: 5415.030002593994
Runtime for 1000 iterations (ms) layoutManager: 5370.990991592407
Runtime for 1000 iterations (ms) CTFramesetterSuggestFrameSizeWithConstraints: 2372.151017189026
Runtime for 1000 iterations (ms) CTFramesetterCreateFrame ObjC: 2300.302028656006
Runtime for 1000 iterations (ms) CTFramesetterCreateFrame-Swift: 2313.6669397354126
Runtime for 1000 iterations (ms) THIS ANSWER size(of:): 2566.351056098938


---RENDER---
Runtime for 1000 iterations (ms) AttributedLabel: 35.032033920288086
Runtime for 1000 iterations (ms) UILabel: 45.948028564453125
Runtime for 1000 iterations (ms) TTTAttributedLabel: 301.1329174041748
Runtime for 1000 iterations (ms) THIS ANSWER: 20.398974418640137

因此,摘要时间:我们做得很好! size(of...) 几乎等于库存的CT布局,这意味着尽管使用了哈希桌查找,但我们的上标的插件很便宜。但是,我们确实在抽奖电话上赢得了胜利。我怀疑这是由于我们必须创建的非常昂贵的30K像素估计框架。如果我们做出更好的估计表现会更好。我已经工作了大约三个小时,所以我称之为退出,将其作为读者的练习。

我也为这个问题而苦苦挣扎。事实证明,正如上面的一些海报所建议的那样,iOS支持的supperssripting或订阅的字体都没有。我的解决方案是购买和安装两个自定义的上标和下标字体(它们为$ 9.99,这是该网站的链接 http://superscriptfont.com/).

真的很难做。只需将字体文件添加为资源,然后添加info.plist条目“应用程序提供的字体”。

下一步是在我的nsattribedstring中搜索适当的标签,删除标签并将字体应用于文本。

效果很好!

Dimitri的答案迅速2扭曲;有效地实现NSBASELINEOFFSETATTRIBUTENAME。

编码时,我处于uiview,因此使用了合理的范围rect。他的答案计算出了自己的矩。

func drawText(context context:CGContextRef, attributedText: NSAttributedString) {

    // All this CoreText iteration just to add support for superscripting.
    // NSBaselineOffsetAttributeName isn't supported by CoreText. So we manully iterate through 
    // all the text ranges, rendering each, and offsetting the baseline where needed.

    let framesetter = CTFramesetterCreateWithAttributedString(attributedText)
    let textRect = CGRectOffset(bounds, 0, 0)
    let path = CGPathCreateWithRect(textRect, nil)
    let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, nil)

    // All the lines of text we'll render...
    let lines = CTFrameGetLines(frame) as [AnyObject]
    let lineCount = lines.count

    // And their origin coordinates...
    var lineOrigins = [CGPoint](count: lineCount, repeatedValue: CGPointZero)
    CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), &lineOrigins);

    for lineIndex in 0..<lineCount  {
        let lineObject = lines[lineIndex]

        // Each run of glyphs we'll render...
        let glyphRuns = CTLineGetGlyphRuns(lineObject as! CTLine) as [AnyObject]
        for r in glyphRuns {
            let run = r as! CTRun
            let runRange = CTRunGetStringRange(run)

            // What attributes are in the NSAttributedString here? If we find NSBaselineOffsetAttributeName, 
            // adjust the baseline.
            let attrs = attributedText.attributesAtIndex(runRange.location, effectiveRange: nil)
            var baselineAdjustment: CGFloat = 0.0
            if let adjust = attrs[NSBaselineOffsetAttributeName as String] as? NSNumber {
                baselineAdjustment = CGFloat(adjust.floatValue)
            }

            CGContextSetTextPosition(context, lineOrigins[lineIndex].x, lineOrigins[lineIndex].y - 25 + baselineAdjustment)

            CTRunDraw(run, context, CFRangeMake(0, 0))
        }
    }
}
许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top