개발 공부/안드로이드

[Compose] 컴포즈 공부하기 10 - Text

yong_DD 2023. 9. 26. 18:04

Text는 컴포즈를 시작하면 가장 처음으로 접하지 않을까 싶다!

(text를 작성한 줄 알고 있다가 이제서야 알게되어 작성하게 되었다😅)

 

그럼 Text에 대해 하나씩 알아가보자!

 

@Composable
fun Text(
    text: String,
    modifier: Modifier = Modifier,
    color: Color = Color.Unspecified,
    fontSize: TextUnit = TextUnit.Unspecified,
    fontStyle: FontStyle? = null,
    fontWeight: FontWeight? = null,
    fontFamily: FontFamily? = null,
    letterSpacing: TextUnit = TextUnit.Unspecified,
    textDecoration: TextDecoration? = null,
    textAlign: TextAlign? = null,
    lineHeight: TextUnit = TextUnit.Unspecified,
    overflow: TextOverflow = TextOverflow.Clip,
    softWrap: Boolean = true,
    maxLines: Int = Int.MAX_VALUE,
    onTextLayout: (TextLayoutResult) -> Unit = {},
    style: TextStyle = LocalTextStyle.current
)

 

1. text, color

text는 당연히 보여줄 string 값이고, color는 글자색이다.

Text(
    text = "Hello $name!",
    color = Color.Red
)

 

2. fontSize, fontStyle, fontWeight, fontFamily

fontSize는 size값으로 sp를 쓰게된다.

dp를 쓰게 되면 에러가 나는데 fontSize는 TextUnit값이 들어가야하고, 이 TextUnit은 3가지의 값이 있다.

internal val TextUnitTypes =
    arrayOf(TextUnitType.Unspecified, TextUnitType.Sp, TextUnitType.Em)

sp와  em을 기본적으로 사용할 수 있기 때문에 xml에서 자유롭게(?) 사용할 수 있었던 dp는 사용이 불가하다.

 

안드로이드 디벨로퍼에서도 sp 또는 em을 사용하도록 되어 있다.

 

나머지 style, weight, family는 xml의 TextView와 동일하다고 봐주면 될 것 같다!

Text(
    text = "Hello $name!",
    fontSize = 12.sp,
    fontStyle = FontStyle.Italic,
    fontWeight = FontWeight.Bold,
    fontFamily = FontFamily.SansSerif
)

 

3. letterSpacing(자간), lineHeight (줄 간격)

letterSpacing과 lineHeight도 동일하게 TextUnit이 들어가야한다.

(sp 또는 em으로 값을 주어야 한다는 뜻!)

Text(
    text = "Hello $name! \n 안녕하세요"
)

Text(
    text = "Hello $name! \n 안녕하세요",
    letterSpacing = 10.sp, // 자간 추가
)

 

Text(
    text = "Hello $name! \n 안녕하세요",
    letterSpacing = 10.sp,
    lineHeight = 10.sp // 줄간격, 기본 값이 있고 지정시 지정된 값으로 변경
)

 

4. textDecoration,  textAlign

textDecroation은 text에 underline을 넣거나 취소선을 넣는 등의 꾸미는 값이다.

textDecoration은 TextDecoration 값이 들어가야하고 아래와 같다.

@Stable
val None: TextDecoration = TextDecoration(0x0)

/**
 * Draws a horizontal line below the text.
 *
 * @sample androidx.compose.ui.text.samples.TextDecorationUnderlineSample
 */
@Stable
val Underline: TextDecoration = TextDecoration(0x1)

/**
 * Draws a horizontal line over the text.
 *
 * @sample androidx.compose.ui.text.samples.TextDecorationLineThroughSample
 */
@Stable
val LineThrough: TextDecoration = TextDecoration(0x2)

/**
 * Creates a decoration that includes all the given decorations.
 *
 * @sample androidx.compose.ui.text.samples.TextDecorationCombinedSample
 *
 * @param decorations The decorations to be added
 */
fun combine(decorations: List<TextDecoration>): TextDecoration {
    val mask = decorations.fastFold(0) { acc, decoration ->
        acc or decoration.mask
    }
    return TextDecoration(mask)
}

 

Underline과 LineThrough는 기본적으로 제공하고 있고, combine도 제공하고 있는데 아래와 같이 사용하면 된다.

Text(
    text = "Hello $name!",
    textDecoration = TextDecoration.combine(
        listOf(
            TextDecoration.LineThrough,
            TextDecoration.Underline
        )
    )
)

