كيف يعمل YYText

Home PDF

تم تحقيق تأثير الظل أعلاه باستخدام الكود التالي:

يمكننا أن نرى أنه تم إنشاء 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! اليوم قمت بترتيب أفكاري، وجعلت نفسي أكتب وأقرأ الكود في نفس الوقت، حتى لا يكون الأمر مملًا، وفي نفس الوقت أقدم مرجعًا للآخرين. حان الوقت للذهاب إلى النوم.


Back 2025.01.18 Donate