How to fit text in a circle in UILabel

前端 未结 4 1891
情歌与酒
情歌与酒 2020-12-28 20:51

I\'d like to flow the text in UILabel into a circle (instead of rect). I did some experiments with NSLayoutManager, NSTextContainer an

相关标签:
4条回答
  • 2020-12-28 20:52

    With Swift 4 and iOS 11, NSTextContainer has a property called exclusionPaths. exclusionPaths has the following declaration:

    An array of path objects representing the regions where text is not displayed in the text container.

    var exclusionPaths: [UIBezierPath] { get set }
    

    Besides, UIBezierPath has a property called usesEvenOddFillRule. usesEvenOddFillRule has the following declaration:

    A Boolean indicating whether the even-odd winding rule is in use for drawing paths.

    var usesEvenOddFillRule: Bool { get set }
    

    By using usesEvenOddFillRule, you can create an exclusion path surrounding a circle with only a few lines of code:

    var exclusionPath: UIBezierPath {
        let path = UIBezierPath(ovalIn: bounds)
        path.append(UIBezierPath(rect: bounds))
        path.usesEvenOddFillRule = true
        return path
    }
    

    The following UITextView and UIViewController subclasses show how to display text inside a circle using NSTextContainer exclusionPaths and UIBezierPath usesEvenOddFillRule properties:

    TextView.swift

    import UIKit
    
    class TextView: UITextView {
    
        convenience init() {
            self.init(frame: .zero, textContainer: nil)
        }
    
        override init(frame: CGRect, textContainer: NSTextContainer?) {
            super.init(frame: frame, textContainer: textContainer)
    
            isScrollEnabled = false
            isEditable = false
            textContainerInset = .zero
            self.textContainer.lineBreakMode = .byTruncatingTail
        }
    
        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    
        var exclusionPath: UIBezierPath {
            let path = UIBezierPath(ovalIn: bounds)
            path.append(UIBezierPath(rect: bounds))
            path.usesEvenOddFillRule = true
            return path
        }
    
    }
    
    extension TextView {
    
        // Draw circle
    
        override func draw(_ rect: CGRect) {
            UIColor.orange.setFill()
            let path = UIBezierPath(ovalIn: rect)
            path.fill()
        }
    
        // Draw exclusion path
    
        /*
         override func draw(_ rect: CGRect) {
             UIColor.orange.setFill()
             exclusionPath.fill()
         }
         */
    
    }
    

    ViewController.swift

    import UIKit
    
    class ViewController: UIViewController {
    
        let textView = TextView()
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            let string = "Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda."
            textView.attributedText = NSAttributedString(string: string)
            view.addSubview(textView)
    
            textView.translatesAutoresizingMaskIntoConstraints = false
            let horizontalConstraint = textView.centerXAnchor.constraint(equalTo: view.centerXAnchor)
            let verticalConstraint = textView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
            let widthConstraint = textView.widthAnchor.constraint(equalToConstant: 240)
            let heightConstraint = textView.heightAnchor.constraint(equalToConstant: 240)
            NSLayoutConstraint.activate([horizontalConstraint, verticalConstraint, widthConstraint, heightConstraint])            
        }
    
        override func viewDidLayoutSubviews() {
            super.viewDidLayoutSubviews()
            textView.textContainer.exclusionPaths = [textView.exclusionPath]
        }
    
    }
    

    By picking one or the other implementation of draw(_:), you will have the following displays:

    0 讨论(0)
  • 2020-12-28 20:53

    Here is my contribution to the above question in Swift 3. https://github.com/icatmed/ICRoundLabel.git

    import UIKit
    import CoreText
    
    @IBDesignable
    open class ICRoundLabel: UILabel {
    
    // Switch on/off text rounding, is on by default
    @IBInspectable open dynamic var isRounded:Bool = true {
        didSet{
            setNeedsDisplay()
        }
    }
    
    // Specify text alignment
    @available(*, unavailable, message: "This property is reserved for Interface Builder. Use 'roundedTextAlignment' instead.")
    @IBInspectable open dynamic var alignment:UInt8 {
        set{
            self.roundedTextAlignment = CTTextAlignment(rawValue: newValue)!
            setNeedsDisplay()
        }
        get{
            return roundedTextAlignment.rawValue
        }
    }
    
    // Font scale
    @IBInspectable open dynamic var fillTextInCenter:Bool = true {
        didSet{
            setNeedsDisplay()
        }
    }
    
    // Font step
    @available(*, unavailable, message: "This property is reserved for Interface Builder. Use 'internalFontStep' instead.")
    @IBInspectable open dynamic var fontStep:CGFloat {
        set(newValue) {
            internalFontStep = max(newValue, 0.1)
        }
        get {
            return internalFontStep
        }
    }
    
    open var roundedTextAlignment:CTTextAlignment = .center
    open var internalFontStep:CGFloat = 1
    
    override open func drawText(in rect: CGRect) {
    
        // Check if custom text draw is needed
        if !isRounded {
            super.drawText(in: rect)
            return
        }
    
        // Check if text exists
        guard let text = self.text else {
            return
        }
    
        if text == "" {
            return
        }
    
        // Get graphics context
        guard let context = UIGraphicsGetCurrentContext() else {
            return
        }
    
        //MARK: Create attributed string
        var stringRange = NSMakeRange(0, text.characters.count)
        let attrString = CFAttributedStringCreate(kCFAllocatorDefault, text as CFString!, attributedText?.attributes(at: 0, effectiveRange: &stringRange) as CFDictionary!)
        let attributedString = CFAttributedStringCreateMutableCopy(kCFAllocatorDefault, CFIndex.max, attrString)!
        let stringLength = CFAttributedStringGetLength(attributedString)
    
        // Set a paragraph style
        let cfStringRange = CFRangeMake(0, stringLength)
        let settings = [CTParagraphStyleSetting(spec: .alignment, valueSize: MemoryLayout.size(ofValue: roundedTextAlignment), value: &roundedTextAlignment)]
        let paragraphStyle = CTParagraphStyleCreate(settings, 1)
    
        CFAttributedStringSetAttribute(attributedString, cfStringRange, kCTParagraphStyleAttributeName, paragraphStyle)
    
        // Make custom transitions with context
        context.translateBy(x: 0.0, y: frame.size.height)
        context.scaleBy(x: 1.0, y: -1.0)
    
        // New drawing rect with insets
        let drawingRect = CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: rect.size.width, height: rect.size.height))
    
        // Align text in center
        var boundingBox = text.boundingRect(with: drawingRect.size, options: NSStringDrawingOptions.usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil)
    
        //MARK: Create elliptical path
        var path = CGPath(roundedRect: drawingRect, cornerWidth: drawingRect.width/2, cornerHeight: drawingRect.height/2, transform: nil)
    
        //MARK: Frame and range calculation nested function
        func getTextFrameRange() -> (CTFrame, CFRange) {
            let textFrame = CTFramesetterCreateFrame(CTFramesetterCreateWithAttributedString(attributedString), cfStringRange, path, nil)
            let rangeThatFits = CTFrameGetVisibleStringRange(textFrame)
            return (textFrame, rangeThatFits)
        }
    
        var textFrame:CTFrame
        var rangeThatFits:CFRange
    
        //MARK: Scaling font size if needed
    
        if fillTextInCenter {
    
            var fontSize = font.pointSize
            var estimatedFont = font.withSize(fontSize)
    
            // Pin text in center of initial rect
            var boxHeight = ceil(boundingBox.height)
    
            func updateBoundingBox() {
                boundingBox.origin = CGPoint(x: ceil((drawingRect.size.height - boxHeight)/2), y: ceil((drawingRect.size.height - boxHeight)/2))
                boundingBox.size = CGSize(width: boxHeight, height: boxHeight)
            }
    
            path = CGPath(roundedRect: boundingBox, cornerWidth: boundingBox.width/2, cornerHeight: boundingBox.height/2, transform: nil)
    
            (_, rangeThatFits) = getTextFrameRange()
    
            updateBoundingBox()
    
            // Fit text in center
            while cfStringRange.length != rangeThatFits.length {
    
                // Increase size of bounding box size if needed
                // or decrease font size
                if boundingBox.width < drawingRect.width {
    
                    boxHeight += 1
    
                    //Update bounding box accoringly to new box size
                    updateBoundingBox()
    
                    path = CGPath(roundedRect: boundingBox, cornerWidth: boundingBox.width/2, cornerHeight: boundingBox.height/2, transform: nil)
    
                    (_, rangeThatFits) = getTextFrameRange()
    
                    continue
                } else {
    
                    CFAttributedStringSetAttribute(attributedString, cfStringRange, kCTFontAttributeName, estimatedFont)
    
                    (_, rangeThatFits) = getTextFrameRange()
    
                    // Increase or decrease font size
                    fontSize += cfStringRange.length < rangeThatFits.length ? internalFontStep : -internalFontStep
                    estimatedFont = font.withSize(fontSize)
                }
            }
        }
    
        //MARK: Draw the text frame in the view's graphics context
        (textFrame, _) = getTextFrameRange()
        CTFrameDraw(textFrame, context)
    
    }
    
    @IBInspectable var borderColor: UIColor = UIColor.white {
        didSet {
            layer.borderColor = borderColor.cgColor
        }
    }
    
    @IBInspectable var borderWidth: CGFloat = 1.0 {
        didSet {
            layer.borderWidth = borderWidth
        }
    }
    
    override open func layoutSubviews() {
        super.layoutSubviews()
        layer.cornerRadius = 0.5 * bounds.size.width
        clipsToBounds = true
    
    
    }
    

    }

    0 讨论(0)
  • 2020-12-28 20:54

    This appeared to be a very simple solution. NSTextContainer has an exclusionPaths property. What you can do is to create two Bezier paths that will define areas that should be excluded.

    enter image description here

    So I did that and here is my method:

    - (void)setCircularExclusionPathWithCenter:(CGPoint)center radius:(CGFloat)radius textView:(UITextView *)textView
    {
        UIBezierPath *topHalf = [UIBezierPath bezierPath];
        [topHalf moveToPoint:CGPointMake(center.x - radius, center.y + radius)];
        [topHalf addLineToPoint:CGPointMake(center.x - radius, center.y)];
        [topHalf addArcWithCenter:center radius:radius startAngle:M_PI endAngle:0.0f clockwise:NO];
        [topHalf addLineToPoint:CGPointMake(center.x + radius, center.y + radius)];
        [topHalf closePath];
    
        UIBezierPath *bottomHalf = [UIBezierPath bezierPath];
        [bottomHalf moveToPoint:CGPointMake(center.x - radius, center.y - radius)];
        [bottomHalf addLineToPoint:CGPointMake(center.x - radius, center.y)];
        [bottomHalf addArcWithCenter:center radius:radius startAngle:M_PI endAngle:0 clockwise:YES];
        [bottomHalf addLineToPoint:CGPointMake(center.x + radius, center.y - radius)];
        [bottomHalf closePath];
    
        textView.textContainer.exclusionPaths = @[bottomHalf, topHalf];
    }
    

    Example usage:

    [self setCircularExclusionPathWithCenter:CGPointMake(160.0f, 200.0f)
                                      radius:100.0f
                                    textView:_textView];
    

    And a result of my experiments:

    enter image description here

    Of course you will have to use a UITextView instead of UILabel but I hope it helps :)

    0 讨论(0)
  • 2020-12-28 21:02

    You can't do this in a UILabel because it doesn't give you access to the TextKit stack. What I do is to build my own TextKit stack and subclass NSTextContainer:

    -(CGRect)lineFragmentRectForProposedRect:(CGRect)proposedRect atIndex:(NSUInteger)characterIndex writingDirection:(NSWritingDirection)baseWritingDirection remainingRect:(CGRect *)remainingRect {
        CGRect result = [super lineFragmentRectForProposedRect:proposedRect atIndex:characterIndex writingDirection:baseWritingDirection remainingRect:remainingRect];
        CGRect r = CGRectMake(0,0,self.size.width,self.size.height);
        UIBezierPath* circle = [UIBezierPath bezierPathWithOvalInRect:r];
        CGPoint p = result.origin;
        while (![circle containsPoint:p]) {
            p.x += .1;
            result.origin = p;
        }
        CGFloat w = result.size.width;
        p = result.origin;
        p.x += w;
        while (![circle containsPoint:p]) {
            w -= .1;
            result.size.width = w;
            p = result.origin;
            p.x += w;
        }
        return result;
    }
    

    Crude but effective. Looks like this:

    enter image description here

    0 讨论(0)
提交回复
热议问题