ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Android/Foundation] 안드로이드에서 LineHeight 속성이 타 플랫폼과 다른 이유
    안드로이드 2024. 5. 6. 03:07

    안드로이드 개발을 하다보면 디자이너가 전달준 텍스트 컴포넌트 스펙을 그대로 사용했는데도

    왠지 모르게 디자인이 묘하게 달라 보일 때가 있습니다.

    왼쪽은 기본 Text고 오른쪽은 이 글의 내용을 적용한 Text 입니다.

     

    왼쪽의 문장에도 LineHeight를 적용했음에도 텍스트가 차지하는 높이가 왜 그대로인지

    이 글을 통해서 의문을 풀어주고자 합니다.


    LineHeight 란?

    LineHeight는 말 그대로 글자 크기와 관계 없이 이 문장이 차지할 높이를 얘기합니다.

    이렇게 하면 글자 크기와는 별개로 텍스트의 영역을 조절할 수 있게 됩니다.

     

    시각적인 요소에 대해서는 견해가 없어서 정확히 LineHeight 수치를 어떻게 맞추는 지는

    잘 모르겠지만 아마도 문장이 너무 빽빽하게 보여지지 않게 하는 의도로 맞추지 않을까 싶습니다.

     

    하지만 실제로 안드로이드에서는 LineHeight 속성을 적용하더라도

    예시의 이미지처럼 동작하지 않습니다.

     

     

    Baseline

    우선 이후의 설명을 위해 Baseline이라는 개념을 알아야합니다.

    Baseline은 텍스트를 작성하는 기준 선입니다.

    안드로이드에서는 Baseline을 기준으로 텍스트의 크기를 상대거리로 계산합니다.

    코드 상의 주석에도 baseline 과의 거리라고 정의되어 있습니다.

     

     

    View의 LineHeight

    View에서 LineHeight 라는 속성이 존재하긴 합니다.

    근데 이 속성을 들여다보면

    내부적으로는 LineSpacing으로 판단하도록 되어있습니다.

     

    안드로이드에서의 행간은 lineSpacingExtra + lineSpacingMultiplier 로 계산되는데,

    이는 Baseline과 다음 Baseline의 거리로 정의합니다.

     

    따라서 LineHeight는 첫째 줄의 윗 부분과 마지막째 줄의 아랫 부분에

    여백이 제외되어서 예상과는 다른 높이를 갖게 되는 것입니다.

     

    예를 들면, lineHeight가 20일 때 text 높이가 16이라고 가정한다면 아래의 높이를 갖게 됩니다.

    - 1줄 일 때 : 16

    - 2줄 일 때 : 16 + 20

    - 3줄 일 때 : 16 + 20 + 20

     

    다행이도 안드로이드 28버전 부터 추가된 두 가지 속성이 있습니다.

    - firstBaselineToTopHeight

    글자의 맨 위부터 첫 번째 줄 baseline까지의 거리

    - lastBaselineToBottomHeight

    맨아래부터 글자의 맨 아래번 째 줄 baseline까지의 거리

     

    baseline을 기준으로 위 아래 간격을 모두 컨트롤 할 수 있게 되었기 때문에 행간 속성을 함께 활용하면

    올바른 lineheight를 맞출 수 있게 되었습니다.

     

    class MyTextView @JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null,
    ) : AppCompatTextView(context, attrs) {
        init {
            val typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyTextView, 0, 0)
            val lh = typedArray.getDimensionPixelSize(R.styleable.MyTextView_bezier_lineHeight, 1).toFloat()
    
            val ascentAbs = paint.fontMetricsInt.ascent.absoluteValue
            val spacer = ((lh - ascentAbs) / 2).coerceAtLeast(0f)
    
            firstBaselineToTopHeight = (ascentAbs + spacer).toInt()
            lastBaselineToBottomHeight = spacer.toInt()
    
            setLineSpacing(lh, 0f)
        }
    }

     

    AppCompatViewInflater

    위 방식대로 진행하게 될 경우 폰트의 높이를 동적으로 계산하기 위해 CustomView가 필수로 사용되어야 합니다.

    일반적으로 CustomView를 그냥 직접 사용해도 되겠지만 간혹 실수로 일반 TextView를 사용할 수도 있습니다.

     

    이럴 때 AppCompatViewInflater 를 활용하면 TextView를 자동으로 CustomView로 변환해주게 됩니다.

    AppCompatDelegateImpl 구현의 일부입니다. 여기서 viewInflaterClass를 가져와서 바꿔치기 해줍니다.

     

    // theme.xml
    <style name="Theme.TypographySample" parent="Base.Theme.TypographySample">
            <item name="viewInflaterClass">com.akmunny.typographysample.MyViewInflater</item>
    </style>
    
    // BezierViewInflater.kt
    class MyViewInflater : AppCompatViewInflater() {
        override fun createTextView(context: Context, attrs: AttributeSet?): AppCompatTextView {
            return MyTextView(context, attrs)
        }
    }

     

     

    이를 통해 style 내부에 typography 속성을 숨기고 CustomView는 ViewInflater를 통해 숨겨줄 수 있게 되었습니다.

     

    Compose의 LineHeight

    Compose는 API 버전과 관계 없이 lineHeight 속성이 존재합니다.

     

    하지만 앞서 설명 드렸던 것 처럼 Compose 역시 lineHeight는 baseline간의 간격으로 정의합니다.

     

    다만 내부적으로 구현 방식이 조금 다른데 Compose에서는 baseline간의 간격으로 하기 위해서

    첫 번째 줄의 위와 마지막 번 째 줄 아래의 padding 값을 제거(trim) 해줍니다

     

    이 정책은 lineHeightStyle을 통해 여러 옵션이 제공되는데, 이 때 위 아래 padding을 제거하지 않는다는

    LineHeightStyle.Trim.None 옵션을 적용하면 원하는 UI를 볼 수 있습니다.

    internal val DefaultTextStyle = TextStyle(
        platformStyle = PlatformTextStyle(
            includeFontPadding = false
        ),
        lineHeightStyle = LineHeightStyle(
            alignment = LineHeightStyle.Alignment.Center,
            trim = LineHeightStyle.Trim.None
        )
    )

     


    이런 디테일들을 맞추다보면 더 퀄리티 있는 UI의 앱을 만들 수 있게 될 수 있다고 생각합니다.

    읽어주셔서 감사합니다!

     

    반응형

    댓글

Designed by Tistory.