问题
I'm trying to add a dotted underline style to an NSAttributedString
(on iOS7+/TextKit). Of course, I tried the built-in NSUnderlinePatternDot
:
NSString *string = self.label.text;
NSRange underlineRange = [string rangeOfString:@"consetetur sadipscing elitr"];
NSMutableAttributedString *attString = [[NSMutableAttributedString alloc] initWithString:string];
[attString addAttributes:@{NSUnderlineStyleAttributeName: @(NSUnderlineStyleSingle | NSUnderlinePatternDot)} range:underlineRange];
self.label.attributedText = attString;
However, the style produced by this is actually rather dashed than dotted:
Am I missing something obvious here (NSUnderlinePatternReallyDotted
? ;) ) or is there perhaps a way to customize the line-dot-pattern?
回答1:
I've spent a little bit of time playing around with Text Kit to get this and other similar scenarios to work. The actual Text Kit solution for this problem is to subclass NSLayoutManager
and override drawUnderline(forGlyphRange:underlineType:baselineOffset:lineFragmentRect:lineFragmentGlyphRange:containerOrigin:)
This is the method that gets called to actually draw the underline that is requested by the attributes in the NSTextStorage
. Here's a description of the parameters that get passed in:
glyphRange
the index of the first glyph and the number of following glyphs to be underlinedunderlineType
theNSUnderlineStyle
value of theNSUnderlineAttributeName
attributebaselineOffset
the distance between the bottom of the bounding box and the baseline offset for the glyphs in the provided rangelineFragmentRect
the rectangle that encloses the entire line that containsglyphRange
lineFragmentGlyphRange
the glyph range that makes up the entire line that containsglyphRange
containerOrigin
the offset of the container within the text view to which it belongs
Steps to draw underline:
- Find the
NSTextContainer
that containsglyphRange
usingNSLayoutManager.textContainer(forGlyphAt:effectiveRange:)
. - Get the bounding rectangle for
glyphRange
usingNSLayoutManager.boundingRect(forGlyphRange:in:)
- Offset the bounding rectangle by the text container origin using
CGRect.offsetBy(dx:dy:)
- Draw your custom underline somewhere between the bottom of the offset bounding rectangle and the baseline (as determined by
baselineOffset
)
回答2:
According to Eiko&Dave's answer, i made an example like this
i've try to finish that by using UILabel rather than UITextView, but could not find a solution after searching from stackoverflow or other websites. So, i used UITextView to do that.
let storage = NSTextStorage()
let layout = UnderlineLayout()
storage.addLayoutManager(layout)
let container = NSTextContainer()
container.widthTracksTextView = true
container.lineFragmentPadding = 0
container.maximumNumberOfLines = 2
container.lineBreakMode = .byTruncatingTail
layout.addTextContainer(container)
let textView = UITextView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width-40, height: 50), textContainer: container)
textView.isUserInteractionEnabled = true
textView.isEditable = false
textView.textContainerInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
textView.text = "1sadasdasdasdasdsadasdfdsf"
textView.backgroundColor = UIColor.white
let rg = NSString(string: textView.text!).range(of: textView.text!)
let attributes = [NSAttributedString.Key.underlineStyle.rawValue: 0x11,
NSAttributedString.Key.underlineColor: UIColor.blue.withAlphaComponent(0.2),
NSAttributedString.Key.foregroundColor: UIColor.black, NSAttributedString.Key.font: UIFont.systemFont(ofSize: 17),
NSAttributedString.Key.baselineOffset:10] as! [NSAttributedString.Key : Any]
storage.addAttributes(attributes, range: rg)
view.addSubview(textView)
override LayoutManage Method
class UnderlineLayout: NSLayoutManager {
override func drawUnderline(forGlyphRange glyphRange: NSRange, underlineType underlineVal: NSUnderlineStyle, baselineOffset: CGFloat, lineFragmentRect lineRect: CGRect, lineFragmentGlyphRange lineGlyphRange: NSRange, containerOrigin: CGPoint) {
if let container = textContainer(forGlyphAt: glyphRange.location, effectiveRange: nil) {
let boundingRect = self.boundingRect(forGlyphRange: glyphRange, in: container)
let offsetRect = boundingRect.offsetBy(dx: containerOrigin.x, dy: containerOrigin.y)
let left = offsetRect.minX
let bottom = offsetRect.maxY-15
let width = offsetRect.width
let path = UIBezierPath()
path.lineWidth = 3
path.move(to: CGPoint(x: left, y: bottom))
path.addLine(to: CGPoint(x: left + width, y: bottom))
path.stroke()
}
}
in my solution, i've to expand lineSpacing also keep a customize underline by using NSAttributedString property NSMutableParagraphStyle().lineSpacing, but it seems that didn't work, but NSAttributedString.Key.baselineOffset is worked. hope can
回答3:
I know this is a 2 year old story but maybe it will help someone facing the same problem, like I did last week.
My workaround was to subclass UITextView
, add a subview (the dotted lines container) and use the textView’s layoutManager method enumerateLineFragmentsForGlyphRange
to get the number of lines and their frames.
Knowing the line’s frame I calculated the y
where I wanted the dots. So I created a view (lineView in which I drawn dots), set clipsToBounds
to YES
, the width to that of the textView and then
added as a subview in the linesContainer at that y
.
After that I set the lineView’s width
to that returned by enumerateLineFragmentsForGlyphRange
.
For multiple rows the same approach: update frames, add new lines or remove according to what enumerateLineFragmentsForGlyphRange
returns.
This method is called in textViewDidChange
after each text update.
Here is the code to get the array of lines and their frames
NSLayoutManager *layoutManager = [self layoutManager];
[layoutManager enumerateLineFragmentsForGlyphRange:NSMakeRange(0, [layoutManager numberOfGlyphs])
usingBlock:^(CGRect rect, CGRect usedRect, NSTextContainer *textContainer, NSRange glyphRange, BOOL *stop) {
[linesFramesArray addObject:NSStringFromCGRect(usedRect)];
}
];
来源:https://stackoverflow.com/questions/25523910/customize-underline-pattern-in-nsattributedstring-ios7