본문 바로가기

ANDROID/Jetpack

[Android/Jetpack Compose] Spannable string 처리하기

기존의 xml 베이스의 레이아웃을 Jetpack compose로 전환하면서 특정 부분의 텍스트에 스타일과 클릭 이벤트를 줘야 하는 Spannable 처리가 필요했다. 

 

Spannable 처리를 통해 부분적으로 string 스타일을 바꿀 수 있을 뿐더러, 클릭시 url 오픈 등의 가이드 액션도 적용할 수 있다. 

 

이를 위해서 사용한 API는 AnnotatedString이다.

 

누군가는 string을 각각 append하는 방식으로 spannable 처리를 하기도 했는데, 다국어 및 리소스 관리에서 불편함이 많을 것 같아서,

 

일단 기본 문장을 넣어놓고, spannable 처리를 하고자 하는 단어들을 별도로 선별해 스타일 및 클릭 이벤트를 처리하도록 적용했다. 

 

1. AnnotatedString을 만드는 Composable 함수

AnnotatedString을 만드는데 다수의 코드가 필요하기 때문에 별도 함수로 추출해 쓰기로 했다. 

@Composable
fun GetAnnotatedTerms(): AnnotatedString {

    // spannable을 적용할 단어 및 단어별 가이드하고자 하는 scheme 선언 
    val terms = stringResource(id = R.string.terms_of_service)
    val privacyPolicy = stringResource(id = R.string.privacy_policy)
    val termsUrl = stringResource(id = R.string.terms_of_service_url)
    val privacyPolicyUrl = stringResource(id = R.string.privacy_policy_url)

    // 어노테이션을 적용할 원본 string
    val fullString = stringResource(id = R.string.login_terms_guide)

    // 원본 string으로부터 따온 spannable 대상 단어의 시작 인덱스와 length
    val (termsStartIndex, termsLength) = fullString.indexOf(terms) to terms.length
    val (privacyStartIndex, privacyLength) = fullString.indexOf(privacyPolicy) to privacyPolicy.length

    return buildAnnotatedString {
        
        // buildAnnotatedString 블럭에 원본 string을 우선 붙인다.
        append(fullString)
        
        // 먼저 따온 인덱스 값을 이용해 각 spannable 대상에 필요한 스타일을 적용한다. 
        addStyle(
            style = SpanStyle(textDecoration = TextDecoration.Underline),
            start = termsStartIndex,
            end = termsStartIndex + termsLength
        )
        addStyle(
            style = SpanStyle(textDecoration = TextDecoration.Underline),
            start = privacyStartIndex,
            end = privacyStartIndex + privacyLength
        )
        
        // 각 spannable 대상에 annotation 값을 태그와 함께 선언한다. 
        // 이는 스타일과 별개로 클릭 이벤트가 발생했을 때 처리를 위해 필요함. 
        addStringAnnotation(
            tag = terms,
            annotation = termsUrl,
            start = termsStartIndex,
            end = termsStartIndex + termsLength
        )
        addStringAnnotation(
            tag = privacyPolicy,
            annotation = privacyPolicyUrl,
            start = privacyStartIndex,
            end = privacyStartIndex + privacyLength
        )

		// 빌더 패턴의 함수이기 때문에 toAnnotatedString으로 AnnotatedString을 build해 마무리한다.
        toAnnotatedString()
    }
}

2. AnnotatedString을 사용하는 ClickableText 사용하기

// 해당 프로젝트에서는 onClick 이벤트를 최상위 composable까지 전달하기 위해 별도 listener를 사용한다.
val actionListener: MyActionListener = MyActionListener() 

ClickableText(text = annotatedString,
                modifier = Modifier
                    .fillMaxWidth(),
                onClick = { offset ->
                    annotatedString.getStringAnnotations(offset, offset)
                        .firstOrNull()?.let { span ->
                           
                            // span으로부터 tag와 annotation을 읽을 수 있기 때문에,
                            // 최종으로 이벤트를 수신하는 대상이 이벤트의 종류와 수행해야 할 명령을 손쉽게 처리가 가능하다. 
                            actionListener.onClick(AnnotationClicked(tag = span.tag, annotation = span.item))
                        }
                })

 

관련 처리를 할 때 xml 베이스의 레이아웃은 좀 더 손이 가고 지저분한 코드가 만들어졌던 것 같은데, 새삼스럽지만 다시금 Jetpack Compose의 시대에 박수를 👏

 

 

AnnotatedString.Builder  |  Android Developers

androidx.car.app.managers

developer.android.com

 

 

Clickable SpannableText in Jetpack Compose

Previously I have posted an article how we can span a text in jetpack compose . See here

medium.com