11

を使用しNSMutableAttribtuedStringて書式設定付きの文字列を作成し、それを Core Text に渡してフレームにレンダリングします。問題は、上付き文字と下付き文字を使用する必要があることです。これらの文字がフォントで使用できない場合 (ほとんどのフォントではサポートされていません)、プロパティを設定kCTSuperscriptAttributeNameしても何も起こりません。

したがって、フォントサイズを変更してベースラインを移動することで偽造するという唯一のオプションが残っていると思います。フォント サイズを少し変更できますが、ベース ラインを変更するコードがわかりません。誰でも助けてもらえますか?

ありがとう!

編集: この問題を分類するために利用できる時間を考慮して、下付き文字「2」が与えられるようにフォントを編集することを考えています...それか、組み込みの iPad フォントを見つけることです。下付き文字「2」を使用できるセリフフォントを知っている人はいますか?

4

8 に答える 8

14

CTParagraphStyleSpecifiers または定義された文字列属性名の定数には、ベースライン設定はありません。したがって、CoreText 自体はテキストのベースライン調整プロパティをサポートしていないと結論付けても問題ないと思います。CTTypesetter でのベースラインの配置についての言及がありますが、iPad の CoreText の行の途中でベースラインを変更する機能と結び付けることはできません。

したがって、レンダリング プロセスに自分で干渉する必要がある可能性があります。例えば:

  • CTFramesetter を作成します。CTFramesetterCreateWithAttributedString
  • そこからCTFrameを取得しますCTFramesetterCreateFrame
  • と を使用CTFrameGetLineOriginsCTFrameGetLinesて CTLines の配列とそれらが描画される場所を取得します (つまり、適切な段落/改行と他のすべてのカーニング/リーディング/その他の配置テキスト属性が適用されたテキスト)
  • それらのうち、上付き文字または下付き文字のない行については、それを使用CTLineDrawして忘れてください
  • 上付き文字または下付き文字の場合CTLineGetGlyphRuns、行上のさまざまなグリフを記述する CTRun オブジェクトの配列を取得するために使用します
  • 各実行で、CTRunGetStringIndicesどのソース文字が実行に含まれているかを判断するために使用します。上付きまたは下付きにしたいものが含まれていない場合は、単にそれCTRunDrawを描画するために使用します
  • それ以外の場合はCTRunGetGlyphs、実行を個々のグリフに分割CTRunGetPositionsし、通常の実行で描画される場所を把握するために使用します
  • CGContextShowGlyphsAtPoint上付き文字または下付き文字で必要なテキスト マトリックスを微調整して、必要に応じて使用します。

フォントが自動上付き文字/下付き文字生成に関連するヒントを持っているかどうかを照会する方法をまだ見つけていないため、少し面倒です。絶望的で解決策がない場合は、CoreText のものをまったく使用しない方がおそらく簡単です — その場合、おそらく独自の属性を定義する必要があります ([NS/CF]AttributedString が任意の属性を適用され、文字列名で識別されます)、通常の NSString 検索方法を使用して、ブラインドから上付き文字または下付き文字で印刷する必要がある領域を識別します。

パフォーマンス上の理由から、バイナリ検索はおそらく、すべての行、行内の実行、および関心のある行内のグリフを検索する方法です。CoreText コンテンツを描画するためのカスタム UIView サブクラスがあると仮定すると、おそらくすべての drawRect: (または、CATiledLayer を使用している場合は同等のメソッド) ごとに行うよりも、事前に行う方が賢明です。

また、CTRun メソッドには、コピーを要求するものを含む C 配列へのポインターを要求するバリアントがあり、コピー操作を節約できる可能性がありますが、必ずしも成功するとは限りません。ドキュメントを確認してください。CoreText API を介して絶対に最適なルートを必ずしもプロットするのではなく、実行可能なソリューションをスケッチしていることを確認しました。

于 2010-11-22T01:26:58.053 に答える
5

これは、Tommy のアウトラインに基づいた、非常にうまく機能するコードです (ただし、1 行だけでテストされています)。属性付き文字列のベースラインを で設定すると、このコードは に線を@"MDBaselineAdjust"引きます。上付き文字を取得するには、フォント サイズも 1 段階小さくします。可能性のプレビュー: http://cloud.mochidev.com/IfPF (「[Xe] 4f 14 ...」と書かれた行)offsetCGPoint

お役に立てれば :)

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);

`

于 2012-08-11T19:49:31.257 に答える
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)];

属性付き文字列出力のイメージ

于 2014-02-19T23:43:56.693 に答える
2

スイフト4

Graham Perks の回答に大まかに基づいています。私は彼のコードをそのまま動作させることはできませんでしたが、3 時間の作業の後、うまく動作するものを作成しました! これを完全に実装し、その他の気の利いたパフォーマンスや機能のアドオン (リンク、非同期描画など) を多数使用したい場合は、私の単一ファイル ライブラリDYLabelをチェックしてください。そうでない場合は、読み進めてください。

私がやっていることはすべてコメントで説明しています。これは、drawRect から呼び出される draw メソッドです。

/// 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 ピクセルの推定フレームが原因であると思われます。より良い見積もりを行うと、パフォーマンスが向上します。私はすでに約 3 時間作業しているので、終了と呼び、それは読者の演習として残します。

于 2018-08-25T23:46:24.263 に答える
2

私自身、これに悩まされてきました。Apple の Core Text ドキュメントによると、iOS はバージョン 3.2 からサポートされているとのことですが、何らかの理由でまだ機能していません。iOS 5 でも... なんとももどかしいです >.<

上付きまたは下付きの数字のみを本当に気にする場合は、回避策を見つけることができました。テキストのブロックに、添字番号 2 が必要な「sub2」タグを含めることができるとします。NSRegularExpression を使用してタグを見つけ、正規表現オブジェクトで replacementStringForResult メソッドを使用して、各タグを Unicode 文字に置き換えます。

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

OSX 文字ビューアを使用すると、Unicode 文字をコードに直接ドロップできます。そこには、すべての上付き文字と下付き文字の数字を含む「数字」と呼ばれる一連の文字があります。コード ウィンドウの適切な場所にカーソルを置いたまま、文字ビューアーをダブルクリックして、必要な文字を挿入します。

適切なフォントがあれば、おそらくどの文字でもこれを行うことができますが、文字マップには、私が見た数字以外のほんの一握りしかありません。

別の方法として、Unicode 文字をソース コンテンツに入れることもできますが、多くの場合 (私のように)、それは不可能です。

于 2012-04-18T22:00:24.407 に答える
1

私もこの問題に苦労しました。上記のポスターの一部が示唆しているように、IOS に付属するフォントはいずれも上付き文字または下付き文字をサポートしていないことが判明しました。私の解決策は、2 つのカスタムの上付き文字フォントと下付き文字フォントを購入してインストールすることでした (これらはそれぞれ 9.99 ドルで、サイトhttp://superscriptfont.com/へのリンクがあります)。

それほど難しいことではありません。フォント ファイルをリソースとして追加し、「アプリケーションが提供するフォント」の info.plist エントリを追加するだけです。

次のステップは、NSAttributedString で適切なタグを検索し、タグを削除して、フォントをテキストに適用することでした。

よく働く!

于 2012-05-07T21:30:00.843 に答える
0

Dimitri の回答に対する Swift 2 のひねり。NSBaselineOffsetAttributeName を効果的に実装します。

コーディングするとき、私は UIView にいたので、使用する合理的な境界 rect がありました。彼の答えは、独自の 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))
        }
    }
}
于 2016-03-07T20:05:38.090 に答える