Centralize a imagem NSTextAttachment ao lado do UILabel de linha única

117

Gostaria de anexar uma NSTextAttachmentimagem à minha string atribuída e centralizá-la verticalmente.

Usei o seguinte código para criar minha string:

NSMutableAttributedString *str = [[NSMutableAttributedString alloc] initWithString:DDLocalizedString(@"title.upcomingHotspots") attributes:attrs];
NSTextAttachment *attachment = [[NSTextAttachment alloc] init];
attachment.image = [[UIImage imageNamed:@"help.png"] imageScaledToFitSize:CGSizeMake(14.f, 14.f)];
cell.textLabel.attributedText = [str copy];

No entanto, a imagem parece estar alinhada com a parte superior da célula textLabel.

captura de tela do problema de deslocamento de anexo de texto

Como posso alterar o retângulo no qual o anexo é desenhado?

Sean Danzeiser
fonte
Eu tenho uma classe de categoria para ter NSString com UIImage e vice-versa. github.com/Pradeepkn/TextWithImage Aproveite.
PradeepKN

Respostas:

59

Você pode alterar o retângulo subclassificando NSTextAttachmente substituindo attachmentBoundsForTextContainer:proposedLineFragment:glyphPosition:characterIndex:. Exemplo:

- (CGRect)attachmentBoundsForTextContainer:(NSTextContainer *)textContainer proposedLineFragment:(CGRect)lineFrag glyphPosition:(CGPoint)position characterIndex:(NSUInteger)charIndex {
    CGRect bounds;
    bounds.origin = CGPointMake(0, -5);
    bounds.size = self.image.size;
    return bounds;
}

Não é uma solução perfeita. Você tem que descobrir a origem Y “a olho” e se você alterar a fonte ou o tamanho do ícone, você provavelmente desejará alterar a origem Y. Mas não consegui encontrar uma maneira melhor, exceto colocando o ícone em uma exibição de imagem separada (que tem suas próprias desvantagens).

rob mayoff
fonte
2
Não tenho ideia de por que votaram contra isso, isso me ajudou muito +1
Jasper
A origem Y é o descendente da fonte. Veja minha resposta abaixo.
phatmann
4
A resposta de Travis é uma solução mais limpa sem subclasses.
SwampThingTom
Para obter mais detalhes, consulte @ mg-han 's resposta stackoverflow.com/a/45161058/280503 (Na minha opinião, deve ser a resposta selecionada para a pergunta.)
gardenofwine
191

Você pode usar o capHeight da fonte.

Objective-C

NSTextAttachment *icon = [[NSTextAttachment alloc] init];
UIImage *iconImage = [UIImage imageNamed:@"icon.png"];
[icon setBounds:CGRectMake(0, roundf(titleFont.capHeight - iconImage.size.height)/2.f, iconImage.size.width, iconImage.size.height)];
[icon setImage:iconImage];
NSAttributedString *iconString = [NSAttributedString attributedStringWithAttachment:icon];
[titleText appendAttributedString:iconString];

Rápido

let iconImage = UIImage(named: "icon.png")!
var icon = NSTextAttachment()
icon.bounds = CGRect(x: 0, y: (titleFont.capHeight - iconImage.size.height).rounded() / 2, width: iconImage.size.width, height: iconImage.size.height)
icon.image = iconImage
let iconString = NSAttributedString(attachment: icon)
titleText.append(iconString)

A imagem do anexo é renderizada na linha de base do texto. E o eixo y dele é invertido como o sistema de coordenadas gráfico principal. Se você quiser mover a imagem para cima, defina bounds.origin.ycomo positivo.

A imagem deve ser alinhada verticalmente no centro com a capHeight do texto. Portanto, precisamos definir o bounds.origin.ypara (capHeight - imageHeight)/2.

Para evitar algum efeito irregular na imagem, devemos arredondar a parte fração de y. Mas as fontes e as imagens geralmente são pequenas, mesmo 1px de diferença faz a imagem parecer desalinhada. Então, apliquei a função round antes de dividir. Isso torna a parte da fração do valor y .0 ou .5

