امتدادات

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

المسافات هي كائنات ترميز فعالة يمكنك استخدامها لتنسيق النص على مستوى الحرف أو الفقرة. من خلال إرفاق امتدادات بكائنات نصية، يمكنك تغيير النص بعدة طرق، بما في ذلك إضافة لون وجعل النص قابلاً للنقر وتكبير حجم النص ورسم النص بطريقة مخصّصة. يمكن للامتدادات أيضًا تغيير سمات TextPaint والرسم على Canvas وتغيير تنسيق النص.

يوفر Android عدة أنواع من الامتدادات التي تغطي مجموعة متنوعة من أنماط أنماط النص الشائعة. يمكنك أيضًا إنشاء النطاقات الزمنية الخاصة بك لتطبيق نمط مخصّص.

إنشاء نطاق وتطبيقه

لإنشاء مدى، يمكنك استخدام إحدى الفئات المدرجة في الجدول التالي. تختلف الفئات استنادًا إلى ما إذا كان النص نفسه قابلاً للتغيير أم لا، وما إذا كان ترميز النص قابلاً للتغيير، وإلى بنية البيانات الأساسية التي تحتوي على بيانات الامتداد.

الفئة النص القابل للتغيير الترميز القابل للتغيير هيكل البيانات
SpannedString لا لا الصفيفة الخطية
SpannableString لا نعم الصفيفة الخطية
SpannableStringBuilder نعم نعم شجرة الفاصل الزمني

تعمل الفئات الثلاث جميعها على توسيع واجهة Spanned. تم توسيع واجهة Spannable أيضًا من قِبل SpannableString وSpannableStringBuilder.

في ما يلي كيفية تحديد التنسيق الذي يجب استخدامه:

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

لتطبيق نطاق، استدعِ setSpan(Object _what_, int _start_, int _end_, int _flags_) على عنصر Spannable. تشير المَعلمة what إلى الامتداد الذي تطبّقه على النص، وتشير المَعلمتَان start وend إلى جزء النص الذي تريد تطبيق الامتداد عليه.

إذا أدرجت نصًا داخل حدود مسافة، يتم توسيع الامتداد تلقائيًا لتضمين النص المدرج. عند إدراج نص في حدود الامتداد، أي عند الفهارس start أو النهاية، تحدِّد المعلَمة 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);
صورة تعرض كيفية تسطير النص باستخدام "تسطير"
الشكل 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);
صورة تعرض استخدام StandardSizeSpan
الشكل 6. تم تكبير حجم النص باستخدام RelativeSizeSpan.

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

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

الفواصل التي تؤثر في الفقرات

يمكن أن يؤثر الامتداد أيضًا على النص على مستوى الفقرة، مثل تغيير المحاذاة أو هامش جزء من النص. الفواصل التي تؤثر على تنفيذ الفقرات بالكامل 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);
صورة تعرض مثالاً على اقتباسSpan
الشكل 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.

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

يمكنك العثور على المزيد من نماذج الاختبار للنطاق في BulletPointSpanTest.

أفضل الممارسات لاستخدام spans

هناك عدة طرق فعّالة من حيث الذاكرة لضبط النص في 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.