이번 포스팅은 앞서 설명했던 drawLine(), drawText()를 이용해 꺾은선그래프를 그리는 과정을 담은 part이다.
drawLine(), drawText()에 미숙하거나 잘 모르겠다면 여기를 클릭해서 학습하고 오자.

image

위의 그래프를 얻을 수 있다.

DrawScope의 확장함수를 별도로 만들어서 가독성을 높였다. 별도로 만들어진 확장함수를 차례대로 살펴보겠다. (baseLine -> baseLineText -> dataLine -> 전체코드)

baseLine()

fun DrawScope.baseLine(color: Color, startX: Float, endX: Float) {
    drawLine(
        color = color,
        start = Offset(startX, size.height),
        end = Offset(endX, size.height),
        strokeWidth = 2f
    )
    drawLine(
        color = color,
        start = Offset(startX, size.height),
        end = Offset(startX, 0f),
        strokeWidth = 2f
    )
    drawLine(
        color = color,
        start = Offset(endX, size.height),
        end = Offset(endX, 0f),
        strokeWidth = 2f
    )
}

Screenshot 2024-06-20 at 3 19 13 AM


보라색으로 표시한부분이 baseLine이다. 그래프의 수치를 확인할 수 있는 뼈대라고 생각하면 된다.

color는 baseLine의 색상,
startX는 RowBaseLine의 시작 x좌표,
endX는 RowBaseLine의 끝 x좌표를 의미한다.
❗️높이는 canvas의 높이와 동일하게 고정하였기 때문에 별도로 설정하지 않는다.

baseLineText()

fun DrawScope.rowBaseLineText(count: Int, color: Color, list: List<TextLayoutResult>, rowSize: Float) {
    // row 점선 데이터
    repeat(count + 1) {
        if (it == 0) {
            drawText(
                textLayoutResult = list[it],
                topLeft = Offset(rowSize * (it / count.toFloat()), size.height),
                color = color
            )
        } else if (it == count) {
            drawText(
                textLayoutResult = list[it],
                topLeft = Offset(
                    rowSize * (it / count.toFloat()) - list[it].size.width,
                    size.height
                ),
                color = color
            )
        } else {
            drawText(
                textLayoutResult = list[it],
                topLeft = Offset(
                    rowSize * (it / count.toFloat()) - list[it].size.width / 2,
                    size.height
                ),
                color = color
            )
        }
    }
}
fun DrawScope.columnBaseLineText(count: Int, color: Color, list: List<TextLayoutResult>, endPadding: Int) {
    // column 점선 데이터
    for (i in 1..count) {
        drawText(
            textLayoutResult = list[i],
            topLeft = Offset(
                size.width - endPadding,
                size.height * (1 - i / count.toFloat())
            ),
            color = color
        )
    }
}

Screenshot 2024-06-20 at 3 30 56 AM


보라색 점선의 영역으로 표시된 부분이 baseLineText()이다. row영역과 column영역으로 구분하였다. row의 text는 rowBaseLineText()로 column의 text는 columnBaseLineText으로 작성한다.

count는 text의 개수,
color는 text의 색상,
list는 text의 정보가 들어있는 List,
endPadding은 보통 text의 크기를 나타내며, 전체 길이에서 text의 크기만큼 좌측으로 이동한 크기를 의미한다. (0으로 설정할 경우 화면에 보이지 않는다.)

dataLine()

fun DrawScope.dataLine(color: Color, rowSize: Float, rowCount: Int, list: List<Int>, lastColumnValue: Int) {
    // 데이터의 그래프
    for (i in 0 until list.size - 1) {
        if (i == 0) {
            drawLine(
                color = color,
                start = Offset(0f, size.height * (1 - list[i] / lastColumnValue.toFloat())),
                end = Offset(
                    rowSize / (rowCount - 1),
                    size.height * (1 - list[1] / lastColumnValue.toFloat())
                ),
                strokeWidth = 5f
            )
        } else {
            drawLine(
                color = color,
                start = Offset(
                    rowSize * (i / (rowCount - 1).toFloat()),
                    size.height * (1 - list[i] / lastColumnValue.toFloat())
                ),
                end = Offset(
                    rowSize * ((i + 1) / (rowCount - 1).toFloat()),
                    size.height * (1 - list[i + 1] / lastColumnValue.toFloat())
                ),
                strokeWidth = 5f
            )
        }
    }
}

Screenshot 2024-06-20 at 3 41 33 AM


dataLine()는 살제 데이터를 의미하는 꺾은선 그래프이다. 보라색 점선영역으로 표시되었다.

color는 꺾은선 그래프의 색상,
rowSize는 rowBaseLine의 width(꺾은선 그래프의 각 선마다 x축 값을 정하는데 필요하다.),
rowCount는 실제 데이터의 수, list는 실제 데이터의 리스트,
lastColumnValue는 list의 max 값(꺾은선 그래프의 각 선마다 y축 값을 정하는데 필요하다.)

전체 코드