No seu caso, a altura da imagem é maior do que capHeight da fonte. Mas você pode usar da mesma maneira. O valor do deslocamento y será negativo. E será definido abaixo da linha de base.

insira a descrição da imagem aqui

MG Han
fonte
OBRIGADO!!!!!!!!!!!!!!
Alex
107

Experimente - [NSTextAttachment bounds]. Nenhuma subclasse necessária.

Para o contexto, estou renderizando um UILabelpara uso como imagem de anexo e, em seguida, definindo os limites da seguinte forma: attachment.bounds = CGRectMake(0, self.font.descender, attachment.image.size.width, attachment.image.size.height)e linhas de base do texto dentro da imagem do rótulo e do texto na linha de string atribuída conforme desejado.

Travis
fonte
Isso funciona desde que você não precise dimensionar a imagem.
phatmann
12
Para Swift 3.0:attachment.bounds = CGRect(x: 0.0, y: self.font.descender, width: attachment.image!.size.width, height: attachment.image!.size.height)
Andy
Incrível, obrigado! Não sabia sobre a descenderpropriedade da UIFont!
Ben
61

Eu encontrei uma solução perfeita para isso, funciona como um encanto para mim, no entanto, você tem que experimentar (provavelmente a constante depende da resolução do dispositivo e talvez o que quer que seja;)

func textAttachment(fontSize: CGFloat) -> NSTextAttachment {
    let font = UIFont.systemFontOfSize(fontSize) //set accordingly to your font, you might pass it in the function
    let textAttachment = NSTextAttachment()
    let image = //some image
    textAttachment.image = image
    let mid = font.descender + font.capHeight
    textAttachment.bounds = CGRectIntegral(CGRect(x: 0, y: font.descender - image.size.height / 2 + mid + 2, width: image.size.width, height: image.size.height))
    return textAttachment
}

Deve funcionar e não deve ficar borrado de nenhuma forma (graças a CGRectIntegral)

Borchero
fonte
Obrigado por postar isso, me levou a uma abordagem muito boa. Percebi que você está adicionando um 2 um tanto mágico ao cálculo da coordenada y.
Ben
2
Aqui está o que eu usei para meu cálculo y: descender + (abs (descender) + capHeight) / 2 - iconHeight / 2
Ben
Por que o +2 para a origem Y?
William LeGate
@WilliamLeGate Eu realmente não sei, apenas experimentei e funcionou para todos os tamanhos de fonte que testei (os que eu precisava) ..
borchero
Caramba ... Essa resposta é incrível.
GGirotto
38

A respeito:

CGFloat offsetY = -10.0;

NSTextAttachment *attachment = [NSTextAttachment new];
attachment.image = image;
attachment.bounds = CGRectMake(0.0, 
                               offsetY, 
                               attachment.image.size.width, 
                               attachment.image.size.height);

Nenhuma subclasse necessária

Jakub Truhlář
fonte
2
Funciona melhor do que usar self.font.descender (que tem um valor padrão de ~ 4 no simulador do iPhone 4s executando iOS 8). -10 parece uma aproximação melhor para o estilo / tamanho da fonte padrão.
Kedar Paranjape
10

@Travis está correto ao afirmar que o deslocamento é o descendente da fonte. Se você também precisar dimensionar a imagem, você precisará usar uma subclasse de NSTextAttachment. Abaixo está o código, que foi inspirado neste artigo . Eu também postei como uma essência .

import UIKit

class ImageAttachment: NSTextAttachment {
    var verticalOffset: CGFloat = 0.0

    // To vertically center the image, pass in the font descender as the vertical offset.
    // We cannot get this info from the text container since it is sometimes nil when `attachmentBoundsForTextContainer`
    // is called.

    convenience init(_ image: UIImage, verticalOffset: CGFloat = 0.0) {
        self.init()
        self.image = image
        self.verticalOffset = verticalOffset
    }

