Customize underline pattern in NSAttributedString (iOS7+)

坚强是说给别人听的谎言 提交于 2019-12-10 15:05:39

问题


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 underlined
  • underlineType the NSUnderlineStyle value of the NSUnderlineAttributeName attribute
  • baselineOffset the distance between the bottom of the bounding box and the baseline offset for the glyphs in the provided range
  • lineFragmentRect the rectangle that encloses the entire line that contains glyphRange
  • lineFragmentGlyphRange the glyph range that makes up the entire line that contains glyphRange
  • containerOrigin the offset of the container within the text view to which it belongs

Steps to draw underline:

  1. Find the NSTextContainer that contains glyphRange using NSLayoutManager.textContainer(forGlyphAt:effectiveRange:).
  2. Get the bounding rectangle for glyphRange using NSLayoutManager.boundingRect(forGlyphRange:in:)
  3. Offset the bounding rectangle by the text container origin using CGRect.offsetBy(dx:dy:)
  4. 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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!