امتدادات

تجربة طريقة "الكتابة"
‫Jetpack Compose هي مجموعة أدوات واجهة المستخدم التي يُنصح باستخدامها على Android. تعرَّف على كيفية استخدام النص في ميزة "إنشاء الأغنية".

النطاقات هي عناصر ترميز قوية يمكنك استخدامها لتنسيق النص على مستوى الحرف أو الفقرة. من خلال ربط نطاقات بأجسام نصية، يمكنك تغيير النص بطرق متنوعة، بما في ذلك إضافة لون، وجعل النص قابلاً للنقر، وتغيير حجم النص، ورسم النص بطريقة مخصّصة. يمكن أن تغيّر النطاقات أيضًا خصائص 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 إلى جزء النص الذي يتم تطبيق المدى عليه.

إذا أدرجت نصًا داخل حدود نطاق، يتوسّع النطاق تلقائيًا ليشمل النص المُدرَج. عند إدراج نص عند حدود النطاق، أي عند الفهرس البدائي أو النهائي، تحدّد المَعلمة flags ما إذا كان النطاق سيتوسّع ليشمل النص المُدرَج. استخدِم العلامة 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
);
صورة تعرض نصًا رماديًا، جزء منه باللون الأحمر
الشكل 1. نص منمّق باستخدام 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)");
صورة توضّح كيف يتضمّن النطاق المزيد من النص عند استخدام SPAN_EXCLUSIVE_INCLUSIVE.
الشكل 2. يتوسّع النطاق ليشمل نصًا إضافيًا عند استخدام 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)`‎
الشكل 3. نص يتضمّن نطاقات متعدّدة: ForegroundColorSpan(Color.RED) و StyleSpan(BOLD).

أنواع الفترات الزمنية في Android

يوفّر Android أكثر من 20 نوعًا من النطاقات في حزمة android.text.style. يصنّف نظام التشغيل Android النطاقات بطريقتَين أساسيتَين:

  • تأثير المدى على النص: يمكن أن يؤثر المدى في مظهر النص أو مقاييس النص.
  • نطاق الامتداد: يمكن تطبيق بعض الامتدادات على أحرف فردية، بينما يجب تطبيق البعض الآخر على فقرة كاملة.
صورة تعرض فئات مختلفة من الفترات الزمنية
الشكل 4. فئات النطاقات في Android

توضّح الأقسام التالية هذه الفئات بمزيد من التفصيل.

المدى الذي يؤثر في مظهر النص

تؤثر بعض النطاقات التي يتم تطبيقها على مستوى الحرف في مظهر النص، مثل تغيير لون النص أو الخلفية وإضافة خطوط تحتية أو خطوط يتوسطها شطب. تمتد هذه النطاقات إلى الفئة 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`
الشكل 5. نص تم تسطيره باستخدام 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
الشكل 6. تم تكبير النص باستخدام RelativeSizeSpan.

يؤدي تطبيق نطاق يؤثر في مقاييس النص إلى إعادة قياس النص بواسطة عنصر مراقبة من أجل الحصول على التنسيق والعرض الصحيحَين، على سبيل المثال، قد يؤدي تغيير حجم النص إلى ظهور الكلمات على أسطر مختلفة. يؤدي تطبيق النطاق السابق إلى إعادة القياس وإعادة احتساب تنسيق النص وإعادة رسم النص.

تمتدّ الفترات الزمنية التي تؤثّر في مقاييس النص إلى الفئة MetricAffectingSpan، وهي فئة مجرّدة تتيح للفئات الفرعية تحديد كيفية تأثير الفترة الزمنية في قياس النص من خلال توفير إمكانية الوصول إلى TextPaint. بما أنّ MetricAffectingSpan يمتد إلى CharacterStyle، تؤثر الفئات الفرعية في مظهر النص على مستوى الأحرف.

المدى الذي يؤثر في الفقرات

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

يوضّح الشكل 8 كيف يفصل نظام التشغيل Android الفقرات في النص.

الشكل 7. في 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
الشكل 8. تم تطبيق 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 ضبط النطاقات واستردادها من النص. عند إجراء الاختبار، نفِّذ اختبار Android JUnit للتأكّد من إضافة النطاقات الصحيحة في المواقع الصحيحة. يحتوي تطبيق "مثال على تنسيق النص" على نطاق يطبّق الترميز على النقاط النقطية من خلال إرفاق 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. على سبيل المثال، لنفترض أنّك تستخدم تنفيذًا مخصّصًا لنطاق يضيف نقطة تعداد قبل بعض النصوص. تكون النقطة ذات حجم ولون محدّدين، وتكون هناك فجوة بين الهامش الأيمن لمساحة الرسم والنقطة.

يمكنك اختبار سلوك هذه الفئة من خلال تنفيذ اختبار AndroidJUnit، والتحقّق من ما يلي:

  • في حال تطبيق النطاق بشكل صحيح، ستظهر نقطة تعداد بالحجم واللون المحدّدين على لوحة العرض، وستكون هناك مسافة مناسبة بين الهامش الأيمن ونقطة التعداد.
  • إذا لم تطبِّق النطاق، لن يظهر أي من السلوك المخصّص.

يمكنك الاطّلاع على تنفيذ هذه الاختبارات في نموذج TextStyling على GitHub.

يمكنك اختبار تفاعلات Canvas من خلال محاكاة لوحة العرض وتمرير العنصر المحاكى إلى طريقة drawLeadingMargin() والتحقّق من أنّه يتم استدعاء الطرق الصحيحة باستخدام المَعلمات الصحيحة.

يمكنك العثور على المزيد من عيّنات اختبارات Span في 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 ويحتفظ بها في الذاكرة. ويجعل ذلك جميع التعديلات مقصودة، إذ لا يمكنك تعديل العنصر الأصلي لتعديل النص.TextViewCharSequence وهذا يعني أنّه في كل مرة تضبط فيها نصًا جديدًا، ينشئ 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.