plus를 사용해서 아래와 같이 작성해도 똑같은 결과 값을 얻을 수 있다!

Text(
    text = "Hello $name!",
    textDecoration = TextDecoration.LineThrough
        .plus(TextDecoration.Underline)
)

 

5. textAlign

textAlign은 text의 정렬을 말한다.

TextAlign의 값은 아래와 같다.

companion object {
    val Left = TextAlign(1)
    val Right = TextAlign(2)
    val Center = TextAlign(3)
    val Justify = TextAlign(4)
    val Start = TextAlign(5)
    val End = TextAlign(6)
    fun values(): List<TextAlign> = listOf(Left, Right, Center, Justify, Start, End)
}

 

text를 센터 정렬 해보자!

Box(
    modifier = Modifier.size(200.dp)
) {
    Text(
        text = "Hello $name!",
        textAlign = TextAlign.Center
    )
}

위와 같이 작성하면 어떻게 될까? 센터 정렬이 될까?

 

답은 당연히 X다!

textAlign은 text의 사이즈 안에서의 정렬이기 때문에 사이즈를 지정해주지 않으면 사이즈는 text에 딱맞게 되어있기 때문에 아무런 일이 일어나지 않는다.

Box(
    modifier = Modifier.size(200.dp)
) {
    Text(
        text = "Hello $name!",
        textAlign = TextAlign.Center,
        modifier = Modifier.fillMaxSize()
    )
}

부모의 사이즈를 받아 위와같이 작성한다면 이제 가운데로 갈 수 있을까?

위의 코드를 돌려보면 아래와 같이 나오게 된다.

이와 같이 나오는 것을 예상했다면  바로 6번으로 가면 된다😁

 

정 가운데로 갈 것 같은데 가운데로 가지 않는다.

위로 올라가서 TextAlign  값을 보면 left, right, start, end, justify, center의 값을 가지고 있다.

각 값들을 보면 좌우와 가운데의 값만을 가지고 있다.

TextAlign은 가로 정렬의 값만을 가지고 있는 것이다!

TextAlign class에도 아래와 같이 적혀있다.

Defines how to align text horizontally.

 

 

 

정 가운데로 가기 위해서는 아래와 같이 할 수 있다.

Box(
    modifier = Modifier.size(200.dp)
) {
    Text(
        text = "Hello $name!",
        textAlign = TextAlign.Center,
        modifier = Modifier.fillMaxWidth()
            .align(Alignment.Center)
    )
}

 modifier에 align값을 주게 되면 생각하는 대로 값이 나올 것이다!

 

물론  TextAlign 값을 주지 않고 아래와 같이 값을 주어도 동일하게 동작한다😀

Box(
    modifier = Modifier.size(200.dp)
) {
    Text(
        text = "Hello $name!",
        textAlign = TextAlign.Center,
        modifier = Modifier.align(Alignment.Center)
    )
}

 

 

6. overflow, softWrap, maxLines

overflow는 text가 지정된 line 수나 길이에 넘쳤을 경우 어떻게 보이게 할 것인지에 대한 것이다.

TextOverflow를 사용해야하며 아래와 같이 3개가 있다.

companion object {
    @Stable
    val Clip = TextOverflow(1)
    
    @Stable
    val Ellipsis = TextOverflow(2)
    
    @Stable
    val Visible = TextOverflow(3)
}

위의 코드를 테스트하기 위해 최대 줄 수인 maxLine을 설정하여 아래와 같이 코드를 작성하여 테스트 해보았다.

Box(
    modifier = Modifier.size(200.dp)
) {
    Text(
        text = "동해 물과 백두산이 마르고 닳도록 하느님이 보우하사 우리나라 만세 무궁화 삼천리 화려강산 대한 사람, 대한으로 길이 보전하세",
        overflow = TextOverflow.Visible, // TextOverflow.Ellipsis, TextOverflow.Clip
        maxLines = 2
    )
}

Ellipsis                                                                            clip                                                                          visible           

Ellipsis는 뒤에 말줄임표가 나오게 된다.

Clip과 Visible은 동일하게 보이나 나올 수 있는 부분까지는 나온다는 공통점은 있지만,

Clip은 나올 수 있는 부분에서 글이 잘린다는 것이고 Visible은 안보이는 곳에서 글이 전체 다 보이고 있다는 차이점이 있다!

 

softWrap은 자동 줄바꿈이라고 보면 된다.

