관리 메뉴

나만을 위한 블로그

[Android] 텍스트에 밑줄 추가하는 법 본문

Android

[Android] 텍스트에 밑줄 추가하는 법

참깨빵위에참깨빵_ 2025. 2. 11. 22:03
728x90
반응형

프로젝트에 저장된 문자열에 밑줄을 추가해서 표시하는 거라면 아래처럼 할 수 있다.

 

<string name="string">여긴 밑줄 안 됨 &lt;u&gt;여기만 밑줄됨&lt;/u&gt; 여긴 밑줄 안 됨</string>

 

그리고 액티비티, 프래그먼트에서 아래처럼 사용한다.

 

binding.tv1.text = Html.fromHtml(getString(R.string.string), Html.FROM_HTML_MODE_LEGACY)

 

또는 아래처럼 써도 상관없다.

 

binding.tv1.text = Html.fromHtml(getString(R.string.string), HtmlCompat.FROM_HTML_MODE_LEGACY)

binding.tv1.text = HtmlCompat.fromHtml(getString(R.string.string), HtmlCompat.FROM_HTML_MODE_LEGACY)

 

아니면 Paint 클래스의 UNDERLINE_TEXT_FLAG를 텍스트뷰의 paintFlags 설정값으로 넘기는 방법도 있다.

 

val underlineText = "밑줄 처리할 텍스트"
binding.tv1.paintFlags = Paint.UNDERLINE_TEXT_FLAG
binding.tv1.text = underlineText

 

 

조금 더 세밀한 제어를 원한다면 UnderlineSpan을 고려할 수 있다.

맨 처음의 <u> 태그를 사용한 것과 같은 효과를 낸다면 아래처럼 할 수 있다. 서버에서 받아온 문자열 일부를 밑줄 처리하고 싶을 때 쓸 수 있을 것이다.

 

override fun onCreate(savedInstanceState: Bundle?) {
        enableEdgeToEdge()
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }

        val fullText = "여긴 밑줄 안 됨 여기만 밑줄됨 여긴 밑줄 안 됨"
        val startIndex = fullText.indexOf("여기만 밑줄됨")
        val endIndex = startIndex + "여기만 밑줄됨".length
        val spannableString = SpannableString(fullText)
        spannableString.setSpan(
            UnderlineSpan(),
            startIndex,
            endIndex,
            Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
        )
        binding.tv1.text = spannableString
}

 

 

하지만 이렇게 기본 제공되는 밑줄이 아닌 색깔과 두께, 위치를 바꾼 밑줄을 구현해야 할 수 있다.

그냥 XML에 View 하나 추가하면 되는 거 아니냐고 생각할 수 있지만 한 줄이었던 텍스트가 2줄, 3줄로 나눠서 표시되더라도 모든 텍스트 밑에 밑줄이 표시돼야 한다면 View 하나 추가하는 것으론 부족하다.

아래의 클래스를 사용하면 별 문제 없이 밑줄 처리를 할 수 있다.

 

import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatTextView

class UnderlineTextView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : AppCompatTextView(context, attrs, defStyleAttr) {

    var underlineColor: Int = 0
    var underlineHeight: Float = 0f
    var underlineMarginTop: Float = 0f

    private val underlinePaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        style = Paint.Style.STROKE
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)

        underlinePaint.color = underlineColor
        underlinePaint.strokeWidth = underlineHeight

        val layout = layout ?: return
        val lineCount = layout.lineCount

        val fm = paint.fontMetrics

        for (i in 0 until lineCount) {
            val lineLeft = layout.getLineLeft(i)
            val lineRight = layout.getLineRight(i)

            val baseline = layout.getLineBaseline(i)

            val textDescent = fm.descent

            val y = baseline + textDescent + underlineMarginTop + underlineHeight

            canvas.drawLine(lineLeft, y, lineRight, y, underlinePaint)
        }
    }

}

 

XML에선 TextView 대신 이 클래스의 전체 경로를 적어준다.

그리고 paddingBottom을 1dp라도 넣어줘야 한다. 밑줄 굵기가 굵다면 2dp, 3dp 정도로 크기를 조절해야 밑줄이 완전히 표시된다.

