vertical top texto left horizontalmente div centrar boton align ios objective-c iphone uilabel typography

ios - texto - top align label swift



¿Cómo UILabel centra verticalmente su texto? (3)

Ha habido mucha confusión sobre lo que realmente estoy tratando de lograr. Así que por favor déjame volver a redactar mi pregunta. Es tan simple como:

¿Cómo se centra Apple en el texto dentro de UILabel ''s -drawTextInRect: :?

Aquí hay un poco de contexto sobre por qué estoy buscando el enfoque de Apple para centrar el texto:

  • No estoy contento con cómo centran el texto dentro de este método
  • Tengo mi propio algoritmo que hace el trabajo como quiero.

Sin embargo, hay algunos lugares en los que no puedo inyectar este algoritmo, ni siquiera con el método swizzling. Conocer el algoritmo exacto que usan (o su equivalente) resolvería mi problema personal.

Aquí hay un proyecto de demostración con el que puedes experimentar. En caso de que esté interesado en cuál es mi problema con la implementación de Apple, mire el lado izquierdo (base UILabel): el texto salta hacia arriba y hacia abajo a medida que aumenta el tamaño de la fuente utilizando el control deslizante, en lugar de moverse gradualmente hacia abajo.

Una vez más, todo lo que estoy buscando es una implementación de -drawTextInRect: que hace lo mismo que UILabel base, pero sin llamar a super .

- (void)drawTextInRect:(CGRect)rect { CGRect const centeredRect = ...; [self.attributedText drawInRect:centeredRect]; }


Después de dos años y una semana en WWDC, finalmente me di cuenta de qué diablos está pasando.

En cuanto a por qué UILabel comporta como lo hace: parece que Apple intenta centrar lo que se encuentra entre el ascendente y el descendente . Sin embargo, el motor de dibujo de texto real siempre ajusta la línea de base a los límites de píxeles. Si uno no presta atención a esto cuando calcula el rect para dibujar el texto, la línea de base puede saltar hacia arriba y hacia abajo, aparentemente de manera arbitraria.

El principal problema es encontrar dónde Apple coloca la línea de base. AutoLayout ofrece un FirstBaselineAnchor, pero su valor desafortunadamente no puede leerse desde el código. El siguiente fragmento de código parece predecir correctamente la línea de base en todas las scale pantalla y contentScaleFactor s siempre que la altura de la etiqueta esté redondeada a píxeles.

extension UILabel { public var predictedBaselineOffset: CGFloat { let scale = self.window?.screen.scale ?? UIScreen.main.scale let availablePixels = round(self.bounds.height * scale) let preferredPixels = ceil(self.font.lineHeight * scale) let ascenderPixels = round(self.font.ascender * scale) let offsetPixels = ceil((availablePixels - preferredPixels) / 2) return (ascenderPixels + offsetPixels) / scale } }

Cuando la altura de la etiqueta no se redondea a píxeles, la línea de base predicha es un píxel desactivado con mayor frecuencia, por ejemplo, para una fuente de 13.0pt en un contenedor de 40.1pt en un dispositivo 2x o una fuente de 16.0pt en un contenedor de 40.1pt en un dispositivo 3x.

Tenga en cuenta que tenemos que trabajar a nivel de píxeles aquí. Trabajar con puntos (es decir, dividir por la escala de la pantalla inmediatamente en lugar de al final) produce errores off-by-one en las pantallas @ 3x debido a imprecisiones de punto flotante.

Por ejemplo, centrar un contenido de 47 px (15.667 pts) en un contenedor de 121 px (40.333 pts) nos da un desplazamiento de 12.333 pts que se acumula a 12.667 pts debido a errores de punto flotante.

Para responder a la pregunta que inicialmente hice, podemos implementar -drawTextInRect: usando la línea de base predicha (con suerte correctamente) para obtener los mismos resultados que hace UILabel .

public class AppleImitatingLabel: UILabel { public override func drawText(in rect: CGRect) { let offset = self.predictedBaselineOffset let frame = rect.offsetBy(dx: 0, dy: offset) self.attributedText!.draw(with: frame, options: [], context: nil) } }

Ver el proyecto en GitHub para una implementación de trabajo.


