Comment fonctionne YYText

Home PDF

L’effet d’ombre ci-dessus est réalisé avec le code suivant :

On peut voir que YYTextShadow est d’abord généré, puis assigné à yy_textShadow de attributedString. Ensuite, attributedString est assigné à YYLabel, et enfin, YYLabel est ajouté à UIView pour être affiché. En suivant yy_textShadow, on découvre que cela consiste principalement à lier textShadow à l’attribut de NSAttributedString, avec la clé YYTextShadowAttributeName et la valeur textShadow. Cela signifie que le shadow est d’abord stocké, puis utilisé ultérieurement. Utilisez Shift + Command + J pour accéder rapidement à la définition :

Il y a ici une fonction addAttribute, qui est définie dans le fichier NSAttributedString.h :

- (void)addAttribute:(NSString *)name value:(id)value range:(NSRange)range;

Cela signifie que vous pouvez lui assigner n’importe quelle paire clé-valeur. La définition de YYTextShadowAttributeName est une chaîne de caractères ordinaire, ce qui signifie que les informations de l’ombre sont d’abord stockées, puis utilisées ultérieurement. Faisons une recherche globale de YYTextShadowAttributeName.

Ensuite, nous arrivons à la fonction YYTextDrawShadow dans YYTextLayout :

CGContextTranslateCTM est utilisé pour modifier les coordonnées de l’origine dans un contexte, donc…

CGContextTranslateCTM(context, point.x, point.y);

Cela signifie qu’il faut déplacer le contexte de dessin vers le point point. Nous devons d’abord comprendre où YYTextDrawShadow est appelé, et nous constatons qu’il est appelé dans drawInContext.

Dans drawInContext, dessinez successivement la bordure du bloc, puis la bordure de fond, l’ombre, le soulignement, le texte, les accessoires, l’ombre intérieure, la ligne de suppression, la bordure du texte et les lignes de débogage.

Alors, où est-ce que drawInContext est utilisé exactement ? On peut voir qu’il y a un paramètre YYTextDebugOption, donc cette fonction n’est certainement pas un rappel système, mais plutôt une fonction appelée par YYText lui-même.

Maintenez la touche Ctrl + 1 enfoncée pour afficher les raccourcis clavier, et vous constaterez qu’il y a quatre endroits où ils sont utilisés.

drawInContext:size:debug reste un appel propre à YYText, car le type de debug est YYTextDebugOption *, qui est spécifique à YY. newAsyncTask ne ressemble pas à un appel système, et addAttachmentToView:layer: est similaire, donc il est fort probable que ce soit drawRect:.

En effet, en regardant l’aide rapide à droite, il y a une explication détaillée, et en dessous de l’aide, il est également indiqué que cela est défini dans UIView. Ensuite, en examinant YYTextContainerView, on voit qu’il hérite de UIView.

Donc YYLabel utilise YYTextContainerView, c’est ça ? Et ensuite, il laisse le système appeler drawRect: dans YYTextContainerView pour dessiner ?

Étrange, YYLabel hérite de UIView. Donc, dans YYText, il devrait y avoir deux ensembles de choses ! Un ensemble YYLabel, et un ensemble YYTextView, comme UILabel et UITextView. Ensuite, revenons à la méthode newAsyncDisplayTask de YYLabel que nous avons vue précédemment,

Très long, au milieu, il appelle drawInContext dans YYTextLayout. newAsyncDisplayTask, où est-il appelé à son tour ?

Sur la deuxième ligne, il est appelé. On peut donc simplement comprendre que YYLabel utilise un processus asynchrone pour dessiner le texte. Et _displayAsync est appelé par la méthode display ci-dessus. En regardant la documentation de display, il est dit que le système l’appellera au moment approprié pour mettre à jour le contenu du layer, et il ne faut pas l’appeler directement. Nous pouvons également y ajouter un point d’arrêt.

