Jak zlokalizować cgrect dla fragmentu tekstu w etykiecie UILabel?
Dla danego NSRange
, chciałbym znaleźć {[2] } w UILabel
, który odpowiada glifom tego NSRange
. Na przykład chciałbym znaleźć CGRect
, który zawiera słowo " pies "w zdaniu" szybki brązowy lis przeskakuje nad leniwym psem."
Sztuczka polega na tym, że UILabel
ma wiele linii, a tekst jest naprawdę attributedText
, więc trudno jest znaleźć dokładną pozycję łańcucha.
Metoda, którą chciałbym napisać na mojej podklasie UILabel
wyglądałoby to mniej więcej tak:
- (CGRect)rectForSubstringWithRange:(NSRange)range;
szczegóły, dla zainteresowanych:
Moim celem jest to, aby móc stworzyć nowy UILabel z dokładnym wyglądem i pozycją UILabel, który następnie mogę animować. mam resztę rozpracowaną, ale to właśnie ten krok powstrzymuje mnie w tej chwili.
Co zrobiłem, aby spróbować rozwiązać problem do tej pory:
- miałem nadzieję, że z iOS 7, byłoby trochę zestawu tekstowego, który rozwiązałby ten problem, ale większość przykładów, które widziałem z zestawem tekstowym, skupia się na
UITextView
iUITextField
, a nie naUILabel
. - widziałem inne pytanie na Stack Overflow tutaj, które obiecuje rozwiązać problem, ale przyjęta odpowiedź ma ponad dwa lata, a kod nie działa dobrze z przypisanym tekstem.
Założę się, że właściwa odpowiedź na to pytanie obejmuje jedną z następujących kwestii:
- używanie standardowego tekstu Metoda Kit, aby rozwiązać ten problem w jednej linii kodu. Założę się, że obejmowałoby
NSLayoutManager
itextContainerForGlyphAtIndex:effectiveRange
- pisanie złożonej metody, która rozbija UILabel na linie i znajduje rect glifu w linii, prawdopodobnie używając podstawowych metod tekstowych. Moim obecnym najlepszym rozwiązaniem jest rozebranie @mattt ' S excellent TTTAttributedLabel , który ma metodę, która znajduje glif w punkcie - jeśli go odwrócę i znajdę punkt dla glifu, może to praca.
Aktualizacja: oto GitHub gist z trzema rzeczami, które próbowałem do tej pory rozwiązać ten problem: https://gist.github.com/bryanjclark/7036101
8 answers
Po odpowiedź Joshuy w kodzie, wymyśliłem następujące, które wydają się działać dobrze:
- (CGRect)boundingRectForCharacterRange:(NSRange)range
{
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:[self attributedText]];
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
[textStorage addLayoutManager:layoutManager];
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:[self bounds].size];
textContainer.lineFragmentPadding = 0;
[layoutManager addTextContainer:textContainer];
NSRange glyphRange;
// Convert the range for glyphs.
[layoutManager characterRangeForGlyphRange:range actualGlyphRange:&glyphRange];
return [layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:textContainer];
}
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 11:54:39
Budowanie z Luke Rogers ' s answer ale napisane w swift:
Swift 2
extension UILabel {
func boundingRectForCharacterRange(_ range: NSRange) -> CGRect? {
guard let attributedText = attributedText else { return nil }
let textStorage = NSTextStorage(attributedString: attributedText)
let layoutManager = NSLayoutManager()
textStorage.addLayoutManager(layoutManager)
let textContainer = NSTextContainer(size: bounds.size)
textContainer.lineFragmentPadding = 0.0
layoutManager.addTextContainer(textContainer)
var glyphRange = NSRange()
// Convert the range for glyphs.
layoutManager.characterRangeForGlyphRange(range, actualGlyphRange: &glyphRange)
return layoutManager.boundingRectForGlyphRange(glyphRange, inTextContainer: textContainer)
}
}
Przykładowe Użycie (Swift 2)
let label = UILabel()
let text = "aa bb cc"
label.attributedText = NSAttributedString(string: text)
let sublayer = CALayer()
sublayer.borderWidth = 1
sublayer.frame = label.boundingRectForCharacterRange(NSRange(text.range(of: "bb")!, in: text))
label.layer.addSublayer(sublayer)
Swift 3/4
extension UILabel {
func boundingRect(forCharacterRange range: NSRange) -> CGRect? {
guard let attributedText = attributedText else { return nil }
let textStorage = NSTextStorage(attributedString: attributedText)
let layoutManager = NSLayoutManager()
textStorage.addLayoutManager(layoutManager)
let textContainer = NSTextContainer(size: bounds.size)
textContainer.lineFragmentPadding = 0.0
layoutManager.addTextContainer(textContainer)
var glyphRange = NSRange()
// Convert the range for glyphs.
layoutManager.characterRange(forGlyphRange: range, actualGlyphRange: &glyphRange)
return layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
}
}
Przykładowe Użycie (Swift 3/4)
let label = UILabel()
let text = "aa bb cc"
label.attributedText = NSAttributedString(string: text)
let sublayer = CALayer()
sublayer.borderWidth = 1
sublayer.frame = label.boundingRect(forCharacterRange: NSRange(text.range(of: "bb")!, in: text))
label.layer.addSublayer(sublayer)
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-11-28 14:50:58
Moją propozycją byłoby użycie zestawu tekstowego. Niestety nie mamy dostępu do menedżera układu, którego używa UILabel
, jednak może być możliwe utworzenie jego repliki i użycie jej do uzyskania rect dla zakresu.
Moją sugestią byłoby utworzenie NSTextStorage
obiektu zawierającego dokładnie ten sam przypisany tekst, jaki znajduje się w Twojej etykiecie. Następnie utwórz NSLayoutManager
i dodaj ją do obiektu magazynowania tekstu. Na koniec utwórz NSTextContainer
o tym samym rozmiarze co etykieta i dodaj ją do układu manager.
Teraz magazyn tekstu ma ten sam tekst, co etykieta, a kontener tekstowy ma ten sam rozmiar, co etykieta, więc powinniśmy być w stanie zapytać menedżera układu, który stworzyliśmy, o rect dla naszego zakresu za pomocą boundingRectForGlyphRange:inTextContainer:
. Upewnij się, że najpierw przekonwertujesz zakres znaków na zakres glifów za pomocą glyphRangeForCharacterRange:actualCharacterRange:
w obiekcie layout manager.
Wszystko idzie dobrze, co powinno dać ci CGRect
zakresu określonego w etykiecie.
nie testowałem tego ale to byłoby moje podejście i naśladując działanie UILabel
powinno mieć duże szanse na sukces.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-10-27 16:42:41
Swift 4 rozwiązanie, będzie działać nawet dla ciągów wielowierszowych, bounds zostały zastąpione przez intrinsicContentSize
extension UILabel {
func boundingRectForCharacterRange(range: NSRange) -> CGRect? {
guard let attributedText = attributedText else { return nil }
let textStorage = NSTextStorage(attributedString: attributedText)
let layoutManager = NSLayoutManager()
textStorage.addLayoutManager(layoutManager)
let textContainer = NSTextContainer(size: intrinsicContentSize)
textContainer.lineFragmentPadding = 0.0
layoutManager.addTextContainer(textContainer)
var glyphRange = NSRange()
layoutManager.characterRange(forGlyphRange: range, actualGlyphRange: &glyphRange)
return layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
}
}
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-12-03 16:12:27
Czy możesz zamiast tego oprzeć swoją klasę na UITextView? Jeśli tak, sprawdź metody protokołu UiTextInput. Zobacz w szczególności geometrię i metody odpoczynku hit.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-10-17 04:31:37
Właściwa odpowiedź jest taka, że rzeczywiście nie można. większość rozwiązań tutaj próbuje odtworzyć UILabel
wewnętrzne zachowanie z różną precyzją. Tak się składa, że w ostatnich wersjach iOS UILabel
renderuje tekst za pomocą frameworków TextKit/CoreText. We wcześniejszych wersjach faktycznie używał Webkita pod maską. Kto wie, co wykorzysta w przyszłości. Odtwarzanie za pomocą TextKit ma oczywiste problemy nawet teraz (nie mówiąc o przyszłościowych rozwiązaniach). Nie do końca wiemy (lub nie możemy zagwarantować) jak układ tekstu i renderowanie są faktycznie wykonywane, tzn. jakie parametry są używane – wystarczy spojrzeć na wszystkie wspomniane przypadki krawędzi. Niektóre rozwiązanie może "przypadkowo" działać dla niektórych prostych przypadkach, które przetestowałeś – to nie gwarantuje niczego nawet w bieżącej wersji iOS. Renderowanie tekstu jest trudne, nie każ mi zaczynać od obsługi RTL, dynamicznego typu, Emotikon itp.
Jeśli potrzebujesz odpowiedniego roztworu po prostu nie używaj UILabel
. Jest to prosty komponent, a fakt, że jego TextKit wewnętrzne nie są ujawnione w API jest wyraźną wskazówką, że Apple nie chce, aby deweloperzy budowali rozwiązania opierając się na tych szczegółach implementacji.
Alternatywnie możesz użyć UITextView. Wygodnie eksponuje wewnętrzne elementy TextKit i jest bardzo konfigurowalny, dzięki czemu można osiągnąć prawie wszystko UILabel
jest dobre dla.
Możesz również przejść na niższy poziom i używać TextKit bezpośrednio(lub nawet niżej z CoreText). Jest szansa, że jeśli naprawdę potrzebujesz znaleźć pozycje tekstowe, wkrótce możesz potrzebować innych potężnych narzędzia dostępne w tych ramach. Na początku są raczej trudne w użyciu, ale czuję, że większość złożoności pochodzi z uczenia się, jak działa układ tekstu i renderowanie – i to jest bardzo istotna i przydatna umiejętność. Po zapoznaniu się z doskonałymi strukturami tekstowymi firmy Apple poczujesz się upoważniony do zrobienia znacznie więcej z tekstem.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-01-07 15:56:07
Dla każdego, kto szuka plain text
rozszerzenia!
extension UILabel {
func boundingRectForCharacterRange(range: NSRange) -> CGRect? {
guard let text = text else { return nil }
let textStorage = NSTextStorage.init(string: text)
let layoutManager = NSLayoutManager()
textStorage.addLayoutManager(layoutManager)
let textContainer = NSTextContainer(size: bounds.size)
textContainer.lineFragmentPadding = 0.0
layoutManager.addTextContainer(textContainer)
var glyphRange = NSRange()
// Convert the range for glyphs.
layoutManager.characterRange(forGlyphRange: range, actualGlyphRange: &glyphRange)
return layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
}
}
P. S. zaktualizowana ODPOWIEDŹ Noodle of Death ' s odpowiedź .
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-03-12 11:35:12
Inny sposób, jeśli masz włączoną automatyczną regulację rozmiaru czcionki, będzie taki:
let stringLength: Int = countElements(self.attributedText!.string)
let substring = (self.attributedText!.string as NSString).substringWithRange(substringRange)
//First, confirm that the range is within the size of the attributed label
if (substringRange.location + substringRange.length > stringLength)
{
return CGRectZero
}
//Second, get the rect of the label as a whole.
let textRect: CGRect = self.textRectForBounds(self.bounds, limitedToNumberOfLines: self.numberOfLines)
let path: CGMutablePathRef = CGPathCreateMutable()
CGPathAddRect(path, nil, textRect)
let framesetter = CTFramesetterCreateWithAttributedString(self.attributedText)
let tempFrame: CTFrameRef = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, stringLength), path, nil)
if (CFArrayGetCount(CTFrameGetLines(tempFrame)) == 0)
{
return CGRectZero
}
let lines: CFArrayRef = CTFrameGetLines(tempFrame)
let numberOfLines: Int = self.numberOfLines > 0 ? min(self.numberOfLines, CFArrayGetCount(lines)) : CFArrayGetCount(lines)
if (numberOfLines == 0)
{
return CGRectZero
}
var returnRect: CGRect = CGRectZero
let nsLinesArray: NSArray = CTFrameGetLines(tempFrame) // Use NSArray to bridge to Array
let ctLinesArray = nsLinesArray as Array
var lineOriginsArray = [CGPoint](count:ctLinesArray.count, repeatedValue: CGPointZero)
CTFrameGetLineOrigins(tempFrame, CFRangeMake(0, numberOfLines), &lineOriginsArray)
for (var lineIndex: CFIndex = 0; lineIndex < numberOfLines; lineIndex++)
{
let lineOrigin: CGPoint = lineOriginsArray[lineIndex]
let line: CTLineRef = unsafeBitCast(CFArrayGetValueAtIndex(lines, lineIndex), CTLineRef.self) //CFArrayGetValueAtIndex(lines, lineIndex)
let lineRange: CFRange = CTLineGetStringRange(line)
if ((lineRange.location <= substringRange.location) && (lineRange.location + lineRange.length >= substringRange.location + substringRange.length))
{
var charIndex: CFIndex = substringRange.location - lineRange.location; // That's the relative location of the line
var secondary: CGFloat = 0.0
let xOffset: CGFloat = CTLineGetOffsetForStringIndex(line, charIndex, &secondary);
// Get bounding information of line
var ascent: CGFloat = 0.0
var descent: CGFloat = 0.0
var leading: CGFloat = 0.0
let width: Double = CTLineGetTypographicBounds(line, &ascent, &descent, &leading)
let yMin: CGFloat = floor(lineOrigin.y - descent);
let yMax: CGFloat = ceil(lineOrigin.y + ascent);
let yOffset: CGFloat = ((yMax - yMin) * CGFloat(lineIndex))
returnRect = (substring as NSString).boundingRect(with: CGSize(width: Double.greatestFiniteMagnitude, height: Double.greatestFiniteMagnitude), options: .usesLineFragmentOrigin, attributes: [.font: self.font ?? UIFont.systemFont(ofSize: 1)], context: nil)
returnRect.origin.x = xOffset + self.frame.origin.x
returnRect.origin.y = yOffset + self.frame.origin.y + ((self.frame.size.height - textRect.size.height) / 2)
break
}
}
return returnRect
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-03-26 11:37:12