كيف يعمل YYText
تم تحقيق تأثير الظل أعلاه باستخدام الكود التالي:
يمكننا أن نرى أنه تم إنشاء YYTextShadow
أولاً، ثم تم تعيينه لخاصية yy_textShadow
في attributedString
، وبعد ذلك تم تعيين attributedString
إلى YYLabel
، ثم تم إضافة YYLabel
إلى UIView
لعرضه. عند تتبع yy_textShadow
، نجد أنه يتم بشكل أساسي ربط textShadow
بسمة NSAttributedString
باستخدام المفتاح YYTextShadowAttributeName
والقيمة textShadow
، أي أنه يتم تخزين الظل أولاً ثم استخدامه لاحقًا. يمكنك الانتقال بسرعة إلى مكان التعريف باستخدام Shift + Command + J:
هناك دالة تُسمى addAttribute
، وهي مُعرَّفة في ملف NSAttributedString.h
:
- (void)addAttribute:(NSString *)name value:(id)value range:(NSRange)range;
ترجمة:
- (void)addAttribute:(NSString *)name value:(id)value range:(NSRange)range;
ملاحظة: الكود الموجود في الكتلة البرمجية هو كود Objective-C، ولا يتم ترجمته.
التفسير هو أنه يمكن تعيين أي أزواج من المفاتيح والقيم له. بينما تعريف YYTextShadowAttributeName
هو عبارة عن سلسلة نصية عادية، مما يعني أنه يتم أولاً تخزين معلومات الظل، ثم يتم استخدامها لاحقًا. دعونا نبحث بشكل شامل عن YYTextShadowAttributeName
.
ثم نصل إلى الدالة YYTextDrawShadow
داخل YYTextLayout
:
CGContextTranslateCTM
تعني تغيير إحداثيات نقطة الأصل في Context، لذا
CGContextTranslateCTM(context, point.x, point.y);
المقصود هو نقل سياق الرسم إلى النقطة point
. دعونا أولاً نحدد مكان استدعاء YYTextDrawShadow
، ونجد أنه يتم استدعاؤه داخل drawInContext
.
في drawInContext
، يتم رسم حدود المربع أولاً، ثم يتم رسم حدود الخلفية، الظل، الخط السفلي، النص، الملحقات، الظل الداخلي، الخط المشطوب، حدود النص، وخطوط التصحيح بالترتيب.
إذن، أين تم استخدام drawInContext
بالضبط؟ يمكننا أن نرى أن هناك معلمة تسمى YYTextDebugOption
، لذا فإن هذه الوظيفة بالتأكيد ليست رد فعل من النظام، بل هي وظيفة يتم استدعاؤها داخل YYText نفسها.
اضغط على Ctrl + 1 لإظهار قائمة الاختصارات، ستلاحظ وجود أربعة أماكن تم استدعاؤها فيها.
drawInContext:size:debug
لا يزال استدعاءً خاصًا بـ YYText، لأن نوع debug
هو YYTextDebugOption *
، وهو خاص بـ YY. newAsyncTask
لا يبدو كاستدعاء نظامي، وكذلك addAttachmentToView:layer:
، لذا من المرجح أن يكون drawRect:
.
بالفعل، بالنظر إلى المساعدة السريعة على اليمين، ستجد شرحًا تفصيليًا، كما أن الجزء السفلي من المساعدة يوضح أنه تم تعريفه في UIView
. وبالنظر إلى YYTextContainerView
، نجد أنه يرث من UIView
.
إذن، YYLabel
يستخدم YYTextContainerView
؟ ثم يقوم النظام باستدعاء drawRect:
داخل YYTextContainerView
للرسم؟
غريب، YYLabel
يمكن أن يرث من UIView
. لذا، يجب أن يكون هناك مجموعتان من الأشياء في YYText! مجموعة YYLabel
، ومجموعة YYTextView
، تمامًا مثل UILabel
و UITextView
. ثم نعود وننظر إلى newAsyncDisplayTask
السابق في YYLabel
،
طويل، في منتصف الطريق يتم استدعاء drawInContext
من داخل YYTextLayout
. newAsyncDisplayTask
، أين يتم استدعاؤه؟
تم استدعاؤه في السطر الثاني. لذا يمكن فهمه ببساطة على أن YYLabel
يستخدم الرسم غير المتزامن للنص. بينما يتم استدعاء _displayAsync
من خلال display
أعلاه. بالنظر إلى وثائق display
، يُقال أن النظام سيستدعيه في الوقت المناسب لتحديث محتوى الطبقة، ولا يجب عليك استدعاؤه مباشرة. يمكننا أيضًا وضع نقطة توقف له.
يبدو أن display
يتم استدعاؤه ضمن معاملة (transaction
) في CALayer
. السبب في استخدام المعاملة ربما يكون لتحقيق تحديثات مجمعة، مما يعزز الكفاءة. لا يبدو أن هناك حاجة إلى التراجع كما في قواعد البيانات.
توضح وثائق النظام الخاصة بـ display
أيضًا أنه إذا كنت ترغب في أن يكون رسم الطبقة (layer) مختلفًا، فيمكنك إعادة كتابة هذه الطريقة لتنفيذ الرسم الخاص بك.
لذلك، لدينا فكرة بسيطة. YYLabel
يعيد كتابة طريقة display
الخاصة بـ UIView
لرسم ظلاله وتأثيراته الأخرى بشكل غير متزامن، يتم حفظ تأثيرات الظل أولاً في السمات (attributes) داخل attributedText
الخاص بـ YYLabel
، ثم يتم استرجاعها عند الرسم في طريقة display
، ويتم استخدام إطار عمل CoreGraphics الخاص بالنظام للرسم.
بعد توضيح بعض الأفكار، سنجد أن القوة الحقيقية تكمن في أمرين: الأول هو تنظيم العديد من التأثيرات والاستدعاءات غير المتزامنة، والثاني هو الإتقان الجيد لإطار العمل الأساسي CoreGraphics. لذا، بعد فهم تنظيم الكود السابق، سنتعمق الآن في إطار عمل CoreGraphics. لنرى كيف يتم الرسم عليه.
لنعد مرة أخرى إلى YYTextDrawShadow
.
هنا، CGContextSaveGState
و CGContextRestoreGState
يحيطان بجزء من الكود الخاص بالرسم. معنى CGContextSaveGState
هو نسخ حالة الرسم الحالية وحفظها في مكدس الرسم. كل Context للرسم يحتفظ بمكدس رسم خاص به. أنا لست متأكدًا تمامًا من كيفية عمل المكدس داخليًا. ولكن يمكننا أن نفهم بشكل مؤقت أنه يجب استدعاء CGContextSaveGState
قبل البدء في الرسم على الـ Context، واستدعاء CGContextRestoreGState
بعد الانتهاء من الرسم، وذلك لضمان أن الرسم الذي يتم في المنتصف يظهر بشكل صحيح في الـ Context.
CGContextTranslateCTM
يستخدم لنقل الـ Context إلى موقع معين. أولاً، يتم نقله إلى الإحداثيات point.x
و point.y
، وهي المواقع المناسبة للرسم. أما بالنسبة للنقل إلى 0 و size.height
، فأنا لست متأكدًا من الغرض منه، وسأحتاج إلى التحقق لاحقًا. بعد ذلك، يتم استخراج lines
وتنفيذ حلقة for
.
lines
是什么؟ تم العثور عليه في YYTextLayout
حيث يتم تعيينه في الدالة (YYTextLayout *)layoutWithContainer:(YYTextContainer *)container text:(NSAttributedString *)text range:(NSRange)range
.
lines
هو عبارة عن مصفوفة تحتوي على أسطر النص التي تم إنشاؤها بواسطة YYTextLayout
. يتم استخدام هذه المصفوفة لتخزين المعلومات حول كل سطر من النص، مثل موقعه وحجمه ومحتواه. يتم تعبئة هذه المصفوفة أثناء عملية إنشاء التخطيط (layout
) للنص داخل الحاوية (container
) المحددة.
عندما تقوم باستدعاء الدالة layoutWithContainer:text:range:
، يتم تحليل النص (text
) وتقسيمه إلى أسطر بناءً على الحاوية المحددة (container
) والنطاق (range
) المحدد. يتم بعد ذلك تخزين هذه الأسطر في المصفوفة lines
، والتي يمكن استخدامها لاحقًا لرسم النص أو إجراء عمليات أخرى متعلقة بالتخطيط.
ثم انتقل إلى تعريف هذه الدالة:
هذه الدالة طويلة جدًا، من السطر 367 إلى السطر 861، أي حوالي 500 سطر من الكود! بعد النظر إلى البداية والنهاية، يمكن ملاحظة أن الغرض منها هو الحصول على هذه المتغيرات. كيف يتم الحصول على lines
؟
يمكننا أن نرى في حلقة for
الكبيرة أنه يتم إضافة كل سطر line
واحدًا تلو الآخر إلى lines
. إذًا، كيف يتم الحصول على lineCount
؟
في السطر 472، تم إنشاء كائن framesetter
، حيث كانت المعلمة text
من نوع NSAttributedString
. بعد ذلك، تم إنشاء CTFrameRef
داخل كائن frameSetter
، ثم تم الحصول على lines
من CTFrameRef
. ما هو بالضبط line
؟ دعونا نضع نقطة توقف له.
تم اكتشاف أن lineCount = 2
لكلمة shadow
، وليس عدد الأحرف كما كنا نتوقع.
لذا يُعتقد أن Shadow
الأبيض بأكمله هو line
واحد، وظله أيضًا line
واحد؟
في YYText، هناك عدة أمثلة، ولكن يتم عرض تأثير واحد فقط، بينما يتم تعليق الأكواد الأخرى. لاحظت شيئًا غريبًا: في حالة Shadow، lineCount = 2
، وفي حالة Multiple Shadows، lineCount
أيضًا يساوي 2. لكن في Multiple Shadows، هناك أيضًا ظل داخلي، أليس من المفترض أن يكون هناك 3 خطوط؟
بالذهاب إلى وثائق Apple الخاصة بـ CTLine
، نجد أن CTLine
يمثل سطرًا من النص، حيث يحتوي كائن CTLine
على مجموعة من glyph runs
. إذن، الأمر ببساطة يتعلق بعدد الأسطر! بالنظر إلى لقطة الشاشة أعلاه، نرى أن shadow
كانت تساوي 2 لأن النص كان shadow\n\n
. لاحظ أن \n\n
تمت إضافتها عمدًا لأغراض تجميلية.
لذا فإن shadow\n\n
عبارة عن سطرين من النص. 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
في ثلاثة أماكن:
تُستخدم لرسم الظلال الداخلية وظلال النصوص بالإضافة إلى النص نفسه. يشير هذا إلى أنها طريقة عامة تُستخدم لرسم الكائن Run
.
في البداية، يتم الحصول على مصفوفة التحويل للنص باستخدام runTextMatrixIsID
للتحقق مما إذا كانت المصفوفة هي مصفوفة الهوية (أي أنها لا تغير النص)، وإذا لم يكن النص مرتبطًا بشكل عمودي أو لم يتم تعيين تحويل الرسوم، يتم البدء في الرسم مباشرة. يتم استدعاء CTRunDraw
لرسم كائن run
. بعد ذلك، عند وضع نقطة توقف، تم اكتشاف أنه عند رسم الظل الأول، تم الدخول فقط إلى if
ولم يتم الدخول إلى else
.
لذلك، نكون قد انتهينا من رسم الظلال!
باختصار، يقوم YYLabel
بحفظ تأثيرات مثل الظل في السمات (attributes) داخل attributedText
، ثم يعيد كتابة طريقة display
الخاصة بـ UIView
. في طريقة display
، يتم الرسم بشكل غير متزامن باستخدام إطار عمل CoreText
للحصول على كائنات CTLine
و CTRun
. يتم استخراج السمات من CTRun
، ثم بناءً على هذه السمات، يتم رسم كائن CTRun
في السياق (Context) باستخدام إطار عمل CoreGraphics
.
الفهم لا يزال غير كافٍ، سأعود لاحقًا لأقرأه مرة أخرى. لا أستطيع إلا أن أتعجب من مدى قوة YY! اليوم قمت بترتيب أفكاري، وجعلت نفسي أكتب وأقرأ الكود في نفس الوقت، حتى لا يكون الأمر مملًا، وفي نفس الوقت أقدم مرجعًا للآخرين. حان الوقت للذهاب إلى النوم.