קטעי טקסט הם אובייקטים חזקים של סימון שאפשר להשתמש בהם כדי לעצב טקסט ברמת התו או הפסקה. כשמחברים קטעי טקסט לאובייקטים של טקסט, אפשר לשנות את הטקסט בדרכים שונות, כולל הוספת צבע, הפיכת הטקסט לקליקביל, שינוי גודל הטקסט ורישום טקסט באופן מותאם אישית. אפשר גם לשנות את המאפיינים של TextPaint
, לצייר על Canvas
ולשנות את פריסת הטקסט באמצעות קטעי טקסט.
ב-Android יש כמה סוגים של span שמכסים מגוון דפוסים נפוצים של עיצוב טקסט. אפשר גם ליצור קטעי טקסט משלכם כדי להחיל עיצוב בהתאמה אישית.
יצירת span והחלה שלו
כדי ליצור span, אפשר להשתמש באחת מהכיתות שמפורטות בטבלה הבאה. ההבדלים בין הכיתות נובעים מהעובדה שהטקסט עצמו יכול להשתנות, מהעובדה שסימון הטקסט יכול להשתנות ומהמבנה הבסיסי של הנתונים שמכיל את נתוני ה-span.
דרגה | טקסט שניתן לשינוי | תגי עיצוב שניתן לשינוי | מבנה נתונים |
---|---|---|---|
SpannedString |
לא | לא | מערך לינארי |
SpannableString |
לא | כן | מערך לינארי |
SpannableStringBuilder |
כן | כן | עץ Interval |
כל שלוש המחלקות מרחיבות את הממשק Spanned
. גם SpannableString
וגם SpannableStringBuilder
מרחיבים את הממשק Spannable
.
כך מחליטים באיזה פורמט להשתמש:
- אם לא משנים את הטקסט או את ה-Markup אחרי היצירה, משתמשים ב-
SpannedString
. - אם צריך לצרף מספר קטן של קטעי טקסט לאובייקט טקסט יחיד והטקסט עצמו הוא לקריאה בלבד, צריך להשתמש ב-
SpannableString
. - אם צריך לשנות טקסט אחרי היצירה ולהצמיד אליו קטעי טקסט, משתמשים ב-
SpannableStringBuilder
. - אם אתם צריכים לצרף מספר גדול של קטעי טקסט לאובייקט טקסט, ללא קשר לכך שהטקסט עצמו הוא לקריאה בלבד, השתמשו ב-
SpannableStringBuilder
.
כדי להחיל span, קוראים ל-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
, ה-span מתרחב כך שיכלול טקסט נוסף.
אפשר לצרף כמה קטעי טקסט לאותו טקסט. בדוגמה הבאה מוסבר איך ליצור טקסט מודגש ואדום:
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)
.
סוגי span ב-Android
ב-Android יש יותר מ-20 סוגי span בחבילה android.text.style. מערכת Android מסווגת את ה-spans בשתי דרכים עיקריות:
- איך ה-span משפיע על הטקסט: ה-span יכול להשפיע על המראה של הטקסט או על מדדי הטקסט.
- היקף ה-span: אפשר להחיל span מסוימים על תווים בודדים, ואילו אחרים צריך להחיל על פסקה שלמה.

בקטעים הבאים מוסבר בהרחבה על הקטגוריות האלה.
קטעי טקסט שמשפיעים על המראה של הטקסט
חלק מהקטעים שחלים ברמת התו השפיעים על מראה הטקסט, כמו שינוי צבע הטקסט או הרקע והוספת קו תחתון או קו מחיקה. ה-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);

UnderlineSpan
.
קטעי טקסט שמשפיעים רק על המראה של הטקסט גורמים לציור מחדש של הטקסט בלי לגרום לחישוב מחדש של הפריסה. הקטעים האלה מיישמים את UpdateAppearance
ומרחיבים את CharacterStyle
.
כדי להגדיר איך לצייר טקסט, תת-הסוגים של CharacterStyle
מספקים גישה לעדכון של TextPaint
.
קטעי טקסט שמשפיעים על מדדי הטקסט
קטעי טקסט אחרים שחלים ברמת התו השפיעים על מדדי הטקסט, כמו גובה השורה וגודל הטקסט. ה-spans האלה מרחיבים את הכיתה 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
.
החלת span שמשפיע על מדדי הטקסט גורמת לאובייקט שמתבונן למדוד מחדש את הטקסט כדי לבדוק אם הפריסה והעיבוד שלו נכונים. לדוגמה, שינוי גודל הטקסט עשוי לגרום למילים להופיע בשורות שונות. החלת ה-span הקודם מפעילה מדידה מחדש, חישוב מחדש של פריסת הטקסט ורישום מחדש של הטקסט.
קטעי טקסט שמשפיעים על מדדי הטקסט מרחיבים את הכיתה MetricAffectingSpan
, שהיא כיתה מופשטת שמאפשרת לתת-כיתות להגדיר איך הקטע משפיע על מדידת הטקסט על ידי מתן גישה ל-TextPaint
. מאחר ש-MetricAffectingSpan
הוא תת-סוג של CharacterStyle
, תת-הסוגים משפיעים על המראה של הטקסט ברמת התו.
קטעי טקסט שמשפיעים על פסקאות
span יכול גם להשפיע על טקסט ברמת הפסקה, למשל שינוי היישור או השוליים של מקטע טקסט. קטעי טקסט שמשפיעים על פסקאות שלמות מיישמים את ParagraphStyle
. כדי להשתמש בקטעי הקוד האלה, צריך לצרף אותם לכל הפסקה, לא כולל תו השורה החדשה בסוף. אם מנסים להחיל קטע טקסט של פסקה על משהו שאינו פסקה שלמה, מערכת Android לא מחילה את הקטע בכלל.
באיור 8 מוצגת אופן ההפרדה של Android בין פסקאות בטקסט.