lineSpacingExtra는 안 넣어도 상관없다. 텍스트 줄 구분을 위해 추가한 속성이다.

 

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".presentation.MainActivity">

    <com.example.regacyviewpractice.presentation.underline.UnderlineTextView
        android:id="@+id/tv1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingBottom="2dp"
        android:lineSpacingExtra="10dp"
        android:textSize="20dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:text="테스트" />

</androidx.constraintlayout.widget.ConstraintLayout>

 

액티비티, 프래그먼트에선 텍스트를 set해주면서 underlineColor, Height, MarginTop의 값을 원하는 대로 설정하면 된다.

 

val fullText = "이 텍스트에 전부 밑줄 칠 것 이 텍스트에 전부 밑줄 칠 것 이 텍스트에 전부 밑줄 칠 것"
binding.tv1.apply {
    text = fullText
    underlineColor = Color.RED
    underlineHeight = 3f
    underlineMarginTop = 1f
}

 

폴드 에뮬레이터에서 확인해 본다. 펼친 상태라면 아래처럼 표시된다.

 

 

접은 상태면 아래처럼 표시된다.

 

 

이렇게 하면 폴드를 접거나 가로 폭이 좁은 기기에서도 텍스트뷰 하나로 여러 밑줄을 표시할 수 있다.

그럼 어떻게 저렇게 작동하는 건가? 작동 원리는 아래와 같다.

 

import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatTextView

class UnderlineTextView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : AppCompatTextView(context, attrs, defStyleAttr) {

    var underlineColor: Int = 0
    var underlineHeight: Float = 0f
    var underlineMarginTop: Float = 0f

    private val underlinePaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        style = Paint.Style.STROKE
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)

        underlinePaint.color = underlineColor
        underlinePaint.strokeWidth = underlineHeight

        val layout = layout ?: return
        val lineCount = layout.lineCount

        val fm = paint.fontMetrics

        for (i in 0 until lineCount) {
            val lineLeft = layout.getLineLeft(i)
            val lineRight = layout.getLineRight(i)

            val baseline = layout.getLineBaseline(i)

            val textDescent = fm.descent

            val y = baseline + textDescent + underlineMarginTop + underlineHeight

            canvas.drawLine(lineLeft, y, lineRight, y, underlinePaint)
        }
    }

}

 

전역 변수들은 뷰에서 호출할 때 원하는 값을 넣기 위해 전부 var로 선언했다.

underlinePaint에는 ANTI_ALIAS_FLAG를 썼는데 이건 안티 앨리어싱을 활성화하는 Paint 플래그다. 고사양 게임을 해봤다면 그래픽 옵션 화면에서 자주 본 단어인데 네모 형태인 픽셀로 이뤄진 곡선, 원 등의 이미지가 매끄럽게 표시되도록 해 준다. 고작 밑줄 긋는데 과한 옵션이라고 생각되지만 기왕 표시되는 거 선명하게 표시되면 좋을 거 같아서 썼다.

 

이후 layout 객체를 통해 텍스트의 각 줄 시작, 끝, 기준선(baseline) 정보를 가져온다. layout 객체는 텍스트뷰가 완전히 그려저서 상호작용할 준비가 끝난 후에 접근할 수 있기 때문에, 텍스트뷰가 아직 다 그려지지 않았다면 layout 객체에 접근할 수 없어서 밑줄을 그리지 않고 넘어간다.

참고로 baseline이 뭔지 모른다면 아래 그림을 참고하자.

 

https://recipes4dev.tistory.com/94

 

그 다음 반복문으로 줄 수(lineCount)만큼 반복하며 getLineLeft(i), getLineRight(i)로 실제로 텍스트가 그려지는 영역의 시작과 끝의 x 좌표값을 구하고, paint.fontMetrics로 표시될 텍스트에서 글자가 실제로 기준선(baseline) 아래로 얼마나 내려갈지에 대한 값인 descent를 가져온다. 이 값은 반복문 안에서 기준선과 underlineMarginTop, underlineHeight를 합쳐서 밑줄과 밑줄 위에 표시되는 글자 사이 간격을 결정하는 y 좌표값이 된다. 이를 통해 x, y 좌표값을 얻었으니 텍스트 밑에 밑줄을 그려나가다 마지막 글자까지 그린 후 종료되는 것이다.

반응형
Comments