YYText はどのように動作するのか

Home PDF

上の影の効果は、以下のコードで実現されています:

先生が YYTextShadow を生成し、それを attributedStringyy_textShadow に代入し、その後 attributedStringYYLabel に代入し、さらに YYLabelUIView に追加して表示していることがわかります。yy_textShadow を追跡すると、主に textShadowNSAttributedString の attribute にバインドされており、キーは YYTextShadowAttributeName、値は textShadow であることがわかります。つまり、最初に shadow を保存し、後で使用するという流れです。Shift + Command + J を使って定義に素早くジャンプできます。

ここに addAttribute というものがあります。これは NSAttributedString.h で定義されています:

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

このメソッドは、指定された属性を指定された範囲のテキストに追加します。nameは属性の名前を表し、valueはその属性の値です。rangeは、属性を適用するテキストの範囲を指定します。

説明によると、任意のキーと値のペアを代入することができます。そして、YYTextShadowAttributeName の定義は普通の文字列であり、これは最初に shadow の情報を保存し、後で使用することを意味します。YYTextShadowAttributeName をグローバルに検索してみましょう。

次に、YYTextLayout 内の YYTextDrawShadow 関数を見ていきましょう:

CGContextTranslateCTM は、コンテキスト内の原点座標を変更することを意味します。したがって、

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

このコードは、Core Graphicsのコンテキスト(context)に対して、指定された点(point.xpoint.y)に基づいて座標系を平行移動(translate)します。具体的には、描画コンテキストの原点がpoint.xpoint.yだけ移動されます。これにより、その後の描画操作は新しい原点を基準として行われます。

描画のコンテキストを point 点に移動することを意味します。まず、どこで YYTextDrawShadow が呼び出されているのかを確認し、それが drawInContext 内で呼び出されていることを発見しました。

drawInContext 内では、順番にブロックの枠線を描画し、その後、背景の枠線、影、下線、テキスト、装飾、内側の影、取り消し線、テキストの枠線、デバッグ用の線を描画します。

では、実際にどこで drawInContext が使われているのでしょうか?その中に YYTextDebugOption というパラメータがあることがわかります。したがって、この関数はシステムのコールバックではなく、YYText 内部で独自に呼び出されていることが確実です。

Ctrl + 1 を押してショートカットを表示すると、4つの場所で呼び出されていることがわかります。

drawInContext:size:debug は依然として YYText 自身の呼び出しです。なぜなら、debug の型は YYTextDebugOption * であり、これは YY 自身のものだからです。newAsyncTask はシステムの呼び出しには見えず、addAttachmentToView:layer: も同様です。したがって、これらはおそらく drawRect: の呼び出しである可能性が高いです。

確かに、右側のクイックヘルプを見ると、詳細な説明があり、ヘルプの下には UIView で定義されていると書かれています。さらに YYTextContainerView を見ると、これは UIView を継承しています。

YYLabelYYTextContainerView を使っているのですか?そしてシステムに YYTextContainerView 内の drawRect: を呼び出させて描画させているのですか?

奇妙ですね、YYLabelUIView を継承しています。つまり、YYText には2つのセットが存在するはずです!1つは YYLabel、もう1つは YYTextView で、UILabelUITextView のように。それでは、先ほどの YYLabelnewAsyncDisplayTask をもう一度見てみましょう。

長いコードの中間で YYTextLayoutdrawInContext が呼び出されています。newAsyncDisplayTask はどこで呼び出されているのでしょうか?

2行目で呼び出されました。したがって、簡単に理解すると、YYLabelはテキストを描画するために非同期を使用していると言えます。そして、_displayAsyncは上記のdisplayによって呼び出されています。displayのドキュメントを見ると、システムが適切なタイミングで呼び出してレイヤーの内容を更新すると書かれています。直接呼び出すべきではありません。また、ブレークポイントを設定することもできます。

displayCALayer のトランザクション中に呼び出されることを説明します。なぜトランザクションを使うのかというと、おそらく更新をバッチ処理して効率を上げるためでしょう。データベースのようなロールバックの必要性はなさそうです。

display のシステムドキュメントには、もしあなたのレイヤーを異なる方法で描画したい場合、このメソッドをオーバーライドして独自の描画を実装できると書かれています。

したがって、簡単にいくつかのアイデアを得ました。YYLabelUIViewdisplay メソッドをオーバーライドして、自身の影などの効果を非同期に描画します。影の効果はまず YYLabelattributedText の属性に保存され、display メソッドで描画する際に取り出されます。描画時にはシステムの CoreGraphics フレームワークが使用されます。

いくつかの考えを整理した後、本当に強力なものは何かがわかります。一方では、これだけの効果や非同期呼び出しなどを組織化する能力であり、もう一方では、基盤となるCoreGraphicsフレームワークの熟練した運用です。したがって、前のコードの組織化について少し理解した後、CoreGraphicsフレームワークに深く入り込んでいきます。どのように描画されているのかを見てみましょう。

YYTextDrawShadow に戻りましょう。

