如何在 UILabel 中将文本放入圆圈中
Posted
技术标签:
【中文标题】如何在 UILabel 中将文本放入圆圈中【英文标题】:How to fit text in a circle in UILabel 【发布时间】:2014-03-04 17:25:41 【问题描述】:我想将UILabel
中的文本排列成一个圆圈(而不是矩形)。
我用NSLayoutManager
、NSTextContainer
和NSTextStorage
做了一些实验,但它似乎不起作用。
下面的示例应该将文本流入一个较小的 40x40 矩形(标签为 120x120),但似乎没有任何效果。
UIFont *font = [UIFont fontWithName:@"HelveticaNeue" size:12];
NSTextStorage *ts = [[NSTextStorage alloc] initWithString:multiline.title attributes:@NSFontAttributeName:font];
NSLayoutManager *lm = [[NSLayoutManager alloc] init];
NSTextContainer *tc = [[NSTextContainer alloc] initWithSize:CGSizeMake(40, 40)];
[lm addTextContainer:tc];
[ts addLayoutManager:lm];
self.label.attributedText = ts;
想法?
【问题讨论】:
【参考方案1】:这似乎是一个非常简单的解决方案。 NSTextContainer
有一个 exclusionPaths
属性。您可以做的是创建两条 Bezier 路径来定义应排除的区域。
所以我这样做了,这是我的方法:
- (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];
示例用法:
[self setCircularExclusionPathWithCenter:CGPointMake(160.0f, 200.0f)
radius:100.0f
textView:_textView];
我的实验结果:
当然,您将不得不使用 UITextView 而不是 UILabel,但我希望它会有所帮助:)
【讨论】:
但问题不在于沿着路径绘制。 这是我想使用的替代方案。我想要 UILabel 的原因是如果文本太大,则截断“...”,如果文本只有 1 行,则垂直居中。 我猜你可以将文本对齐到中心,它应该可以工作。要获得...
,您可以尝试:_textView.textContainer.lineBreakMode = NSLineBreakByTruncatingTail;
。我尚未对其进行测试,但它可能会起作用。【参考方案2】:
您不能在 UILabel 中执行此操作,因为它无法让您访问 TextKit 堆栈。我所做的是构建自己的 TextKit 堆栈和子类 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;
粗鲁但有效。看起来像这样:
【讨论】:
似乎是一个不错的解决方案。我想要 UILabel 的原因是如果文本太大,则截断“...”,如果文本只有 1 行,则垂直居中。有什么想法吗? @matt 这在 swift 中是如何工作的?可以举个例子吗? “它在 Swift 中是如何工作的?”当然与 Objective-C 中的相同。您只需将该代码翻译成 Swift。【参考方案3】:在 Swift 4 和 ios 11 中,NSTextContainer
有一个名为 exclusionPaths
的属性。 exclusionPaths
有以下声明:
表示文本容器中不显示文本的区域的路径对象数组。
var exclusionPaths: [UIBezierPath] get set
此外,UIBezierPath
有一个名为 usesEvenOddFillRule
的属性。 usesEvenOddFillRule
有以下声明:
一个布尔值,指示奇偶缠绕规则是否用于绘制路径。
var usesEvenOddFillRule: Bool get set
通过使用usesEvenOddFillRule
,您可以只用几行代码创建一个围绕一个圆圈的排除路径:
var exclusionPath: UIBezierPath
let path = UIBezierPath(ovalIn: bounds)
path.append(UIBezierPath(rect: bounds))
path.usesEvenOddFillRule = true
return path
以下UITextView
和UIViewController
子类展示了如何使用NSTextContainer
exclusionPaths
和UIBezierPath
usesEvenOddFillRule
属性在圆圈内显示文本:
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]
通过选择draw(_:)
的一种或另一种实现,您将获得以下显示:
【讨论】:
这解决了作者的原始要求,即显示 ... 用于文本截断。 UILabel 不需要获得尾部截断。【参考方案4】:这是我在 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
【讨论】:
感谢您的编辑。我没有投反对票,您的帖子出现在审核队列中,我对其进行了相应的分类。除非经过编辑以包含内容,否则通常会删除仅链接的答案。我猜反对票要么是自动的,要么是其他人的工作。 没有自动投反对票这样的东西。以上是关于如何在 UILabel 中将文本放入圆圈中的主要内容,如果未能解决你的问题,请参考以下文章