תגי span הם אובייקטים רבי-עוצמה של סימון שאפשר להשתמש בהם כדי לעצב טקסט ברמת התו או הפסקה. באמצעות צירוף של טווחים לאובייקטים של טקסט, אפשר לשנות את הטקסט במגוון דרכים, כולל הוספת צבע, הפיכת הטקסט לטקסט שאפשר ללחוץ עליו, שינוי גודל הטקסט וציור הטקסט בצורה מותאמת אישית. אפשר גם להשתמש ב-Span כדי לשנות מאפיינים של 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 מציינים את החלק בטקסט שעליו אתם מחילים את הטווח.
אם מוסיפים טקסט בתוך הגבולות של span, ה-span מתרחב באופן אוטומטי כדי לכלול את הטקסט שנוסף. כשמוסיפים טקסט בגבולות של טווח, כלומר במדדי ההתחלה או הסוף, הפרמטר 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 סוגים של span בחבילה android.text.style. Android מסווג טווחים בשתי דרכים עיקריות:
- איך התג span משפיע על הטקסט: התג span יכול להשפיע על מראה הטקסט או על מדדי הטקסט.
- היקף ההדגשה: יש הדגשות שאפשר להחיל על תווים בודדים, ויש הדגשות שאפשר להחיל רק על פסקה שלמה.

בקטעים הבאים מפורטות הקטגוריות האלה.
תגי span שמשפיעים על מראה הטקסט
חלק מהתגים שמוגדרים ברמת התו משפיעים על מראה הטקסט, למשל
שינוי צבע הטקסט או הרקע והוספה של קו תחתון או קו חוצה. הטווחים האלה מרחיבים את המחלקה 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
.
תגי span שמשפיעים רק על מראה הטקסט מפעילים ציור מחדש של הטקסט בלי להפעיל חישוב מחדש של הפריסה. הטווחים האלה מיישמים את UpdateAppearance
ומרחיבים את CharacterStyle
.
מחלקות משנה של CharacterStyle
מגדירות איך לצייר טקסט על ידי מתן גישה לעדכון TextPaint
.
תגי span שמשפיעים על מדדי הטקסט
טווחים אחרים שחלים ברמת התו משפיעים על מדדי טקסט, כמו גובה השורה וגודל הטקסט. הטווחים האלה מרחיבים את המחלקה 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 שלמעלה מפעילה מדידה מחדש, חישוב מחדש של פריסת הטקסט וציור מחדש של הטקסט.
טווחים שמשפיעים על מדדי טקסט מרחיבים את המחלקה MetricAffectingSpan
, מחלקה מופשטת שמאפשרת למחלקות משנה להגדיר איך הטווח משפיע על מדידת הטקסט על ידי מתן גישה ל-TextPaint
. MetricAffectingSpan
extends
CharacterStyle
, ולכן מחלקות משנה משפיעות על מראה הטקסט ברמת התו.
תגי span שמשפיעים על פסקאות
תג 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
חל על פסקה.
יצירת טווחים מותאמים אישית
אם אתם צריכים פונקציונליות נוספת מעבר למה שמוצע בטווחים הקיימים של Android, אתם יכולים להטמיע טווח מותאם אישית. כשמטמיעים טווח משלכם, צריך להחליט אם הטווח משפיע על הטקסט ברמת התו או ברמת הפסקה, וגם אם הוא משפיע על הפריסה או על המראה של הטקסט. כך תוכלו להבין אילו מחלקות בסיס אפשר להרחיב ואילו ממשקים צריך להטמיע. הטבלה הבאה יכולה לשמש כהפניה:
תרחיש | כיתה או ממשק |
---|---|
הטווח משפיע על הטקסט ברמת התו. | CharacterStyle |
התג span משפיע על מראה הטקסט. | UpdateAppearance |
הטווח משפיע על מדדי הטקסט. | UpdateLayout |
הטווח משפיע על הטקסט ברמת הפסקה. | ParagraphStyle |
לדוגמה, אם אתם צריכים להטמיע טווח מותאם אישית שמשנה את גודל הטקסט ואת הצבע, צריך להרחיב את RelativeSizeSpan
. באמצעות ירושה, המחלקה RelativeSizeSpan
מרחיבה CharacterStyle
ומיישמת את שני הממשקים Update
. מכיוון שהמחלקות האלה כבר מספקות קריאות חוזרות ל-updateDrawState
ול-updateMeasureState
, אפשר לבטל את הקריאות החוזרות האלה כדי להטמיע התנהגות מותאמת אישית. הקוד הבא יוצר טווח מותאם אישית שמרחיב את RelativeSizeSpan
ומבטל את הקריאה החוזרת (callback) של 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
כוללת תג 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.
בדיקת טווחים מותאמים אישית
כשבודקים טווחים, צריך לוודא שהתג TextPaint
מכיל את השינויים הצפויים ושהרכיבים הנכונים מופיעים בתג Canvas
. לדוגמה, נניח שאתם מטמיעים טווח מותאם אישית שמוסיף תבליט לפני טקסט מסוים. לנקודה יש גודל וצבע מוגדרים, ויש רווח בין השוליים הימניים של האזור שאפשר לצייר בו לבין הנקודה.
כדי לבדוק את ההתנהגות של המחלקה הזו, אפשר להטמיע בדיקת AndroidJUnit ולבדוק את הדברים הבאים:
- אם תחילת הטווח וסופו מוגדרים בצורה נכונה, יופיע בבד הציור תבליט בגודל ובצבע שצוינו, ויהיה רווח מתאים בין השוליים הימניים לבין התבליט.
- אם לא תגדירו את טווח הזמן, לא תופיע אף אחת מההתנהגויות המותאמות אישית.
אפשר לראות את ההטמעה של הבדיקות האלה בדוגמה TextStyling ב-GitHub.
כדי לבדוק אינטראקציות עם Canvas, אפשר ליצור אובייקט מדומה של 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
מתעדכן אוטומטית בהתאם לשינוי בטקסט. אם משנים מאפיין פנימי של 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 של תבליט מותאם אישית, אתם יכולים להימנע מהתקורה של קריאה ל-setText()
כמה פעמים על ידי שמירת הפניה ל-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(); } }); } }
שימוש בפונקציות הרחבה של Android KTX
Android KTX כולל גם פונקציות הרחבה שמקלות על העבודה עם טווחים. מידע נוסף זמין במסמכי העזרה בנושא חבילת androidx.core.text.