\n
).
בדוגמת הקוד הבאה מוחלים את הערך QuoteSpan
על פסקה. חשוב לזכור שאם מחברים את ה-span למיקום כלשהו שאינו תחילת או סוף הפסקה, מערכת 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
הוחלה על פסקה.
יצירת מקטעים מותאמים אישית
אם אתם צריכים פונקציונליות נוספת מעבר לזו שסופקת על ידי ה-spans הקיימים ל-Android, תוכלו להטמיע span מותאם אישית. כשמטמיעים span משלכם, צריך להחליט אם ה-span ישפיע על הטקסט ברמת התווים או ברמת הפסקה, וגם אם הוא ישפיע על הפריסה או על המראה של הטקסט. כך תוכלו לקבוע אילו מחלקות בסיס אפשר להרחיב ואילו ממשקים ייתכן שתצטרכו להטמיע. בטבלה הבאה מפורט מידע נוסף:
תרחיש | כיתה או ממשק |
---|---|
ה-span משפיע על הטקסט ברמת התו. | CharacterStyle |
הרוחב של ה-span משפיע על המראה של הטקסט. | UpdateAppearance |
טווח הזמן משפיע על מדדי הטקסט. | UpdateLayout |
ה-span משפיע על הטקסט ברמת הפסקה. | ParagraphStyle |
לדוגמה, אם אתם צריכים להטמיע span בהתאמה אישית שמשנה את גודל הטקסט ואת הצבע שלו, תוכלו להרחיב את RelativeSizeSpan
. באמצעות ירושה, RelativeSizeSpan
מרחיב את CharacterStyle
ומטמיע את שני הממשקים Update
. מכיוון שהכיתה הזו כבר מספקת קריאות חזרה ל-updateDrawState
ול-updateMeasureState
, אפשר לשנות את ברירת המחדל של הקריאות האלה כדי להטמיע את ההתנהגות בהתאמה אישית. הקוד הבא יוצר span בהתאמה אישית שמרחיב את 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
על הטקסט.
בדיקת השימוש ב-span
הממשק Spanned
מאפשר להגדיר קטעי טקסט (spans) וגם לאחזר קטעי טקסט מתוך טקסט. בזמן הבדיקה, מטמיעים בדיקת JUnit ל-Android כדי לוודא שה-spans הנכונים נוספו במיקומים הנכונים. אפליקציית הדוגמה של עיצוב טקסט מכילה span שמחבר את 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.
בדיקת קטעים מותאמים אישית
כשבודקים את ה-spans, מוודאים שהשינויים הצפויים מופיעים ב-TextPaint
ושהרכיבים הנכונים מופיעים ב-Canvas
. לדוגמה, אפשר להטמיע span בהתאמה אישית שמוסיף סימן נקודה-פסיק לתחילת טקסט מסוים. לנקודת הסימון יש גודל וצבע מוגדרים, ויש פער בין השוליים הימניים של האזור שאפשר לצייר בו לבין נקודת הסימון.
אפשר לבדוק את ההתנהגות של הכיתה הזו על ידי הטמעת בדיקת AndroidJUnit, ולבדוק את הדברים הבאים:
- אם מחילים את ה-span בצורה נכונה, נקודה עם תווית תיצור בבד הציור בגודל ובצבע שצוינו, והרווח המתאים יהיה בין השוליים הימניים לבין הנקודה עם התווית.
- אם לא מחילים את ה-span, אף אחד מההתנהגויות בהתאמה אישית לא יופיע.
אפשר לראות את ההטמעה של הבדיקות האלה בדוגמה של TextStyling ב-GitHub.
כדי לבדוק את האינטראקציות של Canvas, אפשר ליצור רמאות ל-Canvas, להעביר את האובייקט המזויף לשיטה drawLeadingMargin()
ולאמת שהשיטות הנכונות נקראות עם הפרמטרים הנכונים.
דוגמאות נוספות לבדיקות של span מפורטות ב-BulletPointSpanTest.
שיטות מומלצות לשימוש ב-spans
יש כמה דרכים לחסוך בזיכרון כשמגדירים טקסט ב-TextView
, בהתאם לצרכים שלכם.
איך מחברים או מנתקים קטע טקסט בלי לשנות את הטקסט שמתחתיו
TextView.setText()
מכיל כמה עומסי יתר שמטפלים ב-spans בצורה שונה. לדוגמה, אפשר להגדיר אובייקט טקסט מסוג 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
מתעדכן באופן אוטומטי בהתאם לשינוי בטקסט. אם משנים מאפיין פנימי של span קיים, צריך להפעיל את 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
.
שינוי מאפייני span פנימיים
אם אתם צריכים לשנות רק מאפיין פנימי של span שניתן לשינוי, כמו צבע הנקודה ב-span של נקודה בהתאמה אישית, תוכלו להימנע מהעלות הנוספת של קריאה ל-setText()
כמה פעמים על ידי שמירת הפניה ל-span בזמן היצירה שלו.
כשצריך לשנות את ה-span, אפשר לשנות את ההפניה ואז לבצע קריאה ל-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 מכיל גם פונקציות תוסף שמקלות על העבודה עם span. מידע נוסף זמין במסמכי העזרה של החבילה androidx.core.text.