السلام عليكم ورحمة الله وبركاته
قمنا في مقال سابق بدعم عرض النصوص باللغة العربية في المحرك Unity، ولكننا نسينا أمرًا مهمًا، وهو دعم الكتابة على حقل الإدخال باللغة العربية وعرض النص بالشكل الصحيح.
وهذا ما سنقوم به في هذا المقال، باستخدام نفس الطريقة التي طبقناها سابقًا، سنحصل على هذه النتيجة في النهاية:
مستلزمات
ما العائق؟
لنتذكر الحل الذي قمنا بتطبيقه في المقال السابق، كنا نأخذ النص الموجود في العنصر Text، نقوم بإصلاحه (ربط الكلمات وترتيب الجمل والأسطر) ثم نعيد تعيين النص للعنصر.
ولكن أثناء الكتابة، فإن النص سيتغير باستمرار، لأن المستخدم هو من يقدم النص هذه المرة، وهو سيتوقع أن تظهر الأحرف والكلمات التي يكتبها بطريقة صحيحة بمجرد كتابتها.
لذا إذا استخدمنا طريقة المقال السابق، فلن نستطيع إصلاح النص في كل مرة يقوم المستخدم بتغييره لأن إضافة الـArabicSupport لا تدعم إصلاح نص يحتوي على أجزاء صحيحة وأجزاء خاطئة، كهذا:
قد تقوم بإبقاء النص بشكله الأصلي ريثما ينتهي المستخدم من كتابته، ولكن كيف ستحدد انتهاء المستخدم من الكتابة؟ هل إذا توقف لمدة معينة أم إذا ضغط على مكان آخر؟..
وفرضًا أنك قمت حقًا بفعل ذلك (تلميح: الحدث OnEndEdit) ، ستكون تجربة سيئة للمستخدم حيث أنه لن يرى النص بشكله الصحيح مباشرة.
الحل: استخدام عنصر Text خفي
تقوم هذه الخدعة على أساس إضافة عنصر Text بالإضافة للـInputField، ويحتوي على نفس خصائص الحقل مثل الأبعاد وحجم الخط وما إلى ذلك.
نقوم بوضعه بحيث يخفي الحقل InputField، ولكننا نبقي للمستخدم القدرة على تعديله.
وبعد أن يقوم بالتعديل، نأخذ النص ونقوم بإصلاحه ونعينه للـText الجديد الخفي. ونعيد هذه العملية بشكل مستمر كل ما يتم تغيير النص.
صفحة AN Games Studio Club شاركتنا بملف برمجي يقوم بهذه العملية، لذلك نشكرها على تقديمه بشكل مفتوح للجميع، هنا تجد الملف كاملًا:
using UnityEngine; using System.Collections; using UnityEngine.UI; using ArabicSupport; public class InputArabicFix : MonoBehaviour { // Use this for initialization InputField textfield; Text fakeDisplay; public bool arabic = true; void Start () { textfield = GetComponent<InputField>(); textfield.textComponent.color = Color.clear; fakeDisplay = (Text)GameObject.Instantiate(textfield.textComponent,textfield.textComponent.transform.localPosition,textfield.textComponent.transform.localRotation); fakeDisplay.transform.SetParent(textfield.transform,false); fakeDisplay.color = Color.black; textfield.onValueChanged.AddListener(delegate{FixNewText();}); } public string text{ get{ return textfield.text; } } // Update is called once per frame void Update () { } void FixNewText(){ if (arabic) { fakeDisplay.text = ArabicFixer.Fix (textfield.text); } else { fakeDisplay.text = textfield.text; }}}
قم بإضافة هذا الملف إلى عنصر Input Field (تأكد من وجود الإضافة Arabic Support في المشروع، هنا تجد تفاصيل لتنصيبها)
أقترح بعض التعديلات على آلية تطبيق هذا الحل:
سأبدأ بشرح تطبيق أول اقتراحين، حيث سنقوم بربط هذه العملية بالملف البرمجي SetArabicFixedText (احصل عليه من المقال السابق) :
... [RequireComponent(typeof(Text))] public class SetArabicFixedText : MonoBehaviour { ... [SerializeField] InputField textFromInputField; private bool getFromInputField; void Start() { UIText = GetComponent<Text>(); rectTransform = GetComponent<RectTransform>(); getFromInputField = textFromInputField != null; /* في حال كنا نستلم النص من حقل إدخال */ if(getFromInputField) { /* نعين النص المطلوب بأنه النص الموجود من البداية في الحقل */ neededText = textFromInputField.text; /* نقوم بإصلاحه*/ UIText.text = UIText.FixArabicUITextLines(useTashkeel, useHinduNumbers, neededText); /* كل ما تم التعديل على نص الحقل */ textFromInputField.onValueChanged.AddListener( delegate (string newText) { /* نقوم بالتحديث بناءً على النص الجديد */ UpdateText(newText); } ); /*لا نريد التعديل على النص بشكل مستمر بما أننا نستلمه من الحقل ونقوم بالتعديل على أساسه */ updateOnRealtime = false; } else if (UIText.text != string.Empty) // إذا لم يكن النص فارغًا { ... } ... } ... }
بعد ذلك، نقوم بإعداد العناصر بهذه الطريقة:
الآن بما أن الحقل أصبح مرتبطًا بملفنا البرمجي، يمكننا التعديل عليه من الملف كيفما نشاء.
إظهار الـمؤشر Caret
قمنا في الخطوة السابقة بإزالة لون الـText الخاص بحقل الإدخال، مما بدوره أزال لون الـCaret وجعلها تختفي.
لنعيدها كما كانت، في دالة Start للملف SetFixedArabicText، نضيف السطرين الآتيين:
void Start() { ... getFromInputField = textFromInputField != null; if(getFromInputField) { neededText = textFromInputField.text; UIText.text = UIText.FixArabicUITextLines(useTashkeel, useHinduNumbers, neededText); updateOnRealtime = false; /* تفعيل القدرة على تغيير لونها */ textFromInputField.customCaretColor = true; /* تعيين اللون بأنه لون النص المعروض - يمكنك تغييره إلى ما تشاء هنا باستخدام Color */ textFromInputField.caretColor = UIText.color; ... } }
تحديد لغة النص
ليس الهدف اللغة نفسها ، وإنما ما إذا كانت من اللغات التي تكتب من اليمين إلى اليسار، وفي حالتنا، الإضافة ArabicSupport تدعم العربية والفارسية، لذلك سنقوم بالتأكد من هاتين اللغتين فقط.
في الملف البرمجي CustomTextFunctions نضيف الدوال (أو يمكن إضافتهم في أي Static Class):
/* تحديد ما إذا كان النص يحتوي على أي حرف يكتب من اليمين إلى اليسار */ public static bool ContainsRTLCharacters(this string text) { //لكل حرف في النص for (int s = 0; s < text.Length; s++) { //نتأكد منه (الدالة في الأسفل) if (text[s].IsRTLCharacter()) return true; } return false; } /* تحديد ما إذا كان الحرف يكتب من اليمين إلى اليسار */ public static bool IsRTLCharacter(this char character) { /* تذكر أن هذه الرموز هي المسؤولة عن تحديد الحروف فمثلا من 0x600 إلى 0x6ff هي الأحرف العربية(والفارسية أيضًا) الأساسية لذلك نتاكد ما إذا كان الحرف في هذا المجال. وكذلك المجالات الأخرى للحركات والأحرف الخاصة*/ if (character >= 0x600 && character <= 0x6ff) return true; if (character >= 0x750 && character <= 0x77f) return true; if (character >= 0xfb50 && character <= 0xfc3f) return true; if (character >= 0xfe70 && character <= 0xfefc) return true; return false; }
تغيير الموضع بناءً على اتجاه النص
إذا كان النص يكتب من اليمين إلى اليسار، فسنقوم بتغيير موضعه إلى اليمين، وإلا نبقيه في اليسار.
في الملف البرمجي SetArabicFixedText:
private bool isRTL; ... void Start() { ... if(getFromInputField) { .... textFromInputField.onValueChanged.AddListener( delegate (string newText) { isRTL = newText.ContainsRTLCharacters(); /* إذا كنت تريد تغيير الاتجاه فقط إذا كان أول حرف في النص يبدأ بحرف يكتب من اليمين إلى اليسار قم باستخدام هذا الشرط بدلاً من isRTL: newText[0].IsRTLCharacter() */ if (newText != string.Empty && isRTL) { UIText.alignment = TextAnchor.MiddleRight; } else UIText.alignment = TextAnchor.MiddleLeft; } } } } ...
تحديد مجال عرض النص
العنصر Text الأساسي الخاص بحقل الإدخال يقوم باقتصاص النص وتغييره على حسب المساحة المتوفرة..
ولكن في الحدث OnValueChanged، فإن النص الذي نستلمه يأتي كاملاً، أي لا يتغير على حسب النص المعروض والمساحة المتوفرة.
لدينا طريقتان:
الأولى أن نستخدم خاصية Horizontal Overflow في العنصر Fake Text ونعيّنها ل Wrap، هكذا:
ولكنني واجهت بعض المشاكل مع هذه الطريقة، منها أنني لم أعد أستطع الانتقال من اليسار إلى اليمين، أو العكس، أثناء الكتابة (في حال ما كتبته لم يسع في المساحة المحددة)
هذه المشاكل موضحة في الصورة المتحركة:
فالنتيجة لم تعجبني حقًا، لذلك قمت ببرمجة الخاصية، مما أعطاني نتيجة أفضل (كما عرضت أول المقال)
والخيار يعود إليكم لتحددوا الأفضل بالنسبة لكم.
الطريقة الثانية: ما فعلته هو أنه بعد تحديث النص، أي في الإطار الذي يلي التحديث..
نأخذ النص الذي تم تعيينه للـInputField، لنقوم بإصلاحه مرة أخرى ونعيّنه للـText الخفي الخاص بنا:
... private bool waitingNextFrameToUpdateText; ... void Start(){ if(getFromInputField) { .... textFromInputField.onValueChanged.AddListener( delegate (string newText) { isRTL = newText.ContainsRTLCharacters(); if (newText != string.Empty && isRTL( { UIText.alignment = TextAnchor.MiddleRight; } else UIText.alignment = TextAnchor.MiddleLeft; } waitingNextFrameToUpdateText= true; } } ... void Update(){ if (waitingNextFrameToUpdateText) { if (isRTL) UpdateText(textFromInputField.textComponent.text); else UIText.text = textFromInputField.textComponent.text; waitingNextFrameToUpdateText = false; } ..... }
المضحك هو أنه مع هذه الطريقة أيضًا، ما زال ليس بإمكاننا الانتقال يمينًا أو يسارًا في النص، سوف نحل هذه المشكلة الآن..
مشكلة الانتقال يمينًا ويسارًا بين النص
أحيانًا إذا كان طول النص أكبر من المساحة المتوفرة له، فإن المستخدم من المفترض أن يستطيع التنقل من اليمين إلى اليسار – أو العكس – لكي يرى النص كاملاً بالنهاية.
لكن بعد أن قمنا بتحديد مجال النص، فإن المستخدم لم يعد يستطيع الانتقال إلى الخلف – أو الأمام – بعد زيادة طول النص..
وذلك بسبب أننا نقوم بإعادة تحديث عنصر الـText الخفي الخاص بنا فقط عند تغيير النص الأصلي، ولكن النص لا يتغير بمجرد الانتقال يمينًا أو يسارًا.
لذلك ما سنفعله هو تحديث النص عند تغيير موضع الـCaret عن آخر موضع قمنا بمشاهدته.
private int lastCaretPosition; void Start(){ ... textFromInputField.onValueChanged.AddListener( delegate (string newText) { /* نحفظ موقع المؤشر عند تغيير النص */ lastCaretPosition = textFromInputField.caretPosition; } ... } void Update(){ ... /* إذا تغير موقع المؤشر من آخر مرة قمنا بحفظه */ if (getFromInputField && textFromInputField.caretPosition != lastCaretPosition) { if (isRTL) { UpdateText(textFromInputField.textComponent.text); } else UIText.text = textFromInputField.textComponent.text; } } ... }
ماذا عن TextMeshPro؟
تطبيق هذه الحلول على العنصر TextMesh Pro Input Field يكون بشكل مشابه جدًا..
فقط نقوم بتغيير الأنواع من Input Field و Text إلى TMP_InputField و TMProUGUI.
وهو الخيار المحبذ، لكنني استخدمت الـInputField الأصلية في هذا المقال للتوضيح والشرح ولكي أنوّع قليلاً.
ختامًا، أتمنى أن يكون المقال قد حاز إعجابكم وأفاد من كان يواجه هذه المشكلة.
لا تنسَو مشاركة المقال مع أصدقائكم..اتركوا آرائكم أو أسئلتكم عن المقال بتعليقٍ خفيف في الأسفل 🙂