SpannableString은 android.text 패키지의 Android SDK 클래스로, TextView에서 하나의 텍스트 문자열의 여러 부분에 다양한 스타일을 적용할 수 있게 해줍니다. HTML 마크업과 달리 SpannableString은 Span 객체 수준에서 작동하여 텍스트의 시각적 표시(색상, 크기, 글꼴, 밑줄, 대화형 요소)를 제어합니다. Google Developers에 따르면 SpannableString은 Android 시스템 구성 요소에서 링크 서식 지정에 사용됩니다. 타사 라이브러리 없이 텍스트 스타일을 지정하는 기본 방법입니다.
주요 사항
SpannableString은 Spannable 인터페이스를 구현하는 Android 클래스로, 시각적 표시를 제어하는 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(밑줄) 등이 포함됩니다. 문자 스팬은 지정된 범위의 각 문자에 개별적으로 적용됩니다.
ParagraphStyle은 전체 단락에 영향을 주는 스팬의 인터페이스입니다. 가장 잘 알려진 대표는 AlignmentSpan으로, 전체 단락을 왼쪽, 가운데 또는 오른쪽으로 정렬합니다. 단락 스팬은 전체 단락을覆盖해야 합니다. 그렇지 않으면 Android가 적용을 무시합니다. 이 제한은 정렬이나 들여쓰기가 전체 텍스트 블록에 대해서만 의미가 있기 때문입니다.
Span 플래그는 범위 경계에서 텍스트가 삽입되거나 삭제될 때 스팬의 동작을 정의하는 네 가지 상수입니다. SPAN_EXCLUSIVE_EXCLUSIVE는 원래 경계 내에서만 스팬을 활성 상태로 유지하고, SPAN_INCLUSIVE_INCLUSIVE는 경계에 텍스트가 추가될 때 확장합니다. SPAN_EXCLUSIVE_INCLUSIVE와 SPAN_INCLUSIVE_EXCLUSIVE는 범위의 시작과 끝에 대해 혼합 동작을 제공합니다.
| 플래그 | 왼쪽 삽입 | 오른쪽 삽입 |
|---|---|---|
| SPAN_EXCLUSIVE_EXCLUSIVE | 포함하지 않음 | 포함하지 않음 |
| SPAN_INCLUSIVE_INCLUSIVE | 포함함 | 포함함 |
| SPAN_EXCLUSIVE_INCLUSIVE | 포함하지 않음 | 포함함 |
| SPAN_INCLUSIVE_EXCLUSIVE | 포함함 | 포함하지 않음 |
올바른 플래그 선택은 사용자가 문자를 삽입하고 삭제할 수 있는 EditText의 Editable 텍스트에 중요합니다. 읽기 전용 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을 찾는 역할을 합니다. 색상 스팬은 링크를 사용자에게 visible하게 만듭니다.
SpannableString 작업은 텍스트 문자열에서 인스턴스를 생성하고 setSpan() 메서드를 통해 순차적으로 스팬을 적용하는 것으로 시작됩니다. 이 메서드는 스팬 객체, 시작 위치, 끝 위치 및 플래그의 네 가지 매개변수를 받습니다. 모든 스팬을 설정한 후 객체는 setText()를 통해 TextView에 전달됩니다.
첫 번째 단어가 빨간색이고 확대된 문자열을 만들어 보겠습니다. 이를 위해 색상에는 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는 렌더링 중에 모든 스팬을 자동으로 적용합니다. 추가 호출이 필요하지 않습니다. 1.5f 배수의 RelativeSizeSpan은 기본 대비 글꼴 크기를 50% 증가시킵니다.
Html.fromHtml()은 HTML 문자열에서 Spanned 객체를 생성하지만 지원되는 태그 세트는 제한적입니다. 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의 세 가지 스팬을 만들고 각각을 동일한 범위에 적용합니다. 각 스팬이 고유한 텍스트 속성을 담당하므로 적용 순서는 결과에 영향을 미치지 않습니다.
다른 유형의 스팬이 부분적으로 겹치는 경우 각 스팬은 자체 경계 내에서 독립적으로 작동합니다. 범위 0~10의 ForegroundColorSpan과 범위 5~15의 StyleSpan(BOLD)는 세그먼트 5~10에서 굵은 빨간색 텍스트를, 10~15에서 굵게만 표시합니다. 각 스팬이 렌더링 중에 고유한 속성을 수정하므로 충돌이 발생하지 않습니다.
getSpans(int start, int end, Class type) 메서드는 지정된 범위 내의 스팬 배열을 반환합니다. 이는 이미 적용된 스타일을 확인하거나 특정 스팬을 제거하는 데 유용합니다. nextSpanTransition()을 사용하면 스팬 변경 경계를 반복할 수 있습니다. 이는 스타일이 변경되는 위치를 알아야 하는 사용자 정의 TextView 구현의 기초입니다.
SpannableStringBuilder는 조각을 삽입, 교체 및 삭제할 수 있는 스타일 텍스트의 단계별 구축을 위한 클래스입니다. 준비된 문자열에서 생성되고 불변인 SpannableString과 달리 Builder는 텍스트 부분을 순차적으로 추가하고 즉시 스타일을 할당할 수 있습니다. 로그, 채팅, 동적 레이블이 있는 뉴스 헤드라인과 같은 복합 메시지에 이상적입니다.
Builder는 Spannable 및 Editable 인터페이스를 구현하여 EditText와 호환됩니다. 사용자는 텍스트를 편집할 수 있으며 스타일이 유지되고 새 문자가 삽입되면 올바르게 이동됩니다. SpannableString은 불변이므로 편집 가능한 필드에는 적합하지 않습니다.
append() 메서드는 빌더 자체를 반환하여 메서드 체이닝을 가능하게 합니다. 텍스트를 추가한 후 setSpan()을 통해 스팬이 적용됩니다. 위치는 현재 빌더 길이를 기준으로 지정됩니다. 더 정확한 콘텐츠 제어를 위해 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)은 변경할 때마다 레이아웃 재계산이 필요하므로 색상이나 밑줄 스팬보다 훨씬 비용이 많이 듭니다.
TextAppearanceSpan을 사용하면 단일 setSpan() 호출로 android:textAppearance XML 리소스에서 전체 스타일 세트를 적용할 수 있습니다. 세 개의 개별 스팬(색상, 크기, 글꼴) 대신 스타일 참조가 있는 단일 TextAppearanceSpan이 사용됩니다. 이렇게 하면 객체 수가 줄어들고 유지 관리가 간소화됩니다. 리소스의 스타일을 변경하면 이 스팬을 사용하는 모든 텍스트에 자동으로 적용됩니다.
각 setSpan()에 대해 새 스팬 인스턴스를 생성하면 가비지 컬렉터에 추가 부하가 발생합니다. 반복적으로 사용되는 경우 상수 스팬 객체를 만드는 것이 최적입니다. 예를 들어 ForegroundColorSpan(Color.RED)는 companion 객체에 저장하고 재사용할 수 있습니다. 그러나 상태가 있는 스팬(다른 핸들러가 있는 ClickableSpan)은 각 경우에 대해 개별적으로 생성해야 합니다.
스팬 성능 문제를 진단하려면 Android Studio의 Layout Inspector와 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에서 작동하지만 편집 가능한 텍스트에는 Editable을 구현하는 SpannableStringBuilder가 선호됩니다. EditText는 변경 사항을 추적하기 위해 Editable 인터페이스가 필요합니다. SpannableString을 EditText에 전달하면 스타일과 함께 텍스트가 표시되지만 편집 중에 Android가 이를 Editable로 변환하여 일부 스팬이 재설정될 수 있습니다.
Jetpack Compose에서는 Android SDK 스팬이 직접 사용되지 않습니다. 대신 Compose는 유사한 기능을 가진 SpannableString의 자체 equivalent인 AnnotatedString을 제공합니다. 개별 문자 스타일에는 SpanStyle, 단락에는 ParagraphStyle을 사용합니다. SpannableString을 AnnotatedString으로 변환하는 것은 스팬을 반복하는 buildAnnotatedString을 통해 가능합니다.
CharacterStyle을 확장하고 updateDrawState(TextPaint tp) 메서드를 재정의하는 클래스를 만듭니다. 메서드 내에서 TextPaint 속성(색상, 선 두께, 효과)을 수정합니다. 미터법 변경에는 UpdateLayout 또는 MetricAffectingSpan을 사용합니다. 사용자 정의 스팬은 내장 스팬과 마찬가지로 setSpan()을 통해 적용됩니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.