    override func attachmentBoundsForTextContainer(textContainer: NSTextContainer, proposedLineFragment lineFrag: CGRect, glyphPosition position: CGPoint, characterIndex charIndex: Int) -> CGRect {
        let height = lineFrag.size.height
        var scale: CGFloat = 1.0;
        let imageSize = image!.size

        if (height < imageSize.height) {
            scale = height / imageSize.height
        }

        return CGRect(x: 0, y: verticalOffset, width: imageSize.width * scale, height: imageSize.height * scale)
    }
}

Use da seguinte maneira:

var text = NSMutableAttributedString(string: "My Text")
let image = UIImage(named: "my-image")!
let imageAttachment = ImageAttachment(image, verticalOffset: myLabel.font.descender)
text.appendAttributedString(NSAttributedString(attachment: imageAttachment))
myLabel.attributedText = text
Phatmann
fonte
10

Se você tem um ascendente muito grande e deseja centralizar a imagem (centro da altura da tampa) como eu, tente isto

let attachment: NSTextAttachment = NSTextAttachment()
attachment.image = image
if let image = attachment.image{
    let y = -(font.ascender-font.capHeight/2-image.size.height/2)
    attachment.bounds = CGRect(x: 0, y: y, width: image.size.width, height: image.size.height).integral
}

O cálculo de y é como a imagem abaixo

insira a descrição da imagem aqui

Observe que o valor de y é 0 porque queremos que a imagem se desloque para baixo da origem

Se você quiser que ele fique no meio de todo o rótulo. Use este valor y:

let y = -((font.ascender-font.descender)/2-image.size.height/2)
IndyZa
fonte
7

Podemos fazer uma extensão no swift 4 que gere um anexo com uma imagem centralizada como esta:

extension NSTextAttachment {
    static func getCenteredImageAttachment(with imageName: String, and 
    font: UIFont?) -> NSTextAttachment? {
        let imageAttachment = NSTextAttachment()
    guard let image = UIImage(named: imageName),
        let font = font else { return nil }

    imageAttachment.bounds = CGRect(x: 0, y: (font.capHeight - image.size.height).rounded() / 2, width: image.size.width, height: image.size.height)
    imageAttachment.image = image
    return imageAttachment
    }
}

Então você pode fazer a ligação enviando o nome da imagem e da fonte:

let imageAttachment = NSTextAttachment.getCenteredImageAttachment(with: imageName,
                                                                   and: youLabel?.font)

E, em seguida, anexe o imageAttachment ao attributeString

Oscar
fonte
1

No meu caso, chamar sizeToFit () ajudou. In swift 5.1

Dentro do seu rótulo personalizado:

func updateUI(text: String?) {
    guard let text = text else {
        attributedText = nil
        return
    }

    let attributedString = NSMutableAttributedString(string:"")

    let textAttachment = NSTextAttachment ()
    textAttachment.image = image

    let sizeSide: CGFloat = 8
    let iconsSize = CGRect(x: CGFloat(0),
                           y: (font.capHeight - sizeSide) / 2,
                           width: sizeSide,
                           height: sizeSide)
    textAttachment.bounds = iconsSize

    attributedString.append(NSAttributedString(attachment: textAttachment))
    attributedString.append(NSMutableAttributedString(string: text))
    attributedText = attributedString

    sizeToFit()
}
Reimond Hill
fonte
0

Use -lineFrag.size.height / 5.0 para a altura dos limites. Centraliza exatamente a imagem e alinha com o texto para todos os tamanhos de fontes

override func attachmentBoundsForTextContainer(textContainer: NSTextContainer, proposedLineFragment lineFrag: CGRect, glyphPosition position: CGPoint, characterIndex charIndex: Int) -> CGRect
{
    var bounds:CGRect = CGRectZero

    bounds.size = self.image?.size as CGSize!
    bounds.origin = CGPointMake(0, -lineFrag.size.height/5.0);

    return bounds;
}
Harish Kumar Kailas
fonte
-lineFrag.size.height/5.0não está correto. Em vez disso, é o descensor da fonte.
phatmann