기본은 true인데 위의 코드에서 softWrap 을 false로 바꾸게 되면!?

Text(
    text = "동해 물과 백두산이 마르고 닳도록 하느님이 보우하사 우리나라 만세 무궁화 삼천리 화려강산 대한 사람, 대한으로 길이 보전하세",
    overflow = TextOverflow.Visible, // TextOverflow.Ellipsis, TextOverflow.Clip
    maxLines = 2,
    softWrap = false
)

maxLine을 2로 지정해도 줄바꿈이 되지 않아 1줄로 나오게 된다.

false일 경우 글자가 무제한 수평 공간이 있는 것처럼 배치되기 때문에 당연히 overflow에 값을 주어도 원하는 것처럼 나올 수 없게 될 수 있다.

 

 

 

7. onTextLayout

새로운 텍스트 레이아웃이 계산될 때 실행되는 콜백으로 TextLayoutResult를 제공한다.

TextLayoutResult 개체는 문단 정보, 텍스트 크기, 기준선 및 기타 세부 정보등을 제공한다.

class TextLayoutResult constructor(
    val layoutInput: TextLayoutInput,
    val multiParagraph: MultiParagraph,
    val size: IntSize
) {
    val firstBaseline: Float = multiParagraph.firstBaseline
    val lastBaseline: Float = multiParagraph.lastBaseline
    val didOverflowHeight: Boolean get() = multiParagraph.didExceedMaxLines ||
        size.height < multiParagraph.height
    val didOverflowWidth: Boolean get() = size.width < multiParagraph.width
    val hasVisualOverflow: Boolean get() = didOverflowWidth || didOverflowHeight
    val placeholderRects: List<Rect?> = multiParagraph.placeholderRects
    val lineCount: Int get() = multiParagraph.lineCount
    
    fun getLineStart(lineIndex: Int): Int = multiParagraph.getLineStart(lineIndex)

    fun getLineEnd(lineIndex: Int, visibleEnd: Boolean = false): Int =
        multiParagraph.getLineEnd(lineIndex, visibleEnd)

    fun isLineEllipsized(lineIndex: Int): Boolean = multiParagraph.isLineEllipsized(lineIndex)

    fun getLineTop(lineIndex: Int): Float = multiParagraph.getLineTop(lineIndex)

    fun getLineBottom(lineIndex: Int): Float = multiParagraph.getLineBottom(lineIndex)

    fun getLineLeft(lineIndex: Int): Float = multiParagraph.getLineLeft(lineIndex)

    fun getLineRight(lineIndex: Int): Float = multiParagraph.getLineRight(lineIndex)

    fun getLineForOffset(offset: Int): Int = multiParagraph.getLineForOffset(offset)

    fun getLineForVerticalPosition(vertical: Float): Int =
        multiParagraph.getLineForVerticalPosition(vertical)

    fun getHorizontalPosition(offset: Int, usePrimaryDirection: Boolean): Float =
        multiParagraph.getHorizontalPosition(offset, usePrimaryDirection)

    fun getParagraphDirection(offset: Int): ResolvedTextDirection =
        multiParagraph.getParagraphDirection(offset)

    fun getBidiRunDirection(offset: Int): ResolvedTextDirection =
        multiParagraph.getBidiRunDirection(offset)

    fun getOffsetForPosition(position: Offset): Int =
        multiParagraph.getOffsetForPosition(position)

    fun getBoundingBox(offset: Int): Rect = multiParagraph.getBoundingBox(offset)

    fun getWordBoundary(offset: Int): TextRange = multiParagraph.getWordBoundary(offset)

    fun getCursorRect(offset: Int): Rect = multiParagraph.getCursorRect(offset)

    fun getPathForRange(start: Int, end: Int): Path = multiParagraph.getPathForRange(start, end)

    //...
}

TextLayout에는 위와 같이 여러 값과 기능들이 있다.

 

var backgroundColor by remember { mutableStateOf(Color.White) }
Text(
    text = text,
    onTextLayout = { result ->
        backgroundColor =
        if(result.lineCount >= 2 ) Color.Red else Color.White
    },
    modifier = Modifier.background(backgroundColor)
)

text의 line 수가 2개 이상일 때 배경이 빨간색으로 변하는 코드이다.

이렇게 값에 의해 배경 등 변경이 필요할 때 사용할 수 있다!


[출처 및 참고]

 

TextUnit  |  Android Developers

androidx.appsearch.builtintypes.properties

developer.android.com