[Android] EditText 실시간으로 특정 부분의 글자색 변경하기. (텍스트 하이라이트)

2021. 10. 4. 15:15Android

반응형

EditText를 사용해 사용자로부터 글자를 입력받을 때, 사용자가 글자를 입력하는 동안 특정 로직에 의해 특정 부분의 글자만 색상을 바꿔줘야 할 때가 있다. 이를 구현하는 방법을 소개한다. 

 

목차

     


     

    결과물

    editText 의 끝에 세 글자만 노란색으로 하이라이트를 하는 로직을 적용했을 경우에 대한 결과물이다. 

    실시간 edittext 하이라이트 예시

     

     

     

     

    1. TextView 또는 EditText 의 특정 글자색 변경하기

    우리는 TextView/EditText 를 사용할 때 setTextColor() 메서드를 통해 글자색을 지정한다. 

    그런데, 가끔 해당 컴포넌트 내에서도 특정 부분의 글자만 색을 변경해야 할 때가 있다. 예를 들어 특정 문구를 하이라이트를 통해 강조할 경우 등. 

    SpannableStringBuilder.setSpan() 를 통해 쉽게 구현할 수 있다.

     

    예시를 통해 코드를 살펴보자.

    textView 에 "코틀린 좋아요! 진짜 좋아요"라는 문구를 넣을 건데, 여기서 끝에 세글자인 '좋아요' 에만 노란색으로 하이라이트를 하고 싶다.

    val text = "코틀린 좋아요! 진짜 좋아요"
    val spannableStringBuilder = SpannableStringBuilder(text)
    
    /*
    * [text]의 마지막 index.
    * endExclusive index 이기 때문에, 사실은 마지막 index 가 아닌, (마지막 index + 1) 값이다.
    * 즉, [text.length] 와 동일한 값.
    */
    val endIndexExclusive = text.length
    
    val startIndex = endIndexExclusive - 3
    
    // startIndex ~ endIndexExclusive 에 어떤 색상을 입혀주세요~ 라는 역할을 한다.
    spannableStringBuilder.setSpan(
    	ForegroundColorSpan(getColor(R.color.colorPrimaryDark)),
    	startIndex,
    	endIndexExclusive,
    	Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
    )
    
    // 위에서 span 을 입힌 [spannableStringBuilder] 을 textView.text 에 넣어준다
    binding.textView.text = spannableStringBuilder

     

    여기서 주의해야할 점은, setSpan 에 들어가는 endIndex 값은 exclusive 값이라는 것. 

    즉, 아래와 같이 구하면 된다. 하이라이트 하려는 문구의 startIndex 에 해당 문구의 길이만큼을 더해주면 되는 것!

    • endIndexExclusive = startIndex + {하이라이트 할 문구}.length 

     

     

     

    2. EditText 입력 시 실시간으로 글자색 변경하기 

    1번을 통해 EditText 에서 특정 글자색만 변경하는 법을 배웠다. 그렇다면, EditText 에서 사용자가 글자를 입력할 때, 실시간으로 특정 조건에 부합하는 글자의 색을 변경해보자.

     

    방법

    1. EditText 의 TextWatcher 를 통해 실시간으로 변경되는 텍스트 값을 받아온다
    2. 하이라이트 하려는 글자(색상을 변경하려는 글자)의 startIndex, endIndexExclusive 값을 구한다
    3. editText.getText().setSpan() 을 통해 span 처리를 해준다. Editable 의 setSpan() 을 사용하는 것. 

    끝! 정말 쉽다.

     

    예시

    상단의 결과물 처럼 editText 의 끝에 세 글자만 노란색으로 하이라이트를 하고 싶다. 

     

    우선, 아래와 같이 editText 에 TextWatcher 를 추가해준다. 

    // editText 에 textWatcher 추가 
    binding.editText.addTextChangedListener(object : TextWatcher {
        ...
        
        override fun afterTextChanged(s: Editable?) {
        	// 새로 글자가 추가될 때마다 로직 체크를 통해 조건에 맞는 글자만 하이라이트처리 해줌.
        	s?.let { highlightText(it) }
        }
    })

     

    editText에 글자가 변경될 때 마다 아래의 highlightText() 를 통해 조건에 맞는 글자만 하이라이트 해준다. 

    이때, setSpan() 으로 하이라이트를 해주기 전에, removeSpan() 을 통해 기존에 추가된 span 들을 제거해줘야 한다. 제거해 주지 않으면, 이전에 설정해놓은 문구에 그대로 색상이 적용되기 때문. 

     

    그 후에 끝에 3글자만 하이라이트 되도록, binding.editText.text.setSpan() 처리를 해준다. 

    private fun highlightText(text: Editable) {
        // 기존에 설정해놓은 span 제거
        binding.editText.text?.let { editable ->
        	// binding.editText.text 에 추가된 모든 span 들을 리스트로 얻는다. 
            val spans = editable.getSpans(0, editable.length, ForegroundColorSpan::class.java)
    
            spans.forEach { span ->
            	editable.removeSpan(span)
            }
        }
    
        // editText의 끝에 세글자만 span 처리를 통해 색을 입힌다.
        // 이 로직은 예시일 뿐이고, 각자 원하는 로직대로 하이라이트 하려는 문구의 start/end index만 구해주면 된다.
        val endIndex = text.length
        val startIndex = if (endIndex < 3) 0 else (endIndex - 3)
    
        binding.editText.text?.setSpan(
            ForegroundColorSpan(getColor(R.color.colorPrimaryDark)),
            startIndex,
            endIndex,
            Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
        )
    }

     

    그럼 끝!! 여기까지만 해주면, 위에 올린 영상처럼 실시간으로 특정 글자만 하이라이트 된다. 

    이렇게 보면 굉장히 간단한데, 이렇게 하기까지 트러블 슈팅 과정이 있었다...

     

     

     

     

    🚧 Trouble Shooting 

    1️⃣ Editable.setSpan() 을 몰랐어..

    처음에는 editText.getText().setSpan() 의 존재를 몰랐다. 즉, Editable 에 setSpan 메소드가 있는 줄 몰랐던 거지.

    그래서 어떻게 했냐? 내가 알았던 방식은 위에 1번 뿐이었다. 즉, 나는 SpannableStringBuilder 를 통해서만 setSpan 을 해줄 수 있는 줄!!!!!!! 

    그래서 글자가 실시간으로 변경될 때마다 SpannableStringBuilder를 생성하고 editText.setText 처리를 해줬다... 아래와 같이.. (물론 아래처럼 완전 실시간은 아니고 debounce 처리를 해주긴 했는데 여기서는 관련이 없기 때문에 생략)

    binding.editText.addTextChangedListener(object : TextWatcher {
        ... 
        
        override fun afterTextChanged(s: Editable?) {
            val spannableStringBuilder = SpannableStringBuilder(s?.toString() ?: "")
    
            spannableStringBuilder.setSpan(
                ForegroundColorSpan(getColor(R.color.colorPrimaryDark)),
                startIndex,
                endIndexExclusive,
                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
            )
            
            // 나는 멍청이 ~~ twit twit
            binding.editText.text = spannableStringBuilder
        }
    })

     

    😖 이렇게 하면 어떤 문제가 발생하냐?

    사용자가 입력하는 도중에 setText() 해주기 때문에, 아래와 같은 문제들이 발생한다. 이 외에도 다양한 이슈들이 존재.

    • 한글의 경우, 쌍자음/받침 등이 있는데 이를 다 입력하는데는 시간이 좀 걸리는데(1-2초 정도?), 이걸 입력하는 와중에 setText 가 되어버리니, '쌉'을 느리게 입력할 경우 ㅆㅏㅂ 같이 입력이 되는 경우도 있고,,
    • 특수문자를 입력하기 위해 키보드에서 시프트를 눌러 특수문자 입력 키보드가 되었는데, setText() 가 되면 키보드가 초기화됨. 

     

    2️⃣ Editable.clearSpans()  대신, Editable.removeSpan() 을 사용한 이유

    위에 코드를 보면, editable.getSpans(0, editable.length, ForegroundColorSpan::class.java) 를 통해 editText 에 적용된 span 들을 구하고 이를 각각 removeSpan() 을 통해 제거해줬다.

    그런데, Editable 속성을 보면, clearSpans() 라는 메소드가 있다. 이 메소드를 사용하면 for 문 없이 clearSpans() 만 호출해주면 되는 데 사용하지 않은 이유는??

    👉 editText 의 동작이 이상해진다; 

    이상해진다는 의미가 뭐냐면, 막 커서가 2개 생기고 심지어 하나는 투명 커서야.. 글자 입력도 이상해지고.. 

     

     

     

    다른 분들은 이런 실수를 하시지 않길 바라며.. 트러블 슈팅기를 공유합니다. 

     

    반응형