@Composable
fun LineGraph(rowData: List<Int>, list: List<Int>, isShowBaseLineText: Boolean = true) {
    val rowDottedLineCount = 4                          // rowBaseLine 개수
    val columnDottedLineCount = 3                       // columnBaseLine 개수

    val dataLineCount = list.size                  // 꺾은선 그래프의 개수(실제 데이틔 수)
    val lastRowValue = rowData.last()              // rowBaseLine 마지막 값
    val maxColumnValue = list.max()                // columnBaseLine 마지막 값(max 값)

    // ------> BaseLine Text 설정
    val textMeasurer = rememberTextMeasurer()

    val style = TextStyle(
        fontSize = 15.sp,
        color = Color.White,
    )
    // <---------

    val rowBaseLineTextList = mutableListOf<TextLayoutResult>()
    val columnBaseLineTextList = mutableListOf<TextLayoutResult>()



    if (isShowBaseLineText) {

        for (i in 0..rowDottedLineCount) {
            val result = (lastRowValue * (i / rowDottedLineCount.toFloat()))
            val text = if (result * 10 % 10 == 0f) {
                result.toInt().toString()
            } else {
                result.toString()
            }
            rowBaseLineTextList.add(remember(text) { textMeasurer.measure(text, style) })
        }


        for (i in 0..columnDottedLineCount) {
            val text = (maxColumnValue * (i / columnDottedLineCount.toFloat())).toInt().toString()
            columnBaseLineTextList.add(remember(text) { textMeasurer.measure(text, style) })
        }
    }



    val baseLineColor = Color.Black
    val dataLineColor = Color(0xFF0C964A)
    val textColor = Color.Black

    Canvas(
        modifier = Modifier
            .fillMaxWidth()
            .aspectRatio(1.2f)
    ) {
        val lastRowPadding = if (columnBaseLineTextList.isEmpty()) 0 else columnBaseLineTextList.maxOf { it.size.width }
        val rowSize = size.width - (lastRowPadding + 10f)

        baseLine(color = baseLineColor, startX = 0f, endX = rowSize)

        if (isShowBaseLineText) {
            // true, false check 할 것
            columnBaseLineText(count = columnDottedLineCount, color = textColor, list = columnBaseLineTextList, endPadding = lastRowPadding)

            // true, false check 할 것
            rowBaseLineText(count = rowDottedLineCount, color = textColor, list = rowBaseLineTextList, rowSize = rowSize)
        }


        dataLine(color = dataLineColor, rowSize = rowSize, rowCount = dataLineCount, list = list, lastColumnValue = maxColumnValue)
    }
}

fun DrawScope.baseLine(color: Color, startX: Float, endX: Float) {
    drawLine(
        color = color,
        start = Offset(startX, size.height),
        end = Offset(endX, size.height),
        strokeWidth = 2f
    )
    drawLine(
        color = color,
        start = Offset(startX, size.height),
        end = Offset(startX, 0f),
        strokeWidth = 2f
    )
    drawLine(
        color = color,
        start = Offset(endX, size.height),
        end = Offset(endX, 0f),
        strokeWidth = 2f
    )
}

fun DrawScope.rowBaseLineText(count: Int, color: Color, list: List<TextLayoutResult>, rowSize: Float) {
    // row 점선 데이터
    repeat(count + 1) {
        if (it == 0) {
            drawText(
                textLayoutResult = list[it],
                topLeft = Offset(rowSize * (it / count.toFloat()), size.height),
                color = color
            )
        } else if (it == count) {
            drawText(
                textLayoutResult = list[it],
                topLeft = Offset(
                    rowSize * (it / count.toFloat()) - list[it].size.width,
                    size.height
                ),
                color = color
            )
        } else {
            drawText(
                textLayoutResult = list[it],
                topLeft = Offset(
                    rowSize * (it / count.toFloat()) - list[it].size.width / 2,
                    size.height
                ),
                color = color
            )
        }
    }
}

fun DrawScope.columnBaseLineText(count: Int, color: Color, list: List<TextLayoutResult>, endPadding: Int) {
    // column 점선 데이터
    for (i in 1..count) {
        drawText(
            textLayoutResult = list[i],
            topLeft = Offset(
                size.width - endPadding,
                size.height * (1 - i / count.toFloat())
            ),
            color = color
        )
    }
}

fun DrawScope.dataLine(color: Color, rowSize: Float, rowCount: Int, list: List<Int>, lastColumnValue: Int) {
    // 데이터의 그래프
    for (i in 0 until list.size - 1) {
        if (i == 0) {
            drawLine(
                color = color,
                start = Offset(0f, size.height * (1 - list[i] / lastColumnValue.toFloat())),
                end = Offset(
                    rowSize / (rowCount - 1),
                    size.height * (1 - list[1] / lastColumnValue.toFloat())
                ),
                strokeWidth = 5f
            )
        } else {
            drawLine(
                color = color,
                start = Offset(
                    rowSize * (i / (rowCount - 1).toFloat()),
                    size.height * (1 - list[i] / lastColumnValue.toFloat())
                ),
                end = Offset(
                    rowSize * ((i + 1) / (rowCount - 1).toFloat()),
                    size.height * (1 - list[i + 1] / lastColumnValue.toFloat())
                ),
                strokeWidth = 5f
            )
        }
    }
}

  • 실제 적용한 코드
    image

    isShowBaseLineText는 BaseLineText를 UI에 나타낼지에 대한 유무를 결정한다.