[Android] MPAndroidChart LineChart 구현

hxeyexn·2023년 12월 2일
0

Intro

지난 PieChart 구현에 이어 MPAndroidChart 라이브러리(MPAndroidChart Github)를 이용해 LineChart를 구현해보려고 한다.

디자이너분이 디자인해주신 화면은 아래처럼 생겼다..! 최대한 비슷하게 만들어보자,,



문서

MPAndroidChart에 공식 문서(MPAndroidChart Document)가 존재하긴 하나 설명이 불친절하고 보기가 너무 힘들다.. 공식 문서만 보기 보다는 wiki(MPAndroidChart wiki)를 같이 보는 게 구현하기 훨씬 수월하다.



구현

dependency 추가

  • build.gradle(Module :app)
dependencies {
    implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0'
}
  • settings.gradle
dependencyResolutionManagement {
    ...
    repositories {
        ...
        maven { url 'https://jitpack.io' }
    }
}

settings.gradle과 app 단위 build.gradle에 dependency를 추가해준다.


xml

dependency를 추가했다면 xml에 사용하고자 하는 차트를 넣어준다.
태그에 com.github.mikephil.charting.charts.를 작성하면 아래 사진처럼 리스트가 나오는데 이중 사용하고자 하는 차트를 입력하면 된다.

아래 코드는 LineChart를 적용한 예시이다.

<com.github.mikephil.charting.charts.LineChart
	android:id="@+id/linechart_weight_weeklyrecord"
    android:layout_width="match_parent"
    android:layout_height="164dp"
    android:layout_marginTop="20dp"
    app:layout_constraintStart_toStartOf="@id/textview_weight_weeklyrecordexplanation"
    app:layout_constraintTop_toBottomOf="@id/textview_weight_weeklyrecordexplanation" />

문제

🧐 문제 1

legend.isEnabled = false 를 이용해 범례를 없애니 x축 데이터 text 아래 부분이 잘리는 문제가 발생했다. 글씨 크기도 기본보다 크게 설정해줘서 엄청 많이 잘렸다.

💡 문제 1 해결방법

binding.linechartWeightWeeklyrecord.apply {
 	...
    legend.xOffset = -50f
    ...
}

범례를 어떻게 없앨까 고민하다가 legend.xOffset = -50f 를 줘서 범례가 안 보이는 위치로 이동시켰다. 범례를 없앤 다른 예제들도 다 데이터 text가 짤려있어서 해결방법을 생각해 내는데 시간이 꽤 걸렸다..

🧐 문제 2

몸무게 값이 0.00kg 형식인데 글씨가 길어서 1주차와 4주차 value가 잘리는 문제가 있었다.

💡 문제 2 해결방법

binding.linechartWeightWeeklyrecord.apply {
 	...
    xAxis.apply {
    	...
        spaceMin = 0.2F
        spaceMax = 0.2F
        ...
    }
    ...
}

x축에 spaceMin, spaceMax를 이용해 차트 앞뒤에 공간을 줘서 해결했다. 이것도 자료가 없어서 문서보면서 이것저것 만져보다가 찾았다..🥲

🧐 문제 3

차트는 xml 안에서 폰트 적용이 안된다,, 코드에서 설정해줘야한다.

💡 문제 3 해결방법

아래 사진 처럼 assets라는 폴더를 만들고 차트에 필요한 fontfamily를 넣어줘야 한다.

🚨 res 내에 font 폴더에 해당 fontfamily를 넣어놨더라도 assets에도 별도로 넣어줘야한다.
assets 폴더를 따로 만들어서 폰트를 넣어야하는지 모르고 조금 헤맸다^^


assets 폴더에 fontfamily를 넣었다면 `Typeface.createFromAsset()`을 이용해 폰트를 변수에 담아주고, 데이터의 typeface를 해당 변수로 설정해주면 된다.
private fun initWeeklyRecordChart() {
	...
        val typo = Typeface.createFromAsset(requireContext().assets, "pretendard_medium.otf")

        weightWeeklyDataSet.apply {
            ...
            valueTypeface = typo
            ...
        }

        binding.linechartWeightWeeklyrecord.apply {
            ...
            xAxis.apply {
                ...
                typeface = typo
                ...
            }
            ...
        }
    }
}

🧐 문제 4

단위와 소수점 자릿수를 원하는 형식으로 설정할 방법이 없어서 formatter를 만들어줘야한다.

💡 문제 4 해결방법

ValueFormatter()를 상속받아 주와 체중 형식을 포맷하는 formatter를 만들어줬다.

class WeekFormatter : ValueFormatter() {
	private val format = DecimalFormat("#주")

	override fun getFormattedValue(value: Float): String {
    	return format.format(value)
    }
}