L’explication est que display est appelé dans une transaction de CALayer. Pourquoi utiliser une transaction ? Probablement pour mettre à jour en masse, ce qui serait plus efficace, non ? Cela ne ressemble pas à un besoin de rollback comme dans une base de données.

La documentation système de display mentionne également que si vous souhaitez que votre layer soit dessiné différemment, vous pouvez surcharger cette méthode pour implémenter votre propre dessin.

Ainsi, nous avons une idée simple. YYLabel utilise la méthode display de UIView pour dessiner de manière asynchrone ses effets tels que les ombres, etc. Les effets d’ombre sont d’abord stockés dans les attributs de attributedText de YYLabel, puis récupérés lors du dessin dans la méthode display. Pour le dessin, le framework CoreGraphics du système est utilisé.

Après avoir clarifié certaines idées, on se rend compte de ce qui est vraiment puissant : d’un côté, c’est la capacité à organiser autant d’effets, d’appels asynchrones, etc., et de l’autre, c’est la maîtrise approfondie du framework CoreGraphics sous-jacent. Ainsi, après avoir acquis une certaine compréhension de l’organisation du code précédent, nous allons maintenant plonger plus profondément dans le framework CoreGraphics. Voyons comment le dessin est effectué.

Revenons à YYTextDrawShadow.

Ici, CGContextSaveGState et CGContextRestoreGState encadrent un bloc de code de dessin. CGContextSaveGState signifie que l’état actuel du dessin est copié et placé dans la pile de dessin. Chaque contexte de dessin maintient une pile de dessin. Je ne suis pas sûr de la manière exacte dont la pile est gérée. Pour l’instant, comprenons simplement qu’il faut appeler CGContextSaveGState avant de dessiner dans le contexte, et CGContextRestoreGState après, afin que le dessin intermédiaire apparaisse correctement dans le contexte. CGContextTranslateCTM déplace le contexte à une position spécifique. Il se déplace d’abord à point.x et point.y, les coordonnées de dessin, puis à 0 et size.height, ce qui n’est pas clair pour le moment, nous verrons cela plus tard. Ensuite, les lines sont récupérées et une boucle for est exécutée.

lines est un tableau qui contient les lignes de texte générées lors de la création d’une mise en page de texte (YYTextLayout). Dans la méthode (YYTextLayout *)layoutWithContainer:(YYTextContainer *)container text:(NSAttributedString *)text range:(NSRange)range, ce tableau est rempli avec les lignes de texte qui sont calculées en fonction du conteneur (container), du texte (text) et de la plage spécifiée (range).

Chaque élément du tableau lines représente une ligne de texte dans la mise en page, et ces lignes sont utilisées pour afficher le texte correctement formaté à l’écran.

Ensuite, naviguez jusqu’à la définition de cette fonction :

Cette fonction est très longue, de la ligne 367 à la ligne 861, soit 500 lignes de code ! En regardant le début et la fin, on peut voir que son utilité est d’obtenir ces variables. Comment lines est-il obtenu ?

On peut voir que dans une grande boucle for, chaque line est ajoutée une par une dans lines. Mais comment est obtenu le lineCount ?

À la ligne 472, un objet framesetter est créé, avec le paramètre text étant un NSAttributedString. Ensuite, un CTFrameRef est créé à partir de l’objet frameSetter, puis les lines sont obtenues à partir du CTFrameRef. Mais qu’est-ce exactement qu’une line ? Mettons un point d’arrêt pour l’examiner.

On a découvert que pour le mot shadow, la valeur de lineCount = 2 ne correspond pas au nombre de lettres comme on pourrait s’y attendre.

Donc, on peut supposer que le Shadow blanc est en fait une seule line, et l’ombre est également une seule line ?

Dans YYText, il y a plusieurs exemples, mais un seul effet est affiché, les autres codes étant commentés. J’ai remarqué quelque chose d’étrange : pour Shadow, lineCount = 2, et pour Multiple Shadows, lineCount est également égal à 2. Cependant, Multiple Shadows a également une ombre intérieure, donc cela devrait être 3, non ?

