المسافات هي كائنات ترميز فعالة يمكنك استخدامها لتنسيق النص على مستوى الحرف أو الفقرة. من خلال إرفاق امتدادات بكائنات نصية، يمكنك تغيير
النص بعدة طرق، بما في ذلك إضافة لون وجعل النص قابلاً للنقر
وتكبير حجم النص ورسم النص بطريقة مخصّصة. يمكن للامتدادات أيضًا
تغيير سمات 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 );
نظرًا لضبط النطاق باستخدام 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)");
يمكنك إرفاق امتدادات متعددة بالنص نفسه. يوضح المثال التالي كيفية إنشاء نص غامق وأحمر:
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 );
أنواع نطاقات Android
يوفر Android أكثر من 20 نوعًا من النطاقات في حزمة android.text.style. يصنّف 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);
تؤدي المساحات الفاصلة التي تؤثر فقط على مظهر النص إلى إعادة رسم النص بدون
إعادة احتساب التنسيق. تنفّذ هذه النطاقات 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);
يؤدي تطبيق مسافة تؤثر في مقاييس النص إلى إعادة قياس النص من أجل الحصول على التنسيق والعرض الصحيحَين من خلال كائن الملاحظة. على سبيل المثال، قد يؤدي تغيير حجم النص إلى ظهور الكلمات على أسطر مختلفة. يؤدي تطبيق النطاق السابق إلى إعادة قياس النص وإعادة احتسابه وإعادة رسمه.
إنّ الامتدادات التي تؤثر في مقاييس النص تعمل على تمديد الفئة MetricAffectingSpan
، وهي فئة مجردة تتيح للفئات الفرعية تحديد كيفية تأثير الامتداد في قياس النص من خلال توفير إمكانية الوصول إلى TextPaint
. ولأنّ MetricAffectingSpan
يوسِّع
CharacterSpan
، تؤثر الفئات الفرعية في مظهر النص على مستوى
الحرف.
الفواصل التي تؤثر في الفقرات
يمكن أن يؤثر الامتداد أيضًا على النص على مستوى الفقرة، مثل تغيير المحاذاة أو هامش جزء من النص. الفواصل التي تؤثر على تنفيذ
الفقرات بالكامل ParagraphStyle
. لاستخدام هذه المسافات، يجب إرفاقها بالفقرة بأكملها، باستثناء حرف السطر الجديد في النهاية. إذا حاولت تطبيق مدى فقرة على شيء آخر
غير الفقرة بأكملها، فإن Android لا يطبق المدى على الإطلاق.
يوضّح الشكل 8 كيفية فصل Android بين الفقرات في النص.
في مثال الرمز التالي، يتم تطبيق 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);
إنشاء نطاقات مخصّصة
إذا كنت بحاجة إلى وظائف أكثر من تلك المتوفّرة في نطاقات 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.