Как поместить текст в круг в UILabel
Я хотел бы пропустить текст в 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;
Ides?
4 ответов
это казалось очень простым решением. NSTextContainer
есть exclusionPaths
собственность. Вы можете создать два пути Безье, которые будут определять области, которые следует исключить.
Итак, я сделал это и вот мой способ:
- (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, потому что это не дает вам доступ к стеку 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;
}
грубый, но эффективный. Выглядит так:
С Swift 4 и iOS 11,NSTextContainer
есть свойство под названием exclusionPaths
. exclusionPaths
есть следующее объявление:
массив объектов path, представляющих области, в которых текст не отображается в текстовом контейнере.
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.Свифт!--43-->
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.Свифт!--43-->
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(_:)
, вы будете иметь следующие проявления:
вот мой вклад в вышеупомянутый вопрос в 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
}
}