En consultant la documentation Apple pour CTLine, il est indiqué que CTLine représente une ligne de texte, et un objet CTLine contient un ensemble de glyph runs. Donc, il s’agit simplement du nombre de lignes ! En regardant la capture d’écran du point d’arrêt ci-dessus, la raison pour laquelle shadow avait une valeur de 2 est que son texte était shadow\n\n. Comme vous pouvez le voir, \n\n a été ajouté intentionnellement pour des raisons esthétiques :

Donc, shadow\n\n représente deux lignes de texte. CTLine correspond à ce que nous appelons habituellement une ligne. Revenons maintenant à notre lineCount :

Ici, nous obtenons un tableau de CTLines, puis nous comptons le nombre d’éléments dans ce tableau. Si lineCount est supérieur à 0, nous obtenons l’origine des coordonnées pour chaque ligne. Maintenant que nous avons lineCount, passons à la boucle for.

Depuis le tableau ctLines, on obtient un CTLine, puis on crée un objet YYTextLine qui est ensuite ajouté au tableau lines. Ensuite, on effectue quelques calculs de frame pour la line. Le constructeur de YYTextLine est assez simple : il sauvegarde d’abord la position, si le texte est en mode vertical, et l’objet CTLine :

- (instancetype)initWithCTLine:(CTLineRef)ctLine position:(CGPoint)position vertical:(BOOL)vertical {
    self = [super init];
    if (self) {
        _ctLine = CFRetain(ctLine);
        _position = position;
        _vertical = vertical;
    }
    return self;
}

Une fois que vous avez bien compris lines, revenons à la fonction YYTextDrawShadow précédente :

Le code est maintenant plus simple. D’abord, on récupère le nombre de lignes, on les parcourt, puis on obtient le tableau GlyphRuns, qu’on parcourt également. Un GlyphRun peut être considéré comme un élément graphique ou une unité de dessin. Ensuite, on extrait le tableau attributes, on utilise notre YYTextShadowAttributeName précédemment défini pour récupérer l’ombre (shadow) que nous avons attribuée au départ, et enfin on commence à dessiner l’ombre :

Une boucle while qui dessine continuellement des ombres portées. Appelle CGContextSetShadowWithColor pour définir le déplacement, le rayon et la couleur de l’ombre. Ensuite, appelle YYTextDrawRun pour effectuer le dessin réel. YYTextDrawRun est appelé à trois endroits :

Utilisé pour dessiner des ombres intérieures, des ombres de texte ainsi que le texte lui-même. Cela indique qu’il s’agit d’une méthode générique utilisée pour dessiner l’objet Run.

On commence par obtenir la matrice de transformation du texte, puis on utilise runTextMatrixIsID pour vérifier si elle reste inchangée. Si ce n’est pas une mise en page verticale ou si aucune transformation graphique n’est définie, on passe directement au dessin. On appelle CTRunDraw pour dessiner l’objet run. Ensuite, en mettant un point d’arrêt, on constate que lors du dessin de l’ombre initiale, le programme n’entre que dans le bloc if et ne passe pas dans le bloc else.

Ainsi, notre dessin d’ombres est terminé !

En résumé, YYLabel stocke d’abord les effets tels que les ombres dans les attributs de attributedText, puis redéfinit la méthode display de UIView. Dans display, il effectue un rendu asynchrone en utilisant le framework CoreText pour obtenir des objets CTLine et CTRun. Ensuite, il récupère les attributs à partir de CTRun et, en fonction des propriétés contenues dans ces attributs, utilise le framework CoreGraphics pour dessiner l’objet CTRun dans le contexte.

La compréhension n’est pas encore suffisante, je reviendrai lire cela plus tard. Je ne peux m’empêcher de m’exclamer que YY est vraiment trop fort ! Aujourd’hui, j’ai organisé mes idées, me permettant d’écrire tout en lisant le code, pour ne pas que cela devienne ennuyeux, et en même temps pour offrir une référence à tout le monde. Il est temps d’aller dormir.


Back 2025.01.18 Donate