SpannableString је класа Android SDK из пакета android.text која омогућује примену више стилова на различите делове једног текста у TextView. За разлику од HTML ознака, SpannableString ради на нивоу објеката Span, управљајући визуелним приказом текста: бојом, величином, фонтом, подвлачењем и интерактивним елементима. Према Google Developers, SpannableString се користи у системским компонентама Android-а за форматирање веза. То је основни начин стилизације текста без повезивања додатних библиотека.
Главно
SpannableString је класа Android-а која имплементира интерфејс Spannable, што чува текст заједно са скупом објеката Span који управљају визуелним приказом. За разлику од обичног String-а, SpannableString омогућује прикачивање атрибута стила на одређене опсеге знакова: направите део текста црвеним, повећајте фонт у наслову или додајте линк који се може кликнути унутар пасуса.
Класа се налази у пакету android.text и доступна је од API Level 1. SpannableString је непромењив — након креирања, његова структура се фиксира, а замена текста захтева креирање новог објекта. За динамичко уређивање користи се SpannableStringBuilder, који подржава уметање и брисање знакова без губитка стилова.
CharSequence је основни интерфејс за текстуалне податке, који имплементирају String, StringBuilder и SpannableString. Основна разлика између SpannableString-а и String-а је у подршци за прикачивање произвољних објеката на подстринг. TextView препознаје интерфејс Spannable и приликом исцртавања примењује објекте спана на одговарајуће делове текста. Ако се обичан String проследи TextView-у, никакви стилови неће бити примењени.
SpannableString чува текст као низ char[] и засебни низ објеката спана са метаподацима о почетној и крањој позицији. Приликом позива setSpan(what, start, end, flags), објекат what се чува у листи заједно са информацијама о опсегу. Приликом исцртавања, TextView узастопно примењује све спанове који се налазе у приказаном опсегу, позивајући методе updateDrawState и updateMeasureState.
CharacterStyle је основна класа за спанове који утичу на појединачне знакове, без обзира на њихов положај у редовима. Овде спадају ForegroundColorSpan (боја текста), RelativeSizeSpan (релативна величина), StyleSpan (подебљано и косо), UnderlineSpan (подвлачење) и други. Character-спанови се примењују на сваки знак у наведеном опсегу појединачно.
ParagraphStyle је интерфејс за спанове који утичу на целе пасусе. Најпознатији представник је AlignmentSpan, који поравнава цео пасус улево, по центру или удесно. Paragraph-спанови морају да покрију цео пасус, иначе Android занемарује њихову примену. Ово ограничење је због тога што поравнање или увлачење има смисла само за цео блок текста.
Span flags су четири константе које одређују понашање спана приликом уметања или брисања текста на границама његовог опсега. SPAN_EXCLUSIVE_EXCLUSIVE задржава спан активним само унутар изворних граница, SPAN_INCLUSIVE_INCLUSIVE га проширује приликом додавања текста на границе. SPAN_EXCLUSIVE_INCLUSIVE и SPAN_INCLUSIVE_EXCLUSIVE пружају мешовито понашање за почетак и крај опсега.
| Флаг | Уметање са леве | Уметање са десне |
|---|---|---|
| SPAN_EXCLUSIVE_EXCLUSIVE | не укључује | не укључује |
| SPAN_INCLUSIVE_INCLUSIVE | укључује | укључује |
| SPAN_EXCLUSIVE_INCLUSIVE | не укључује | укључује |
| SPAN_INCLUSIVE_EXCLUSIVE | укључује | не укључује |
Правилан одабир флага је критичан за Editable текст у EditText, где корисник може да умеће и брише знакове. За TextView само за читање обично се користи SPAN_EXCLUSIVE_EXCLUSIVE — стил се примењује само на изворни опсег и не проширује се приликом програмских промена.
Android SDK пружа више од 25 уграђених класа спанова, који покривају већину задатака стилизације текста. Свака класа имплементира интерфејс CharacterStyle или ParagraphStyle и прихвата параметре кроз конструктор. Све класе се налазе у пакету android.text.style и доступне су без повезивања додатних зависности.
ForegroundColorSpan поставља боју текста за наведени опсег, прихватајући боју у формату int. BackgroundColorSpan боји позадину испод текста, што је корисно за истицање претражних упита. AbsoluteSizeSpan поставља тачну величину фонта у пикселима, RelativeSizeSpan — множилац у односу на основну величину текста у TextView.
StyleSpan прихвата константе Typeface.NORMAL, Typeface.BOLD, Typeface.ITALIC или BOLD_ITALIC и мења изглед знакова. UnderlineSpan додаје подвлачење, StrikethroughSpan — прецртавање. SuperscriptSpan и SubscriptSpan стварају надредне и подредне индексе. TypefaceSpan омогућује постављање прилагођеног фонта кроз објекат Typeface за опсег текста.
ClickableSpan је апстрактна класа за креирање делова текста који се могу кликнути. Приликом притиска позива се метода onClick(). Да би кликови радили, TextView мора да има setMovementMethod(LinkMovementMethod.getInstance()). URLSpan је подкласа ClickableSpan-а за хипервезе са аутоматским отварањем браузера. ClickableSpan се често комбинује са ForegroundColorSpan да би се веза визуелно истакла плавом бојом.
val spannable = SpannableString("Open developer.android.com")
spannable.setSpan(
URLSpan("https://developer.android.com"),
9, 31, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
spannable.setSpan(
ForegroundColorSpan(Color.BLUE),
9, 31, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
textView.text = spannable
textView.movementMethod = LinkMovementMethod.getInstance()
Без LinkMovementMethod кликови на URLSpan неће бити обрађени. MovementMethod је одговоран за пресретање додирних догађаја и проналажење ClickableSpan-а на позицији додира. Обојени спан чини везу видљивом за корисника.
Рад са SpannableString почиње креирањем инстанце из текстовног низа и узастопним примењивањем спанова кроз методу setSpan(). Метода прихвата четири параметра: објекат спана, почетну позицију, крању позицију и флагове. Након постављања свих спанова, објекат се прослеђује TextView-у кроз setText().
Направићемо низ где ће прва реч бити црвена и веће величине. За ово се користи ForegroundColorSpan за боју и RelativeSizeSpan за размеру. Оба спана се примењују на исти опсег независно један од другог — редослед позива setSpan није битан.
val text = "Header: remaining text"
val spannable = SpannableString(text)
val colorSpan = ForegroundColorSpan(Color.RED)
val sizeSpan = RelativeSizeSpan(1.5f)
spannable.setSpan(colorSpan, 0, 9, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
spannable.setSpan(sizeSpan, 0, 9, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
textView.text = spannable
Знакови са позиције 0 до 9 ће добити оба стила истовремено. TextView аутоматски примењује све спанове приликом исцртавања — нису потребни додатни позиви. RelativeSizeSpan са множиоцем 1.5f ће повећати величину фонта за 50% у односу на основну.
Html.fromHtml() ствара Spanned објекат из HTML низа, али је скуп подржаних ознака ограничен. SpannableString пружа потпуну контролу над сваким атрибутом без ограничења HTML-а. Ако је потребно претворити HTML у спанове са накнадним додавањем прилагођених стилова, може се користити Html.fromHtml() као основа, а затим допунити спановима кроз setSpan().
val htmlText = Html.fromHtml(
"<b>Important:</b> check the data",
Html.FROM_HTML_MODE_LEGACY
)
val spannable = SpannableString(htmlText)
spannable.setSpan(
ForegroundColorSpan(Color.RED),
0, 6, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
textView.text = spannable
Резултат — текст „Важно:“ ће бити подебљан (из HTML-а) и црвен (из спана). Овакав приступ је згодан за рад са серверским садржајем, где је део форматирања постављен кроз HTML, а део се додаје програмски на клијентској страни.
SpannableString омогућује примену неограниченог броја спанова на исти или преклапајући се опсег. Комбиновање спанова је кључна предност у односу на HTML ознаке, где угњеждени ознаци могу да дођу у сукоб. Спанови су независни и примењују се узастопно приликом исцртавања.
На пример, може се направити део текста истовремено подебљан, црвен и кљикабилан. За ово се креирају три спана — StyleSpan, ForegroundColorSpan и ClickableSpan — и сваки се примењује на исти опсег. Редослед примене не утиче на резултат, јер сваки спан је одговоран за своју карактеристику текста.
Ако се спанови различитих типова делимично преклапају, сваки ради независно у својим границама. ForegroundColorSpan на опсегу 0-10 и StyleSpan(BOLD) на опсегу 5-15 даће црвени подебљан текст на делу 5-10 и само подебљан на 10-15. Не настају конфликти, јер сваки спан мења свој атрибут приликом исцртавања.
Метода getSpans(int start, int end, Class type) враћа низ спанова који се налазе у наведеном опсегу. Ово је корисно за проверу који стилови су примењени или за уклањање одређених спанова. Са nextSpanTransition() може се итерирати по границама промене спанова — ово је основа рада прилагођених TextView-а који морају да знају где се стил мења.
SpannableStringBuilder је класа за корак по корак изградњу стилизованог текста са могућношћу уметања, замене и брисања делова. За разлику од SpannableString-а, који се креира из готовог низа и је непромењив, Builder омогућује узастопно додавање делова текста и додељивање стилова у ходу. То је идеалан избор за сложене поруке: логове, четове, новинске наслове са динамичким ознакама.
Builder имплементира интерфејсе Spannable и Editable, што га чини компатибилним са EditText. Корисник може да уређује текст, а стилови се чувају и правилно померају приликом уметања нових знакова. SpannableString, будући непромењив, није погодан за поља за уређивање.
Метода append() враћа сам builder, омогућући изградњу ланца позива. Након додавања текста, спанови се примењују кроз setSpan(). Позиције се наводе у односу на тренутну дужину builder-а. Insert() и replace() такође су доступни за прецизнију контролу над садржајем.
val builder = SpannableStringBuilder()
.append("New ")
.append("comment")
val blue = ForegroundColorSpan(Color.BLUE)
val gray = ForegroundColorSpan(Color.GRAY)
builder.setSpan(blue, 0, 6, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
builder.setSpan(gray, 6, 17, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
textView.text = builder
Текст „Нови “ је обојен плавом, а „коментар“ сивом. Приликом уметања додатних знакова између њих, спан неће утицати на нови текст захваљујући флаговима EXCLUSIVE. Builder аутоматски коригује унутрашње индексе приликом измене.
Коришћење великог броја спанова утиче на перформансе исцртавања TextView. Сваки спан позива методу updateDrawState() или updateMeasureState() при сваком поновном исцртавању. Препоручује се ограничавање броја спанова по једном TextView-у на 50–100 за комфортан рад на уређајима средњег сегмента. Спанови који мењају величину текста (RelativeSizeSpan, AbsoluteSizeSpan) захтевају прерачунавање layout-а при свакој промени, што је значајно скупље од спанова само за боју или подвлачење.
TextAppearanceSpan омогућује примену целог скупа стилова из xml ресурса android:textAppearance једном операцијом setSpan(). Уместо три одвојена спана (боја, величина, фонт) користи се један TextAppearanceSpan са референцом на стил. Ово смањује број објеката и поједностављује одржавање — промена стила у ресурсу се аутоматски примењује на сав текст који користи овај спан.
Креирање нове инстанце спана за сваки setSpan() доводи до додатног оптерећења сакупљача смећа. Оптимално је креирати константне објекте спанова ако се вишекратно примењују. На пример, ForegroundColorSpan(Color.RED) се може сачувати у companion object-у и поново користити. Међутим, спанови са стањем (ClickableSpan са различитим руководиоцима) морају се креирати појединачно за сваки случај.
За дијагностику проблема перформанси спанова користите Layout Inspector у Android Studio-у и GPU профилер. Ако TextView са великим бројем спанова приметно успорава приликом скроловања, размотрите замену дела спанова статичким стиловима кроз TextAppearanceSpan или смањење броја спанова комбиновањем атрибута у прилагођеним имплементацијама UpdateAppearance.
Често постављана питања
String је непромењив низ знакова без подршке за стилове. SpannableString чува исте знакове, али додатно садржи низ објеката Span са информацијама о форматирању. TextView одређује тип прослеђеног CharSequence-а и примењује спанове на одговарајуће опсеге приликом исцртавања. String занемарује све атрибуте стила и приказује се као обичан текст.
Метода removeSpan(Object span) уклања одређени спан. За потпуно чишћење позовите getSpans(0, length, Object::class.java), који враћа низ свих спанова, затим сваки уклоните кроз removeSpan. Алтернативно направите нови SpannableString(text.toString()) без спанова. SpannableStringBuilder има методу clear() која уклања и текст и спанове.
SpannableString ради у EditText-у, али за уређиви текст је пожељнији SpannableStringBuilder који имплементира Editable. EditText захтева Editable интерфејс за праћење промена. Ако се SpannableString проследи EditText-у, текст ће се приказати са стиловима, али ће га Android приликом уређивања претворити у Editable, што може да ресетује део спанова.
У Jetpack Compose-у се спанови Android SDK-а не користе директно. Уместо тога, Compose пружа AnnotatedString — сопствени еквивалент SpannableString-а са сличним могућностима: SpanStyle за стилове појединачних знакова и ParagraphStyle за пасусе. Конверзија SpannableString-а у AnnotatedString је могућа кроз buildAnnotatedString са итерацијом по спановима.
Направите класу која наслеђује CharacterStyle и прегазите методу updateDrawState(TextPaint tp). Унутар методе измените својства TextPaint-а: боју, дебљину линије, ефекте. За метричке промене користите UpdateLayout или MetricAffectingSpan. Прилагођени спанови се примењују кроз setSpan() исто као и уграђени.
Резиме
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също