class WeightDataFormatter : ValueFormatter() {
    private val format = DecimalFormat("0.00kg")

	override fun getFormattedValue(value: Float): String {
    	return format.format(value)
    }
}

formatter는 valueFormatter에 지정해주면 된다!

weightWeeklyDataSet.apply {
	...
    valueFormatter = WeightDataFormatter()
    ...
}

Fragment

package com.project.meongcare.weight.view

import android.graphics.Typeface
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import com.github.mikephil.charting.components.XAxis
import com.github.mikephil.charting.data.Entry
import com.github.mikephil.charting.data.LineData
import com.github.mikephil.charting.data.LineDataSet
import com.github.mikephil.charting.formatter.ValueFormatter
import com.project.meongcare.R
import com.project.meongcare.databinding.FragmentWeightBinding
import java.text.DecimalFormat

class WeightFragment : Fragment() {
    private var _binding: FragmentWeightBinding? = null
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?,
    ): View {
        _binding = FragmentWeightBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(
        view: View,
        savedInstanceState: Bundle?,
    ) {
        super.onViewCreated(view, savedInstanceState)
        initWeeklyRecordChart()
    }

    private fun initWeeklyRecordChart() {
        val weightWeeklyData =
            listOf(
                Entry(1f, 4.67f),
                Entry(2f, 5f),
                Entry(3f, 4.8f),
                Entry(4f, 4.7f),
            )

        val weightWeeklyDataSet = LineDataSet(weightWeeklyData, "")

        val lineColor = resources.getColor(R.color.main4, null)

		// 폰트를 코드로 적용해야해서 typo라는 변수에 사용할 폰트를 담아줬다.
        val typo = Typeface.createFromAsset(requireContext().assets, "pretendard_medium.otf")

        weightWeeklyDataSet.apply {
            valueTextSize = 12F
            // font 설정
            valueTypeface = typo
            valueTextColor = resources.getColor(R.color.gray3, null)
            valueFormatter = WeightDataFormatter()
            color = lineColor 
            setCircleColor(lineColor)
            setDrawCircleHole(false)
            setDrawFilled(true)
            fillDrawable =
                ContextCompat.getDrawable(requireContext(), R.drawable.weight_weekly_chart_gradient)
        }

        binding.linechartWeightWeeklyrecord.apply {
            data = LineData(weightWeeklyDataSet)

            xAxis.apply {
                granularity = 1F
                textSize = 14F
                // font 설정
                typeface = typo
                textColor = resources.getColor(R.color.gray4, null)
                position = XAxis.XAxisPosition.BOTTOM
                // 단위 포맷
                valueFormatter = WeekFormatter()
                // 글씨가 길어 1주차와 4주차 value가 잘리는 문제를 해결하기 위해 
                // spaceMin과 spaceMax로 차트 앞뒤에 공간을 줌
                spaceMin = 0.2F
                spaceMax = 0.2F
                setDrawGridLines(false)
                // 디자인에는 x축 맨 아래 선이 없기 때문에 배경이랑 동일한 색을 설정해서 안 보이게 함
                axisLineColor = resources.getColor(R.color.white, null)
            }

            axisLeft.apply {
                setDrawLabels(false)
                setDrawAxisLine(false)
                gridColor = resources.getColor(R.color.gray2, null)
                gridLineWidth = 1F
            }

            axisRight.apply {
                setDrawLabels(false)
                setDrawAxisLine(false)
                setDrawGridLines(false)
            }

            description.isEnabled = false
            // legend.isEnabled를 이용해 범례를 숨겼을 때 
            // x축 데이터 text 아래 부분이 잘리는 문제를 해결하기 위해 legend.xOffset 사용
            legend.xOffset = -50f
            // 사용자가 마커를 터치할 수 있도록 하기 위해 true로 설정
            setTouchEnabled(true)
            setScaleEnabled(false)
            // 확대, 축소 설정
            setPinchZoom(false)
            // 마커 설정
            setDrawMarkers(true)
            marker = WeightCustomMarker(context, R.layout.weight_marker)
            animateY(1200)
        }
    }

    class WeekFormatter : ValueFormatter() {
        private val format = DecimalFormat("#주")

        override fun getFormattedValue(value: Float): String {
            return format.format(value)
        }
    }

    class WeightDataFormatter : ValueFormatter() {
        private val format = DecimalFormat("0.00kg")

        override fun getFormattedValue(value: Float): String {
            return format.format(value)
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

Pie Chart랑 중복되는 내용은 조금 생략했다.



결과

아직 마커는 손을 좀 봐야하지만 그래도 디자인과 거의 비슷하게 구현되었다! 🎉

profile
Android Developer

0개의 댓글