我想仅使用
UILabel
attributedText
属性对 NSAttributedString
中的第一个字符进行首字下沉。像这样:
(来源:interpretationbydesign.com)
我已经尝试将第一个字符范围的基线调整为负值,它可以将第一个字符的顶部与第一行其余部分的顶部对齐。但我还没有找到任何方法让其他行流到首字下沉字符的右侧。
可以使用
NSAttributedString only
解决这个问题,还是我必须拆分字符串并使用 Core Text 自己渲染它?
正如其他人提到的,仅用
NSAttributedString
是不可能做到这一点的。尼古拉有正确的方法,使用CTFrameSetters
。然而,它可以告诉框架设置器在特定区域(即由CGPath定义)渲染文本。
您必须创建 2 个框架设置器,一个用于首字下沉,另一个用于文本的其余部分。
然后,抓住首字下沉的框架并构建一个围绕首字下沉框架空间运行的
CGPathRef
。
然后,将两个框架设置器渲染到您的视图中。
我创建了一个示例项目,其中包含一个名为 DropCapView 的对象,该对象是 UIView 的子类。此视图呈现第一个字符并将其余文本包裹在其周围。
看起来像这样:
有相当多的步骤,所以我添加了一个指向托管该示例的 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
CoreText 无法进行首字下沉,因为它由字形运行组成的行组成。首字下沉会覆盖多行,这是不受支持的。
要实现此效果,您必须单独绘制盖子,然后在围绕它的路径中绘制其余文本。
长话短说:在 UILabel 中不可能,可能,但在 CoreText 中需要做一些工作。
使用 CoreText 执行此操作的步骤是:
如果您使用的是 UITextView,则可以按照
Dannie P建议的此处使用
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)]
}
}
结果:
不,仅使用
NSAttributedString
和标准字符串绘图无法完成此操作。
由于首字下沉是段落的属性,因此
CTParagraphStyle
必须包含有关首字下沉的信息。 CTParagraphStyle
中影响段落开头缩进的唯一属性是 kCTParagraphStyleSpecifierFirstLineHeadIndent
,但仅影响第一行。
无法告诉
CTFramesetter
如何计算第二行及更多行的开头。
唯一的方法是定义您自己的属性并编写代码来使用承认此自定义属性的
CTFramesetter
和 CTTypesetter
绘制字符串。
现在应该没那么糟糕。这不是真正的下沉式上限,因为它向上延伸,但这对我的情况来说很好。
// have two computed properties
let myText = "Blah blah blah"
var firstCharacter: Character {
myText.removeFirst()
}
var remainingText: Substring {
myText.dropFirst(0)
}
// ...in the body...
VStack {
(Text(String(firstCharacter))
.font(.custom("CosmicSolace-Regular", size: 64))
+ Text(remainingText)
.font(.custom("Subjectivity-Regular", size: 16))
.fontWeight(.light)
)
.foregroundStyle(.bodyText)
.frame(maxWidth: .infinity)
}