【问题标题】:Drop cap with NSAttributedString带有 NSAttributedString 的首字下沉
【发布时间】:2012-12-22 07:13:18
【问题描述】:

我想只使用 attributedText NSAttributedString 属性在 UILabel 中做一个首字下沉。像这样:


(来源:interpretationbydesign.com

我已经尝试将第一个字符范围的基线调整为负值,它适用于将第一个字符的顶部与第一行其余部分的顶部对齐。但是我还没有找到任何方法让其他行流到下垂字符的右侧。

这可以使用NSAttributedString only 解决,还是我必须拆分字符串并使用Core Text 自己呈现?

【问题讨论】:

  • 你能放一张你到目前为止所取得的成就的截图吗?还有测试代码?
  • 我在我的应用程序中使用首字下沉字符串,但为此,我使用了 UIWebView 并使用 HTML 来实现该效果。我不确定它可以在 UILabel 中完成

标签: ios uilabel core-text typography nsattributedstring


【解决方案1】:

正如其他人所提到的,仅使用NSAttributedString 是不可能做到这一点的。 Nikolai 有正确的方法,使用CTFrameSetters。但是,可以告诉框架设置器在特定区域(即由 CGPath 定义)呈现文本。

您必须创建 2 个框架设置器,一个用于首字下沉,另一个用于其余文本。

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

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

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

看起来像这样:

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

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

附:这来自于:https://stackoverflow.com/a/9272955/1218605

【讨论】:

    【解决方案2】:

    CoreText 不能使用首字下沉,因为它由字形运行组成的行组成。首字下沉将覆盖不支持的多行。

    要达到这种效果,您必须单独绘制帽子,然后在围绕它的路径中绘制其余文本。

    长话短说:在 UILabel 中不可能,但可能,但在 CoreText 中需要做一些工作。

    使用 CoreText 的步骤是:

    • 为单个字符创建框架设置器。
    • 得到它的界限
    • 创建一个路径,以避开首字下沉的框架
    • 使用此路径为其余字符创建框架设置器
    • 绘制第一个字形
    • 画休息

    【讨论】:

    • 我不想用 UILabel 来做,我想用 Core Text 来做,但只使用 NSAttributesString。不是几个框架设置器,也不是一个带路径的框架设置器。
    • 正如我所说,使用单个属性字符串是不可能的。请参阅我的 CoreText 介绍以了解框架设置的工作原理。 cocoanetics.com/2011/01/befriending-core-text
    【解决方案3】:

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

    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)]
      }
    }
    

    结果:

    Full example on github

    【讨论】:

      【解决方案4】:

      不,这不能通过 NSAttributedString 和标准字符串绘图来完成。

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

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

      唯一的方法是定义您自己的属性并编写代码以使用确认此自定义属性的CTFramesetterCTTypesetter 绘制字符串。

      【讨论】:

        【解决方案5】:

        不是一个完美的解决方案,但您应该尝试 DTCoreText 并将您的正常 NSString 呈现为 formatted HTML。在 HTML 中,可以“首字母大写”。

        【讨论】:

          猜你喜欢
          • 2010-09-29
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2018-12-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多