ここでは、CGContextSaveGStateCGContextRestoreGState が描画コードの一部を囲んでいます。CGContextSaveGState の意味は、現在の描画状態をコピーして、描画スタックにプッシュすることです。各描画コンテキストは、描画スタックを維持しています。スタックの内部操作については詳しくはわかりませんが、とりあえず、描画コンテキストの前に CGContextSaveGState を呼び出し、描画コンテキストの後に CGContextRestoreGState を呼び出すことで、その間の描画がコンテキストに有効に表示されると理解しておきます。CGContextTranslateCTM は、コンテキストを対応する位置に移動します。まず point.xpoint.y に移動し、描画の対応位置に移動します。その後、0 と size.height に移動する理由はまだわからないので、後でまた確認します。次に lines を取り出し、for ループを実行します。

lines とは何ですか?YYTextLayout 内の (YYTextLayout *)layoutWithContainer:(YYTextContainer *)container text:(NSAttributedString *)text range:(NSRange)range で値が設定されているのを見つけました。

次に、この関数の定義部分に移動します:

この関数は非常に長く、367行から861行まで、500行ものコードがあります!最初と最後を見ると、その目的はこれらの変数を取得することだとわかります。linesはどのように取得されるのでしょうか?

大きな for ループの中で、1行ずつ linelines に追加しているのが見られます。では、lineCount はどのように得られるのでしょうか?

472行目では、framesetterオブジェクトが作成され、textパラメータはNSAttributedStringです。その後、frameSetterオブジェクト内にCTFrameRefが作成され、CTFrameRefからlinesが取得されます。lineとは一体何でしょうか?ここにブレークポイントを設定して確認してみましょう。

発見しましたが、shadow という単語の lineCount = 2 は、私たちが想像していた文字数ではありませんでした。

なので、白い Shadow 全体が一つの line で、影も一つの line だと推測していますか?

YYText にはいくつかの例がありますが、そのうちの1つの効果だけを表示し、他のコードをコメントアウトしています。奇妙なことに、Shadow の lineCount = 2 で、Multiple Shadows の lineCount も 2 です。しかし、Multiple Shadows には内側の影もあるので、3 つになるはずですよね?

CTLine の Apple ドキュメントを調べると、CTLine は一行のテキストを表し、CTLine オブジェクトは一連の glyph runs を含んでいると書かれています。つまり、単純に行数のことです!上のブレークポイントのスクリーンショットを見ると、先ほど shadow が 2 だったのは、そのテキストが shadow\n\n だったからです。先ほど、\n\n は意図的に追加され、見た目を良くするためでした:

したがって、shadow\n\n は2行のテキストです。CTLine は、私たちが普段「行」と呼んでいるものです。次に、lineCount を見てみましょう:

ここで CTLines 配列を取得し、その中の要素数を取得します。そして、lineCount が 0 より大きい場合、各行の座標原点を取得します。さて、lineCount が得られたので、次に for ループを見ていきましょう。

ctLines 配列から CTLine を取得し、次に YYTextLine オブジェクトを取得して、それを lines 配列に追加します。その後、line のフレーム計算を行います。YYTextLine のコンストラクタはシンプルで、位置、垂直レイアウトかどうか、CTLine オブジェクトを保存します:

lines を理解した後、以前の YYTextDrawShadow に戻りましょう:

これでコードがシンプルになりました。まず行数を取得し、それを走査します。次に GlyphRuns 配列を取得し、それを走査します。GlyphRun は図形や描画単位と理解できます。その後、attributes 配列を取得し、以前の YYTextShadowAttributeName を使用して、最初に割り当てた shadow を取得します。そして、影の描画を開始します:

while ループを使用して、サブシャドウを繰り返し描画します。CGContextSetShadowWithColor を呼び出して、シャドウのオフセット、半径、色を設定します。その後、YYTextDrawRun を呼び出して実際に描画を行います。YYTextDrawRun は3つの場所で呼び出されます:

内側の影やテキストの影、テキスト自体を描画するために使用されます。これは、Run オブジェクトを描画するための汎用的なメソッドであることを示しています。

最初にテキストの変換行列を取得し、runTextMatrixIsIDを使用してそれが変更されていないかどうかを確認します。もし垂直レイアウトでないか、または図形変換が設定されていない場合は、直接描画を開始します。CTRunDrawを呼び出してrunオブジェクトを描画します。その後、ブレークポイントを設定して、最初の影を描画する際にif文の中に入るが、else文には入らないことを確認します。

以上で、私たちの影の描画は終了です!

まとめると、YYLabel はまず、影などの効果を attributedText の attributes に保存し、UIViewdisplay メソッドをオーバーライドします。display メソッド内で非同期描画を行い、CoreText フレームワークを使用して CTLineCTRun オブジェクトを取得します。CTRun から attributes を取得し、その後、attributes 内の各プロパティに基づいて、CoreGraphics フレームワークを使用して CTRun オブジェクトを Context に描画します。

理解がまだ十分でないので、後でまた読み返してみようと思います。YYの強さに改めて感嘆せざるを得ません!今日は考えを整理し、コードを書きながら読み進めることで、単調さを避けつつ、皆さんの参考にもなるようにしました。そろそろ寝る時間です。


Back 2025.01.18 Donate