תגי Span הם אובייקטים רבי-עוצמה של סימון שאפשר להשתמש בהם כדי לעצב טקסט ברמת התו או הפסקה. על ידי צירוף של span לאובייקטים של טקסט, אפשר לשנות את הטקסט במגוון דרכים, כולל הוספת צבע, הפיכת הטקסט לטקסט שאפשר ללחוץ עליו, שינוי גודל הטקסט וציור הטקסט בצורה מותאמת אישית. אפשר גם להשתמש ב-Span כדי לשנות מאפיינים של TextPaint, לצייר על Canvas ולשנות את פריסת הטקסט.
ב-Android יש כמה סוגים של טווחים שמכסים מגוון דפוסים נפוצים של עיצוב טקסט. אפשר גם ליצור טווחים משלכם כדי להחיל סגנון מותאם אישית.
יצירה והחלה של טווח
כדי ליצור טווח, אפשר להשתמש באחת מהמחלקות שמפורטות בטבלה הבאה. ההבדלים בין המחלקות תלויים בשאלה אם הטקסט עצמו ניתן לשינוי, אם תגי העיצוב של הטקסט ניתנים לשינוי ובאיזה מבנה נתונים בסיסי מאוחסנים נתוני הטווח.
| דרגה | טקסט שניתן לשינוי | תגי עיצוב שניתנים לשינוי | מבנה הנתונים |
|---|---|---|---|
SpannedString |
לא | לא | מערך לינארי |
SpannableString |
לא | כן | מערך לינארי |
SpannableStringBuilder |
כן | כן | עץ אינטרוולים |
כל שלוש המחלקות מרחיבות את הממשק Spanned. SpannableString וגם SpannableStringBuilder מרחיבים את הממשק של Spannable.
כך מחליטים באיזה פורמט להשתמש:
- אם לא משנים את הטקסט או את תגי העיצוב אחרי היצירה, משתמשים ב-
SpannedString. - אם אתם צריכים לצרף מספר קטן של תגי span לאובייקט טקסט יחיד והטקסט עצמו הוא לקריאה בלבד, השתמשו בתג
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 שאפשר להחיל על תווים בודדים, ויש תגי 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 שמשפיע על מדדי הטקסט גורמת לאובייקט שמתבצעת בו מדידה למדוד מחדש את הטקסט כדי שהפריסה והעיבוד יהיו נכונים – לדוגמה, שינוי גודל הטקסט עשוי לגרום למילים להופיע בשורות שונות. החלת התג span שלמעלה מפעילה מדידה מחדש, חישוב מחדש של פריסת הטקסט וציור מחדש של הטקסט.
טווחים שמשפיעים על מדדי טקסט מרחיבים את המחלקה MetricAffectingSpan, מחלקה מופשטת שמאפשרת למחלקות משנה להגדיר איך הטווח משפיע על מדידת הטקסט על ידי מתן גישה אל TextPaint. MetricAffectingSpan extends
CharacterStyle, ולכן מחלקות משנה משפיעות על מראה הטקסט ברמת התו.
תגי span שמשפיעים על פסקאות
תג 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
extends 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.
שיטות מומלצות לשימוש בתגי span
יש כמה דרכים להגדיר טקסט ב-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 נעשים בכוונה – אי אפשר לעדכן את האובייקט המקורי של 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 כולל גם פונקציות הרחבה שמקלות על העבודה עם spans. מידע נוסף זמין בתיעוד של חבילת androidx.core.text.