کارکتر اتصال مجازی یا (Zero-Width Joiner) یک کارکتر کنترلی در یونیکد هستش و در برخی از خطهای پیچیده مثل فارسی، عربی و هندی کاربرد داره. اضافه کردن این کارکتر به حروف متصل باعث میشود، حرف به شکل کوچکش نمایش داده شود.
کد این کارکتر در یونیکد (U+200D) است و در جاهای دیگر به این شکل میشه ازش استفاد کرد:
Unicode Character 'ZERO WIDTH JOINER': (U+200D) UTF-8 (hex): e2808d UTF-8 (binary): 11100010:10000000:10001101 UTF-16 (hex): 0x200D UTF-16 (decimal): 8205 C/C++/Java/\u200D" Python3: u"\u200D"
الان دیگه تقریبا در اکثر برنامهها قابلیت جستجو دیده میشه، شما هم اگر قبلا در برنامه هایتان قابلیت جستجو را پیادهسازی کرده باشید احتمالا متوجه همچین مشکلی شدهاید که امکان توپر کردن "بخشی از یک کلمه" وجود ندارد.
برای مثال به اسکرین شات بالا توجه کنید کلمه "برای" به شکل "برای" نمایش داده شده. به همین دلیل هستش که هیچکدام از سایتها و برنامههای فارسی از این قابلیت در نمایش پیشنهادهای جستجو استفاده نمیکنند. حتی در گوگل هم این امکان را در جستجوی عبارت های فارسی و عربی غیرفعال شده است.
قبل از این که سراغ راه حل بریم، ابتدا دلیل به وجود آمدن این مشکل را برسی میکنم.
مثال ب<b>رای تشبیه</b> خروجی: مثال برای تشبیه
همان طور که قطعه کد بالا مشاهده میکنید برای تغییر استایل بخشی از متن عبارتی به قبل و بعد آن اضافه میشود این عبارتها در نهایت از دید کاربر پنهان میماند اما باعث میشوند کارکترهای متصل، جدا از هم نمایش داده شوند. اضافه کردن کارکتر اتصال مجازی (ZWJ) در جاهای مناسب میتونه این مشکل به راحتی حل کند.
در ادامه پیادهسازی این راهحل را با زبان برنامهنویسی کاتلین در اندروید رو برسی میکنیم.
تابع Char.isConnectable() امکان اتصال کارکتر مورد نظر به کارکتر بعدی را مشخص میکند.
نکته: این تابع فقط از کارکترهای قابل اتصال خط فارسی و عربی استاندارد پشتیبانی میکند در صورت نیاز به پشتیبانی خطهایی مثل کوردی میتوانید کارکترهای قابل اتصال آن خطها را به تابع اضافه کنید.
private fun Char.isConnectable(): Boolean { return "يبپتثجچحخسشصضطظعغفقکگلمنهیكيئ".indexOf(this) != -1 }
تابع getSpannableString() دو پارامتر از نوع String را از ورودی دریافت میکند یکی [query] که بخشی از متن که قرار است توپر نمایش داده شود و دیگری [text] که کل متن میباشد. خروجی تابع نمونه از کلاس SpannableString میباشد.
نکته: در تابع زیر فرض بر این گرفته شده که مقدار پارامتر [query] همیشه از از شروع کلمه آغاز میشه. در غیر این صورت هیچ قسمتی از متن توپر نخواهد شد.
fun getSpannableString(text: String, query: String): SpannableString { val zwj = Char(0x200D).toString() // Zero Width Joiner Character if (query.length > text.length) return SpannableString(text) val firstMatch = "\\b$query".toRegex().find(text)?: return SpannableString(text) val startIndex = firstMatch.range.first var endIndex = startIndex + query.length val s = StringBuilder(text) if (s[endIndex-1].isConnectable2() && s[endIndex].isWhitespace().not()) { s.insert(endIndex, zwj + zwj) endIndex += 1 } return SpannableString(s.toString()).apply { setSpan( StyleSpan(Typeface.BOLD), startIndex, endIndex, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE ) setSpan( ForegroundColorSpan(Color.BLACK), startIndex, endIndex, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE ) } }
مثال برای نحوه استفاده از تابع بالا:
textview.text = getSpannableString(text = "حسین صفدری", query= "حسی")