امتدادات

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

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

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

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

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

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

تقوم جميع الفئات الثلاث بتوسيع نطاق Spanned من واجهة pyplot. يُوسِّع 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 إلى string:

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 spans.

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

الفواصل التي تؤثر في مظهر النص

تؤثر بعض المسافات التي تنطبق على مستوى الحرف في مظهر النص، مثل تغيير لون النص أو الخلفية وإضافة تسطير أو خطوط يتوسطها خط. هذه تمتد تمتد صف واحد (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);
صورة تعرض استخدام مقياس relatedSizeSpan
الشكل 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);
صورة تعرض مثالاً على SketchSpan
الشكل 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:

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

هناك عدة طرق فعالة في الذاكرة لضبط النص في 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();
            }
        });
    }
}

استخدام وظائف إضافة KTX لنظام التشغيل Android

يحتوي Android KTX أيضًا على وظائف امتداد تجعل العمل مع امتدادات كثيرًا. لمزيد من المعلومات، راجع وثائق androidx.core.text طرد.