使用NSAttributedString删除上限

时间:2013-01-08 12:25:00

标签: ios uilabel core-text typography nsattributedstring

我想仅使用UILabel attributedText属性在NSAttributedString中使用首字下沉字符。像这样:

http://www.interpretationbydesign.com/wp-content/uploads/2009/02/drop_caps.gif

我已经尝试将第一个字符范围的基线调整为负值,并且它可以将第一个字符的顶部与第一个字符的其余部分的顶部对齐。但是我没有找到任何方法让其他线条流到掉落角色的右侧。

可以使用NSAttributedString only解决这个问题,还是必须拆分字符串并使用Core Text自行渲染?

5 个答案:

答案 0 :(得分:15)

正如其他人提到的那样,只有NSAttributedString才能做到这一点。尼古拉使用CTFrameSetters有正确的方法。但是 可以告诉framesetter在特定区域中呈现文本(即由CGPath定义)。

你必须创建2个框架,一个用于下拉帽,另一个用于文本的其余部分。

然后,抓住首字下沉的框架并构建一个CGPathRef,围绕下降框的框架空间运行。

然后,将两个框架设置渲染到视图中。

我用一个名为DropCapView的对象创建了一个示例项目,该对象是UIView的子类。此视图呈现第一个字符并包围其周围的剩余文本。

看起来像这样:

dropcap on ios

有很多步骤,所以我添加了一个链接到托管该示例的github项目。项目中有评论可以帮助您。

DropCap project on GitHub

您必须使用textBox元素的形状(即CGPathRef)来围绕视图边缘进行填充,并将其拧紧到首字母上。

以下是绘图方法的内容:

- (void)drawRect:(CGRect)rect {
    //make sure that all the variables exist and are non-nil
    NSAssert(_text != nil, @"text is nil");
    NSAssert(_textColor != nil, @"textColor is nil");
    NSAssert(_fontName != nil, @"fontName is nil");
    NSAssert(_dropCapFontSize > 0, @"dropCapFontSize is <= 0");
    NSAssert(_textFontSize > 0, @"textFontSize is <=0");

    //convert the text aligment from NSTextAligment to CTTextAlignment
    CTTextAlignment ctTextAlignment = NSTextAlignmentToCTTextAlignment(_textAlignment);

    //create a paragraph style
    CTParagraphStyleSetting paragraphStyleSettings[] = { {
            .spec = kCTParagraphStyleSpecifierAlignment,
            .valueSize = sizeof ctTextAlignment,
            .value = &ctTextAlignment
        }
    };

    CFIndex settingCount = sizeof paragraphStyleSettings / sizeof *paragraphStyleSettings;
    CTParagraphStyleRef style = CTParagraphStyleCreate(paragraphStyleSettings, settingCount);

    //create two fonts, with the same name but differing font sizes
    CTFontRef dropCapFontRef = CTFontCreateWithName((__bridge CFStringRef)_fontName, _dropCapFontSize, NULL);
    CTFontRef textFontRef = CTFontCreateWithName((__bridge CFStringRef)_fontName, _textFontSize, NULL);

    //create a dictionary of style elements for the drop cap letter
    NSDictionary *dropCapDict = [NSDictionary dictionaryWithObjectsAndKeys:
                                (__bridge id)dropCapFontRef, kCTFontAttributeName,
                                _textColor.CGColor, kCTForegroundColorAttributeName,
                                style, kCTParagraphStyleAttributeName,
                                @(_dropCapKernValue) , kCTKernAttributeName,
                                nil];
    //convert it to a CFDictionaryRef
    CFDictionaryRef dropCapAttributes = (__bridge CFDictionaryRef)dropCapDict;

    //create a dictionary of style elements for the main text body
    NSDictionary *textDict = [NSDictionary dictionaryWithObjectsAndKeys:
                                 (__bridge id)textFontRef, kCTFontAttributeName,
                                 _textColor.CGColor, kCTForegroundColorAttributeName,
                                 style, kCTParagraphStyleAttributeName,
                                 nil];
    //convert it to a CFDictionaryRef
    CFDictionaryRef textAttributes = (__bridge CFDictionaryRef)textDict;

    //clean up, because the dictionaries now have copies
    CFRelease(dropCapFontRef);
    CFRelease(textFontRef);
    CFRelease(style);

    //create an attributed string for the dropcap
    CFAttributedStringRef dropCapString = CFAttributedStringCreate(kCFAllocatorDefault,
                                                                   (__bridge CFStringRef)[_text substringToIndex:1],
                                                                   dropCapAttributes);

    //create an attributed string for the text body
    CFAttributedStringRef textString = CFAttributedStringCreate(kCFAllocatorDefault,
                                                                (__bridge CFStringRef)[_text substringFromIndex:1],
                                                                   textAttributes);

    //create an frame setter for the dropcap
    CTFramesetterRef dropCapSetter = CTFramesetterCreateWithAttributedString(dropCapString);

    //create an frame setter for the dropcap
    CTFramesetterRef textSetter = CTFramesetterCreateWithAttributedString(textString);

    //clean up
    CFRelease(dropCapString);
    CFRelease(textString);

    //get the size of the drop cap letter
    CFRange range;
    CGSize maxSizeConstraint = CGSizeMake(200.0f, 200.0f);
    CGSize dropCapSize = CTFramesetterSuggestFrameSizeWithConstraints(dropCapSetter,
                                                                      CFRangeMake(0, 1),
                                                                      dropCapAttributes,
                                                                      maxSizeConstraint,
                                                                      &range);

    //create the path that the main body of text will be drawn into
    //i create the path based on the dropCapSize
    //adjusting to tighten things up (e.g. the *0.8,done by eye)
    //to get some padding around the edges of the screen
    //you could go to +5 (x) and self.frame.size.width -5 (same for height)
    CGMutablePathRef textBox = CGPathCreateMutable();
    CGPathMoveToPoint(textBox, nil, dropCapSize.width, 0);
    CGPathAddLineToPoint(textBox, nil, dropCapSize.width, dropCapSize.height * 0.8); 
    CGPathAddLineToPoint(textBox, nil, 0, dropCapSize.height * 0.8);
    CGPathAddLineToPoint(textBox, nil, 0, self.frame.size.height);
    CGPathAddLineToPoint(textBox, nil, self.frame.size.width, self.frame.size.height);
    CGPathAddLineToPoint(textBox, nil, self.frame.size.width, 0);
    CGPathCloseSubpath(textBox);

    //create a transform which will flip the CGContext into the same orientation as the UIView
    CGAffineTransform flipTransform = CGAffineTransformIdentity;
    flipTransform = CGAffineTransformTranslate(flipTransform,
                                               0,
                                               self.bounds.size.height);
    flipTransform = CGAffineTransformScale(flipTransform, 1, -1);

    //invert the path for the text box
    CGPathRef invertedTextBox = CGPathCreateCopyByTransformingPath(textBox,
                                                                   &flipTransform);
    CFRelease(textBox);

    //create the CTFrame that will hold the main body of text
    CTFrameRef textFrame = CTFramesetterCreateFrame(textSetter,
                                                    CFRangeMake(0, 0),
                                                    invertedTextBox,
                                                    NULL);
    CFRelease(invertedTextBox);
    CFRelease(textSetter);

    //create the drop cap text box
    //it is inverted already because we don't have to create an independent cgpathref (like above)
    CGPathRef dropCapTextBox = CGPathCreateWithRect(CGRectMake(_dropCapKernValue/2.0f,
                                                               0,
                                                               dropCapSize.width,
                                                               dropCapSize.height),
                                                    &flipTransform);
    CTFrameRef dropCapFrame = CTFramesetterCreateFrame(dropCapSetter,
                                                       CFRangeMake(0, 0),
                                                       dropCapTextBox,
                                                       NULL);
    CFRelease(dropCapTextBox);
    CFRelease(dropCapSetter);

    //draw the frames into our graphic context
    CGContextRef gc = UIGraphicsGetCurrentContext();
    CGContextSaveGState(gc); {
        CGContextConcatCTM(gc, flipTransform);
        CTFrameDraw(dropCapFrame, gc);
        CTFrameDraw(textFrame, gc);
    } CGContextRestoreGState(gc);
    CFRelease(dropCapFrame);
    CFRelease(textFrame);
}

