النطاقات هي كائنات ترميز قوية يمكنك استخدامها لتصميم النص على مستوى الحرف أو الفقرة. من خلال إرفاق النطاقات بكائنات النص، يمكنك تغيير النص بطرق متنوعة، بما في ذلك إضافة لون وجعل النص قابلاً للنقر وتغيير حجم النص ورسمه بطريقة مخصّصة. يمكن للنطاقات أيضًا
تغيير 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 إلى جزء النص الذي تطبّق عليه النطاق.
إذا أدرجت نصًا داخل حدود النطاق، يتوسّع النطاق تلقائيًا ليشمل النص المُدرَج. عند إدراج نص عند حدود النطاق، أي عند الفهرسَين 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 );
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 النطاقات بطريقتَين أساسيتَين:
- كيفية تأثير النطاق في النص: يمكن أن يؤثر النطاق في مظهر النص أو مقاييس النص.
- نطاق النطاق: يمكن تطبيق بعض النطاقات على أحرف فردية، بينما يجب تطبيق نطاقات أخرى على فقرة كاملة.
تصف الأقسام التالية هذه الفئات بمزيد من التفصيل.
النطاقات التي تؤثر في مظهر النص
تؤثر بعض النطاقات التي يتم تطبيقها على مستوى الحرف في مظهر النص، مثل تغيير لون النص أو الخلفية وإضافة خطوط تحت النص أو خطوط يتوسطها. توسّع هذه
النطاقات فئة
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، تؤثر الفئات الفرعية في مظهر النص على مستوى الحرف.
النطاقات التي تؤثر في الفقرات
يمكن أن يؤثر النطاق أيضًا في النص على مستوى الفقرة، مثل تغيير المحاذاة أو الهامش لكتلة نص. تنفّذ النطاقات التي تؤثر في الفقرات بأكملها
implement ParagraphStyle. لاستخدام هذه النطاقات، عليك إرفاقها بالفقرة بأكملها، باستثناء حرف السطر الجديد في النهاية. إذا حاولت تطبيق نطاق فقرة على شيء آخر غير فقرة كاملة، لن يطبّق Android النطاق على الإطلاق.
يوضّح الشكل 8 كيف يفصل Android الفقرات في النص.
\n) character.
يطبّق مثال الرمز البرمجي التالي
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 ضبط النطاقات واستردادها من النص. عند الاختبار، نفِّذ اختبار Android JUnit
للتأكّد من إضافة النطاقات الصحيحة
في المواضع الصحيحة. يحتوي نموذج تطبيق Text Styling على نطاق يطبّق الترميز على نقاط التعداد من خلال إرفاق 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 sample على 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.