الفواصل هي عناصر ترميز فعّالة يمكنك استخدامها لتنسيق النص على مستوى
الحرف أو الفقرة. من خلال إرفاق أقسام بنصوص، يمكنك تغيير
النص بطرق متنوعة، بما في ذلك إضافة لون وجعل النص قابلاً للنقر عليه
وتغيير حجم النص ورسم النص بطريقة مخصّصة. يمكن للمقاطع أيضًا
تغيير خصائص TextPaint
، والرسم على
Canvas
، وتغيير تنسيق النص.
يقدّم Android عدة أنواع من النطاقات التي تغطي مجموعة متنوعة من أنماط تنسيق النص المشترَكة. يمكنك أيضًا إنشاء مقاطعك الخاصة لتطبيق تصميم مخصّص.
إنشاء نطاق وتطبيقه
لإنشاء نطاق، يمكنك استخدام إحدى الفئات المدرَجة في الجدول التالي. تختلف الفئات استنادًا إلى ما إذا كان النص نفسه قابلاً للتغيير، وما إذا كان تمييز النص قابلاً للتغيير، وبنية البيانات الأساسية التي تحتوي على بيانات النطاق.
الفئة | نص قابل للتغيير | الترميز القابل للتغيير | بنية البيانات |
---|---|---|---|
SpannedString |
لا | لا | مصفوفة خطية |
SpannableString |
لا | نعم | مصفوفة خطية |
SpannableStringBuilder |
نعم | نعم | شجرة الفواصل الزمنية |
وتوسّع كل الفئات الثلاث واجهة Spanned
. توفّر SpannableString
وSpannableStringBuilder
أيضًا واجهة
Spannable
.
في ما يلي كيفية تحديد السجلّ الذي يجب استخدامه:
- إذا لم تكن بصدد تعديل النص أو الترميز بعد الإنشاء، استخدِم
SpannedString
. - إذا كنت بحاجة إلى إرفاق عدد صغير من النطاقات بكائن نصي واحد
وإذا كان النص نفسه للقراءة فقط، استخدِم
SpannableString
. - إذا كنت بحاجة إلى تعديل النص بعد إنشائه وتحتاج إلى إرفاق نطاقَين بالنص، استخدِم
SpannableStringBuilder
. - إذا كنت بحاجة إلى إرفاق عدد كبير من النطاقات بعنصر نصي، بغض النظر عما إذا كان النص نفسه للقراءة فقط، استخدِم
SpannableStringBuilder
.
لتطبيق نطاق، استخدِم setSpan(Object _what_, int _start_, int _end_, int
_flags_)
على عنصر Spannable
. تشير المَعلمة what إلى النطاق الذي يتم تطبيقه
على النص، وتشير المَعلمتان start وend إلى الجزء
من النص الذي يتم تطبيق النطاق عليه.
إذا أدرجت نصًا داخل حدود نطاق، يتم توسيع النطاق تلقائيًا لتشمله. عند إدراج نص عند حدود الامتداد
، أي في فهرسَي البداية أو النهاية، تحدِّد مَعلمة العلامات
ما إذا كان سيتم توسيع النطاق ليشمل النص الذي تم إدراجه. استخدِم العلامة
Spannable.SPAN_EXCLUSIVE_INCLUSIVE
لتضمين النص المُدرَج، واستخدِم العلامة
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
لاستبعاد النص المُدرَج.
يوضّح المثال التالي كيفية إرفاق رمز
ForegroundColorSpan
بسلسلة:
Kotlin
val spannable = SpannableStringBuilder("Text is spantastic!") spannable.setSpan( ForegroundColorSpan(Color.RED), 8, // start 12, // end Spannable.SPAN_EXCLUSIVE_INCLUSIVE )
Java
SpannableStringBuilder spannable = new SpannableStringBuilder("Text is spantastic!"); spannable.setSpan( new ForegroundColorSpan(Color.RED), 8, // start 12, // end Spannable.SPAN_EXCLUSIVE_INCLUSIVE );

ForegroundColorSpan
بما أنّه تم ضبط النطاق باستخدام Spannable.SPAN_EXCLUSIVE_INCLUSIVE
، يتم
توسيعه ليشمل النص المُدرَج عند حدود النطاق، كما هو موضّح في المثال التالي:
Kotlin
val spannable = SpannableStringBuilder("Text is spantastic!") spannable.setSpan( ForegroundColorSpan(Color.RED), 8, // start 12, // end Spannable.SPAN_EXCLUSIVE_INCLUSIVE ) spannable.insert(12, "(& fon)")
Java
SpannableStringBuilder spannable = new SpannableStringBuilder("Text is spantastic!"); spannable.setSpan( new ForegroundColorSpan(Color.RED), 8, // start 12, // end Spannable.SPAN_EXCLUSIVE_INCLUSIVE ); spannable.insert(12, "(& fon)");

Spannable.SPAN_EXCLUSIVE_INCLUSIVE
.
يمكنك إرفاق عدّة مقاطع نصية بالنص نفسه. يوضّح المثال التالي كيفية إنشاء نص غامق وأحمر:
Kotlin
val spannable = SpannableString("Text is spantastic!") spannable.setSpan(ForegroundColorSpan(Color.RED), 8, 12, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) spannable.setSpan( StyleSpan(Typeface.BOLD), 8, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE )
Java
SpannableString spannable = new SpannableString("Text is spantastic!"); spannable.setSpan( new ForegroundColorSpan(Color.RED), 8, 12, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE ); spannable.setSpan( new StyleSpan(Typeface.BOLD), 8, spannable.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE );

ForegroundColorSpan(Color.RED)
و
StyleSpan(BOLD)
أنواع الفواصل في Android
يقدّم Android أكثر من 20 نوعًا من النطاقات في حزمة android.text.style. يصنف Android النطاقات بطريقتَين أساسيتَين:
- كيفية تأثير النطاق في النص: يمكن أن يؤثر النطاق في مظهر النص أو قياسات النص.
- نطاق النطاق: يمكن تطبيق بعض النطاقات على أحرف فردية، في حين يجب تطبيق البعض الآخر على فقرة بأكملها.

توضّح الأقسام التالية هذه الفئات بمزيد من التفصيل.
النطاقات التي تؤثّر في مظهر النص
تؤثر بعض النطاقات التي يتم تطبيقها على مستوى الحرف في مظهر النص، مثل
تغيير لون النص أو الخلفية وإضافة خطوط تحتية أو مشطوبة. تُوسّع هذه
العناصر span فئة
CharacterStyle
.
يوضّح مثال الرمز البرمجي التالي كيفية تطبيق UnderlineSpan
لتمييز
النص:
Kotlin
val string = SpannableString("Text with underline span") string.setSpan(UnderlineSpan(), 10, 19, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
Java
SpannableString string = new SpannableString("Text with underline span"); string.setSpan(new UnderlineSpan(), 10, 19, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

UnderlineSpan
إنّ النطاقات التي تؤثر في مظهر النص فقط تؤدي إلى إعادة رسم النص بدون
إعادة احتساب التنسيق. تنفِّذ هذه النطاقات
UpdateAppearance
وتوسِّع
CharacterStyle
.
تحدِّد فئات CharacterStyle
الفرعية كيفية رسم النص من خلال توفير إمكانية الوصول إلى
تعديل TextPaint
.
النطاقات التي تؤثّر في مقاييس النص
تؤثر النطاقات الأخرى التي تنطبق على مستوى الحرف في مقاييس النص، مثل
ارتفاع السطر وحجم النص. تُمدِّد هذه النطاقات فئة
MetricAffectingSpan
.
ينشئ مثال التعليمة البرمجية التالي رمزًا RelativeSizeSpan
يؤدي إلى زيادة حجم النص بنسبة %50:
Kotlin
val string = SpannableString("Text with relative size span") string.setSpan(RelativeSizeSpan(1.5f), 10, 24, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
Java
SpannableString string = new SpannableString("Text with relative size span"); string.setSpan(new RelativeSizeSpan(1.5f), 10, 24, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

RelativeSizeSpan
يؤدي تطبيق نطاق يؤثر في مقاييس النص إلى أن يعيد عنصر المراقبةقياس النص لعرضه وتنسيقه بشكل صحيح. على سبيل المثال، قد يؤدي تغيير حجم النص إلى ظهور الكلمات على أسطر مختلفة. يؤدي تطبيق الفاصل السابق إلى إعادة قياس تنسيق النص وإعادة احتسابه وإعادة رسمه.
تُوسّع النطاقات التي تؤثّر في مقاييس النصوص فئة MetricAffectingSpan
، وهي
فئة مجردة تتيح للفئات الفرعية تحديد كيفية تأثير النطاق في قياس النصوص
من خلال توفير إمكانية الوصول إلى TextPaint
. بما أنّ MetricAffectingSpan
توسّع
CharacterStyle
، تؤثّر الفئات الفرعية في مظهر النص على مستوى
الحرف.
النطاقات التي تؤثر في الفقرات
يمكن أن يؤثر النطاق أيضًا في النص على مستوى الفقرة، مثل تغيير
المحاذاة أو هامش مجموعة من النصوص. النطاقات التي تؤثر في فقرات كاملة
تنفِّذ ParagraphStyle
. لاستخدام هذه النطاقات، يمكنك إرفاقها بالفقرة بأكملها، باستثناء حرف السطر الجديد المنتهي. إذا حاولت تطبيق نطاق فقرة على شيء آخر غير
فقرة كاملة، لن يطبّق Android النطاق على الإطلاق.
يوضّح الشكل 8 كيفية فصل Android للفقرات في النص.

\n
).
يطبّق مثال الرمز البرمجي التالي علامة
QuoteSpan
على فقرة. يُرجى العِلم أنّه
في حال إرفاق النطاق بأي موضع آخر غير بداية أو نهاية
فقرة، لن يطبِّق Android النمط على الإطلاق.
Kotlin
spannable.setSpan(QuoteSpan(color), 8, text.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
Java
spannable.setSpan(new QuoteSpan(color), 8, text.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

QuoteSpan
على فقرة.
إنشاء مقاطع مخصّصة
إذا كنت بحاجة إلى وظائف أكثر من تلك المقدَّمة في علامات Android الحالية، يمكنك تنفيذ علامة مخصّصة. عند تنفيذ نطاقك الخاص، عليك تحديد ما إذا كان نطاقك يؤثر في النص على مستوى الحرف أو على مستوى الفقرة، وما إذا كان يؤثر في تنسيق النص أو مظهره. يساعدك ذلك في تحديد الفئات الأساسية التي يمكنك توسيع نطاقها والواجهات التي قد تحتاج إلى تنفيذها. يمكنك استخدام الجدول التالي كمرجع:
السيناريو | الفئة أو الواجهة |
---|---|
يؤثر النطاق في النص على مستوى الحرف. | CharacterStyle |
يؤثر نطاق النص في مظهره. | UpdateAppearance |
يؤثر النطاق في مقاييس النص. | UpdateLayout |
يؤثر النطاق في النص على مستوى الفقرة. | ParagraphStyle |
على سبيل المثال، إذا كنت بحاجة إلى تنفيذ نطاق مخصّص يُعدّل حجم النص و
لونه، وسِّع RelativeSizeSpan
. من خلال اكتساب السمات، RelativeSizeSpan
يُوسّع CharacterStyle
وينفِّذ الواجهات Update
. بما أنّ هذه
الفئة توفّر بالفعل وظائف استدعاء للحدثَين updateDrawState
وupdateMeasureState
،
يمكنك إلغاء هذه وظائف الاستدعاء لتنفيذ السلوك المخصّص. تنشئ التعليمة البرمجية التالية عنصرًا مخصّصًا يمتد إلى RelativeSizeSpan
ويتجاوز الإجراء المرجعي updateDrawState
لضبط لون TextPaint
:
Kotlin
class RelativeSizeColorSpan( size: Float, @ColorInt private val color: Int ) : RelativeSizeSpan(size) { override fun updateDrawState(textPaint: TextPaint) { super.updateDrawState(textPaint) textPaint.color = color } }
Java
public class RelativeSizeColorSpan extends RelativeSizeSpan { private int color; public RelativeSizeColorSpan(float spanSize, int spanColor) { super(spanSize); color = spanColor; } @Override public void updateDrawState(TextPaint textPaint) { super.updateDrawState(textPaint); textPaint.setColor(color); } }
يوضّح هذا المثال كيفية إنشاء نطاق مخصّص. يمكنك الحصول على
التأثير نفسه من خلال تطبيق RelativeSizeSpan
وForegroundColorSpan
على النص.
استخدام نطاق الاختبار
تتيح لك واجهة Spanned
ضبط النطاقات واستردادها أيضًا من
النص. عند إجراء الاختبار، نفِّذ اختبار JUnit لنظام التشغيل Android للتحقّق من إضافة النطاقات الصحيحة
في المواقع الصحيحة. يحتوي تطبيق ตัวอย่าง تنسيق النص
على نطاق يطبّق ترميزًا على النقاط التعدادية من خلال إرفاق BulletPointSpan
بالنص. يوضّح مثال الرمز البرمجي التالي كيفية اختبار
ما إذا كانت النقاط النقطية تظهر على النحو المتوقّع:
Kotlin
@Test fun textWithBulletPoints() { val result = builder.markdownToSpans("Points\n* one\n+ two") // Check whether the markup tags are removed. assertEquals("Points\none\ntwo", result.toString()) // Get all the spans attached to the SpannedString. val spans = result.getSpans<Any>(0, result.length, Any::class.java) // Check whether the correct number of spans are created. assertEquals(2, spans.size.toLong()) // Check whether the spans are instances of BulletPointSpan. val bulletSpan1 = spans[0] as BulletPointSpan val bulletSpan2 = spans[1] as BulletPointSpan // Check whether the start and end indices are the expected ones. assertEquals(7, result.getSpanStart(bulletSpan1).toLong()) assertEquals(11, result.getSpanEnd(bulletSpan1).toLong()) assertEquals(11, result.getSpanStart(bulletSpan2).toLong()) assertEquals(14, result.getSpanEnd(bulletSpan2).toLong()) }
Java
@Test public void textWithBulletPoints() { SpannedString result = builder.markdownToSpans("Points\n* one\n+ two"); // Check whether the markup tags are removed. assertEquals("Points\none\ntwo", result.toString()); // Get all the spans attached to the SpannedString. Object[] spans = result.getSpans(0, result.length(), Object.class); // Check whether the correct number of spans are created. assertEquals(2, spans.length); // Check whether the spans are instances of BulletPointSpan. BulletPointSpan bulletSpan1 = (BulletPointSpan) spans[0]; BulletPointSpan bulletSpan2 = (BulletPointSpan) spans[1]; // Check whether the start and end indices are the expected ones. assertEquals(7, result.getSpanStart(bulletSpan1)); assertEquals(11, result.getSpanEnd(bulletSpan1)); assertEquals(11, result.getSpanStart(bulletSpan2)); assertEquals(14, result.getSpanEnd(bulletSpan2)); }
لمزيد من أمثلة الاختبارات، يُرجى الاطّلاع على MarkdownBuilderTest على GitHub.
اختبار النطاقات المخصّصة
عند اختبار النطاقات، تأكَّد من أنّ TextPaint
يحتوي على تعديلات
متوقّعة وأنّ العناصر الصحيحة تظهر في Canvas
. على سبيل المثال، ننصحك بتنفيذ عنصر span مخصّص يضيف نقطة قائمة في بداية
بعض النصوص. يكون للنقطة قائمة بحجم ولون محدّدَين، وتوجد فجوة
بين الهامش الأيمن للمساحة القابلة للرسم والنقطة قائمة.
يمكنك اختبار سلوك هذه الفئة من خلال تنفيذ اختبار AndroidJUnit، والتحقّق مما يلي:
- في حال تطبيق النطاق بشكل صحيح، ستظهر نقطة قائمة بالحجم وال اللون المحدَّدَين على اللوحة، وستظهر المساحة المناسبة بين الmargin الأيمن ونقطة القائمة.
- في حال عدم تطبيق النطاق، لن يظهر أي من السلوك المخصّص.
يمكنك الاطّلاع على تنفيذ هذه الاختبارات في نموذج TextStyling على GitHub.
يمكنك اختبار تفاعلات Canvas من خلال محاكاة اللوحة، ونقل العنصر الذي تمّت محاكاته إلى drawLeadingMargin()
الطريقة، والتأكّد من استدعاء الطرق الصحيحة باستخدام المَعلمات الصحيحة.
يمكنك العثور على المزيد من نماذج اختبار النطاقات في BulletPointSpanTest.
أفضل الممارسات لاستخدام النطاقات
هناك عدة طرق فعّالة في استخدام الذاكرة لضبط النص في TextView
، وذلك تبعًا
لاحتياجاتك.
إرفاق نطاق أو فصله بدون تغيير النص الأساسي
يحتوي TextView.setText()
على العديد من عمليات التحميل الزائد التي تتعامل مع النطاقات بشكلٍ مختلف. على سبيل المثال، يمكنك
ضبط عنصر نص Spannable
باستخدام الرمز البرمجي التالي:
Kotlin
textView.setText(spannableObject)
Java
textView.setText(spannableObject);
عند استدعاء هذه الوظيفة الزائدة setText()
، تنشئ TextView
نسخة من
Spannable
بصفتها SpannedString
وتحفظها في الذاكرة بصفتها CharSequence
.
وهذا يعني أنّ النص والمقاطع لا يمكن تغييرها، لذا عندما تحتاج إلى
تعديل النص أو المقاطع، أنشئ عنصرًا جديدًا من النوع Spannable
واطلب
setText()
مرة أخرى، ما يؤدي أيضًا إلى إعادة قياس التنسيق وإعادة رسمه.
للإشارة إلى أنّ النطاقات يجب أن تكون قابلة للتغيير، يمكنك بدلاً من ذلك استخدام
setText(CharSequence text, TextView.BufferType
type)
،
كما هو موضّح في المثال التالي:
Kotlin
textView.setText(spannable, BufferType.SPANNABLE) val spannableText = textView.text as Spannable spannableText.setSpan( ForegroundColorSpan(color), 8, spannableText.length, SPAN_INCLUSIVE_INCLUSIVE )
Java
textView.setText(spannable, BufferType.SPANNABLE); Spannable spannableText = (Spannable) textView.getText(); spannableText.setSpan( new ForegroundColorSpan(color), 8, spannableText.getLength(), SPAN_INCLUSIVE_INCLUSIVE);
في هذا المثال، تؤدي المَعلمة
BufferType.SPANNABLE
إلى إنشاء TextView
لإنشاء SpannableString
، وأصبح الآن للكائن
CharSequence
الذي يحتفظ به TextView
ترميز قابل للتغيير و
نص غير قابل للتغيير. لتعديل النطاق، استرجع النص كعنصر Spannable
ثم
عدِّل النطاقات حسب الحاجة.
عند إرفاق أقسام أو فصلها أو تغيير موضعها، يتم تعديل TextView
تلقائيًا
لتعكس التغيير في النص. في حال تغيير سمة داخلية
لقطعة حالية، استخدِم invalidate()
لإجراء تغييرات ذات صلة بالمظهر أو
requestLayout()
لإجراء تغييرات ذات صلة بالمقياس.
ضبط النص في TextView عدة مرات
في بعض الحالات، مثل عند استخدام
RecyclerView.ViewHolder
،
قد تحتاج إلى إعادة استخدام TextView
وضبط النص عدة مرات. بشكلٍ
تلقائي، بغض النظر عمّا إذا كنت قد ضبطتBufferType
، ينشئTextView
نسخة من عنصرCharSequence
ويحتفظ بها في الذاكرة. يؤدي ذلك إلى جعل جميع تعديلات
TextView
مقصودة، إذ لا يمكنك تعديل العنصر
CharSequence
الأصلي لتعديل النص. وهذا يعني أنّه في كل مرة تضبط فيها
نصًا جديدًا، تنشئ TextView
عنصرًا جديدًا.
إذا كنت تريد التحكّم بشكل أكبر في هذه العملية وتجنُّب إنشاء
العنصر الإضافي، يمكنك تنفيذ
Spannable.Factory
الخاص بك وإلغاء
newSpannable()
.
بدلاً من إنشاء عنصر نصي جديد، يمكنك تحويل القيمة المتوفّرة
CharSequence
إلى Spannable
وعرضها، كما هو موضّح في المثال التالي:
Kotlin
val spannableFactory = object : Spannable.Factory() { override fun newSpannable(source: CharSequence?): Spannable { return source as Spannable } }
Java
Spannable.Factory spannableFactory = new Spannable.Factory(){ @Override public Spannable newSpannable(CharSequence source) { return (Spannable) source; } };
يجب استخدام textView.setText(spannableObject, BufferType.SPANNABLE)
عند
ضبط النص. بخلاف ذلك، يتم إنشاء المصدر CharSequence
كمثيل Spanned
ولا يمكن تحويله إلى Spannable
، ما يؤدي إلى طرح newSpannable()
لخطأ
ClassCastException
.
بعد إلغاء newSpannable()
، أخبِر TextView
باستخدام Factory
الجديد:
Kotlin
textView.setSpannableFactory(spannableFactory)
Java
textView.setSpannableFactory(spannableFactory);
اضبط عنصر Spannable.Factory
مرة واحدة، بعد الحصول على مرجع إلى
TextView
مباشرةً. إذا كنت تستخدم RecyclerView
، اضبط عنصر Factory
عند
تضخيم مشاهداتك لأول مرة. يتجنّب ذلك إنشاء عناصر إضافية عندما يربط
RecyclerView
عنصرًا جديدًا بـ ViewHolder
.
تغيير سمات النطاق الداخلي
إذا كنت بحاجة إلى تغيير سمة داخلية فقط لعنصر نصي قابل للتغيير، مثل
لون النقطة في عنصر نصي مخصّص للنقاط، يمكنك تجنُّب الوقت الذي تستغرِقه عملية استدعاء
setText()
عدة مرات من خلال الاحتفاظ بمرجع إلى العنصر النصي عند إنشائه.
عندما تحتاج إلى تعديل النطاق، يمكنك تعديل المرجع ثم استدعاء
invalidate()
أو requestLayout()
على TextView
، استنادًا إلى نوع
السمة التي غيّرتها.
في مثال التعليمات البرمجية التالي، يكون لون تنفيذ النقاط المخصّصة هو الأحمر التلقائي الذي يتغيّر إلى الرمادي عند النقر على زر:
Kotlin
class MainActivity : AppCompatActivity() { // Keeping the span as a field. val bulletSpan = BulletPointSpan(color = Color.RED) override fun onCreate(savedInstanceState: Bundle?) { ... val spannable = SpannableString("Text is spantastic") // Setting the span to the bulletSpan field. spannable.setSpan( bulletSpan, 0, 4, Spanned.SPAN_INCLUSIVE_INCLUSIVE ) styledText.setText(spannable) button.setOnClickListener { // Change the color of the mutable span. bulletSpan.color = Color.GRAY // Color doesn't change until invalidate is called. styledText.invalidate() } } }
Java
public class MainActivity extends AppCompatActivity { private BulletPointSpan bulletSpan = new BulletPointSpan(Color.RED); @Override protected void onCreate(Bundle savedInstanceState) { ... SpannableString spannable = new SpannableString("Text is spantastic"); // Setting the span to the bulletSpan field. spannable.setSpan(bulletSpan, 0, 4, Spanned.SPAN_INCLUSIVE_INCLUSIVE); styledText.setText(spannable); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { // Change the color of the mutable span. bulletSpan.setColor(Color.GRAY); // Color doesn't change until invalidate is called. styledText.invalidate(); } }); } }
استخدام وظائف إضافة Android KTX
يحتوي Android KTX أيضًا على دوالّ إضافية تسهّل العمل مع النطاقات. لمزيد من المعلومات، يُرجى الاطّلاع على مستندات حِزمة androidx.core.text.