P.S。这带来了一些灵感:https://stackoverflow.com/a/9272955/1218605

答案 1 :(得分:7)

CoreText无法执行首字下沉,因为它包含由字形运行组成的行。下降盖将覆盖多条不受支持的线。

要实现此效果,您必须单独绘制上限,然后在围绕它的路径中绘制其余文本。

长话短说:在UILabel中是不可能的,但是可以使用CoreText进行相当多的工作。

使用CoreText执行此操作的步骤如下:

  • 为单个字符创建框架设置。
  • 得到它的界限
  • 创建一个可以省略掉落框架的路径
  • 使用此路径为剩余字符创建框架设置
  • 绘制第一个字形
  • 画休息

答案 2 :(得分:3)

不,仅使用NSAttributedString和标准字符串绘图无法完成此操作。

由于首字下沉是段落的属性,CTParagraphStyle必须包含有关首字下沉的信息。 CTParagraphStyle中唯一影响段落开头缩进的属性是kCTParagraphStyleSpecifierFirstLineHeadIndent,但这仅影响第一行。

没有办法告诉CTFramesetter如何计算第二行和更多行的开头。

唯一的方法是定义自己的属性并编写代码以使用CTFramesetterCTTypesetter来确认此自定义属性。

答案 3 :(得分:2)

如果您使用的是UITextView,则可以按照Dannie P建议的here使用textView.textContainer.exclusionPaths

Swift中的示例:

class WrappingTextVC: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()

    let textView = UITextView()
    textView.translatesAutoresizingMaskIntoConstraints = false
    textView.text = "ropcap example. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris aliquam vulputate ex. Fusce interdum ultricies justo in tempus. Sed ornare justo in purus dignissim, et rutrum diam pulvinar. Quisque tristique eros ligula, at dictum odio tempor sed. Fusce non nisi sapien. Donec libero orci, finibus ac libero ac, tristique pretium ex. Aenean eu lorem ut nulla elementum imperdiet. Ut posuere, nulla ut tincidunt viverra, diam massa tincidunt arcu, in lobortis erat ex sed quam. Mauris lobortis libero magna, suscipit luctus lacus imperdiet eu. Ut non dignissim lacus. Vivamus eget odio massa. Aenean pretium eget erat sed ornare. In quis tortor urna. Quisque euismod, augue vel pretium suscipit, magna diam consequat urna, id aliquet est ligula id eros. Duis eget tristique orci, quis porta turpis. Donec commodo ullamcorper purus. Suspendisse et hendrerit mi. Nulla pellentesque semper nibh vitae vulputate. Pellentesque quis volutpat velit, ut bibendum magna. Morbi sagittis, erat rutrum  Suspendisse potenti. Nulla facilisi. Praesent libero est, tincidunt sit amet tempus id, blandit sit amet mi. Morbi sed odio nunc. Mauris lobortis elementum orci, at consectetur nisl egestas a. Pellentesque vel lectus maximus, semper lorem eget, accumsan mi. Etiam semper tellus ac leo porta lobortis."
    textView.backgroundColor = .lightGray
    textView.textColor = .black
    view.addSubview(textView)

    textView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20).isActive = true
    textView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20).isActive = true
    textView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20).isActive = true
    textView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -40).isActive = true

    let dropCap = UILabel()
    dropCap.text = "D"
    dropCap.font = UIFont.boldSystemFont(ofSize: 60)
    dropCap.backgroundColor = .lightText
    dropCap.sizeToFit()
    textView.addSubview(dropCap)

    textView.textContainer.exclusionPaths = [UIBezierPath(rect: dropCap.frame)]
  }
}

结果:

Text wraps around dropcap

Full example on github

答案 4 :(得分:1)

不是一个完美的解决方案,但您应该尝试DTCoreText并将正常NSString渲染为formatted HTML。在HTML中,可以“删除上限”一个字母。