我正在使用NSMutableAttribtuedString
来构建带有格式的字符串,然后我将其传递给Core Text以呈现为框架。问题是,我需要使用上标和下标。除非字体中有这些字符(大多数字体不支持),否则设置属性kCTSuperscriptAttributeName
什么都不做。
所以我想我只留下了唯一的选择,就是通过改变字体大小和移动基线来伪造它。我可以做字体大小位,但不知道改变基线的代码。有人可以帮忙吗?
谢谢!
编辑:考虑到我可以用来解决这个问题的时间,我正在考虑编辑一个字体,以便给它一个下标“2”......或者找到一个内置的iPad字体确实。有没有人知道任何带有下标“2”的衬线字体我可以使用吗?答案 0 :(得分:14)
CTParagraphStyleSpecifiers或定义的字符串属性名称常量中没有基线设置。因此,我认为结论CoreText本身不支持文本的基线调整属性是安全的。在CTTypesetter中有一个对基线位置的引用,但我无法将其与任何在iPad的CoreText中的一行中改变基线的能力联系起来。
因此,您可能需要自己干预渲染过程。例如:
CTFramesetterCreateWithAttributedString
CTFramesetterCreateFrame
CTFrameGetLineOrigins
和CTFrameGetLines
获取CTLines数组以及应该绘制的位置(即具有合适段落/换行符的文本以及所有其他字距调整/前导/其他定位文本属性施加的)CTLineDraw
并忘记它CTLineGetGlyphRuns
获取描述行上各种字形的CTRun对象数组CTRunGetStringIndices
确定运行中的源字符;如果没有你想要的上标或下标,只需使用CTRunDraw
来绘制东西CTRunGetGlyphs
打破单个字形和CTRunGetPositions
以确定在正常情况下绘制它们的位置CGContextShowGlyphsAtPoint
,在上标或下标中调整您想要的文本矩阵我还没有找到一种方法来查询字体是否具有自动上标/下标生成的相关提示,这使得事情有点棘手。如果你是绝望的并且没有解决方案,那么根本不使用CoreText的东西可能更容易 - 在这种情况下你应该定义你自己的属性(这就是为什么[NS / CF] AttributedString允许任意属性应用,由字符串名称标识)并使用正常的NSString搜索方法来识别需要在盲人的上标或下标中打印的区域。
出于性能原因,二进制搜索可能是继续搜索所有行,行内的运行以及运行中的字形的方式。假设您有一个自定义的UIView子类来绘制CoreText内容,提前做到这一点可能更聪明,而不是每个drawRect :(或等效的方法,如果你使用的是CATiledLayer)。
此外,CTRun方法的变体请求指向C数组的指针,该数组包含您要求复制的内容,可能会保存复制操作但不一定成功。查看文档。我刚刚确定我正在草拟一个可行的解决方案,而不是通过CoreText API绘制绝对最佳路线。
答案 1 :(得分:5)
以下是一些基于Tommy大纲的代码,它可以很好地完成工作(尽管只在单行测试)。使用@"MDBaselineAdjust"
在您的属性字符串上设置基线,此代码将该行绘制为offset
,CGPoint
。要获得上标,也要将字体大小降低一个档次。预览可能的内容: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);
`
答案 2 :(得分:3)
您现在可以使用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)];
答案 3 :(得分:2)
我自己一直遇到麻烦。 Apple的Core Text文档声称自3.2版以来iOS一直受到支持,但由于某种原因它仍然无效。即使在iOS 5中......非常令人沮丧的>。<
如果您真的关心上标或下标数字,我设法找到了解决方法。假设您有一个文本块可能包含一个“sub2”标记,您需要下标编号2.使用NSRegularExpression查找标记,然后在regex对象上使用replacementStringForResult方法将每个标记替换为unicode字符:
if ([match isEqualToString:@"<sub2/>"])
{
replacement = @"₂";
}
如果使用OSX字符查看器,则可以将unicode字符直接删除到代码中。那里有一组叫做“数字”的字符,里面有所有的上标和下标数字。只需将光标留在代码窗口中的适当位置,然后双击字符查看器即可插入所需的字符。
使用正确的字体,您也可以使用任何字母执行此操作,但字符映射只有少量可用的非数字,我见过。
或者您可以将unicode字符放在源内容中,但在很多情况下(如我的),这是不可能的。
答案 4 :(得分:2)
基于格雷厄姆·珀克斯(Graham Perks)的回答,非常宽松。我无法按原样运行他的代码,但是经过三个小时的工作,我创建了一些很棒的东西!如果您希望完全实现此功能以及其他许多不错的性能和功能附件(链接,异步绘图等),请查看我的单个文件库DYLabel。如果没有,请继续阅读。
我在评论中解释了我所做的一切。这是draw方法,可从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布局,这意味着尽管使用哈希表查找,但我们用于上标的附加组件相当便宜。但是,我们确实在平局上取得了胜利。我怀疑这是由于我们必须创建非常昂贵的3万像素估算框架。如果我们做出更好的估计,性能将会更好。我已经工作了大约三个小时,所以我叫它退出,把它留给读者练习。
答案 5 :(得分:1)
我也在努力解决这个问题。事实证明,正如上面提到的一些海报所示,IOS附带的任何字体都不支持上标或下标。我的解决方案是购买并安装两个自定义的上标和下标字体(每个都是9.99美元,这里是指向网站http://superscriptfont.com/的链接)。
真的很难做到。只需将字体文件添加为资源,并为“应用程序提供的字体”添加info.plist条目。
下一步是在我的NSAttributedString中搜索相应的标签,删除标签并将字体应用于文本。
效果很好!
答案 6 :(得分:0)
斯威夫特2对迪米特里的回答扭曲;有效地实现了NSBaselineOffsetAttributeName。
编码时,我在UIView中,因此有一个合理的界限使用。他的答案计算了自己的矩形。
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))
}
}
}