No tengo una respuesta, pero hice una pequeña investigación y pensé en compartirla. Utilicé Xtrace , que te permite rastrear las llamadas del método Objective-C, para intentar averiguar qué está haciendo UILabel. Xtrace.h Xtrace.mm Xtrace.mm del repositorio de Xtrace git al proyecto de demostración UILabel de Christian. En RootViewController.m , agregué #import "Xtrace.h" y luego experimenté con varios filtros de rastreo. Añadiendo lo siguiente a loadView de loadView :

[Xtrace includeMethods:@"drawWithRect|drawInRect|drawTextInRect"]; [Xtrace traceClassPattern:@"String|UILabel|AppleLabel" excluding:nil];

Me da esta salida:

| [<AppleLabel 0x7fedf141b520> drawTextInRect:{{0, 0}, {187.5, 333.5}}] | [<NSConcreteMutableAttributedString 0x7fedf160ec30>/NSAttributedString drawInRect:{{0, 156.5}, {187.5, 333.5}}] | [<UILabel 0x7fedf1418dc0> drawTextInRect:{{0, 0}, {187.5, 333.5}}] | [<UILabel 0x7fedf1418dc0> _drawTextInRect:{{0, 0}, {187.5, 333.5}} baselineCalculationOnly:0] | [<__NSCFConstantString 0x1051d1db0>/NSString drawWithRect:{{0, 156.3134765625}, {187.5, 20.287109375}} options:1L attributes:<__NSDictionaryM 0x7fedf141ae00> context:<NSStringDrawingContext 0x7fedf15503c0>]

(Estoy ejecutando esto con Xcode 7.0 beta, simulador de iOS 9.0).

Podemos ver que la implementación de Apple no envía drawInRect: a una NSAttributedString, sino drawWithRect:options:attributes: a una NSString. Supongo que estos terminarán haciendo lo mismo.

También podemos ver exactamente cuál es el desplazamiento Y calculado. En esta posición inicial del deslizador, es 156.3134765625. Es interesante notar que no es algo que caiga en algún valor entero redondeado, o un múltiplo de 0.25 o similar, que de otra manera sería una explicación para el salto.

También vemos que UILabel envía 20.287109375 como la altura, este es el mismo valor que se calcula por self.attributedText.size.height . Parece que está usando eso, aunque no he podido rastrear esa llamada (¿tal vez está usando Core Text directamente?).

Así que ahí es donde estoy atrapado. Intenté ver la diferencia entre lo que calcula UILabel y la fórmula obvia "labelYOffset = (limitsHeight - labelHeight) / 2.0", pero no encuentro un patrón consistente.

Actualización 1. Para que podamos modelar este esquivo desplazamiento de Y como

labelYOffset = (limitsHeight - labelHeight) / 2.0 + error

Lo que es el error , es lo que estamos tratando de averiguar. Tratemos de estudiar esto como un científico. Here está la salida de Xtrace cuando arrastré la diapositiva al final y luego al principio. También agregué un NSLog en cada cambio del control deslizante que ayuda a separar las entradas. Escribí una secuencia de comandos de Python para trazar el error en la altura de la etiqueta.

Interesante. Vemos que hay cierta linealidad en los errores, pero extrañamente saltan entre líneas en ciertos puntos, si entiendes lo que quiero decir. ¿Que esta pasando aqui? Estaré perdiendo el sueño hasta que esto esté resuelto. :)


Traté esto mucho en una aplicación que hice.

Creo que el problema probablemente no sea tanto con UILabel como la fuente, incluyendo el ascendente y los descendientes. Mi suposición es que UILabel toma estos valores en consideración cuando coloca el cuerpo del texto que tiene un impacto en la posición vertical aparente. El problema con una gran cantidad de fuentes es que estos valores están totalmente arruinados en el archivo TTF / OTF, por lo que obtiene, por ejemplo, recorte del descensor con .sizeToFit (), líneas superpuestas, posición vertical torcida, etc.

Hay tres maneras de lidiar con esto:

1) Use una NSAttributedString y más la línea de base con NSBaselineOffsetAttributeName. TTTAttributedLabel es un buen atajo.

2) Use CoreText y CoreGraphics para representar su propio diseño de texto en un CGLayer dentro de una subclase UIView personalizada y crear efectivamente su propio equivalente de UILabel. ¡Esto es duro!

3) Arregle el archivo de fuente para que la línea de base, el ascendente y el descendente sean correctos