일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- Kotlin
- 테스트
- Coroutines
- 앱바
- viewmodel
- 계측
- appbar
- android
- activity
- livedata
- 앱
- binding
- tabbar
- Navigation
- CustomScrollView
- data
- drift
- DART
- 안드로이드
- ScrollView
- Compose
- Button
- textfield
- scroll
- Flutter
- Dialog
- LifeCycle
- TEST
- intent
- textview
- Today
- Total
Study Record
[안드로이드] 꺽은선 그래프 직접 그리기 - canvas 본문
안드로이드 스튜디오에서 직접 그래프를 그려보자!
먼저, 그래프를 그리기 위한 새로운 클래스를 만든다.
class MyCanvas @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null, defStyleAttr: Int = 0)
: View(context, attrs, defStyleAttr){
// 뷰의 내용이 렌더링 될때 호출 된다.
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
// 그리고 싶은대로 그린다.
}
}
onDraw() 함수 안에 그래프를 만들 코드를 작성해주면 된다.
기본적으로 canvas 로 그림을 그리기 위해서 Paint() 객체로 그림을 그릴 선 혹은 원, 글씨 등의 타입을 정해줘야 한다.
그래프에 그릴 직선과 원, 점선, 글자가 필요하다.
// 원을 그릴 때 사용할 Paint 객체
private var circlePoint = Paint().apply {
color = Color.parseColor("#684923") // 선 색깔
style = Paint.Style.STROKE // 채우지는 않고 외곽선만 그린다.
strokeWidth = 10f // 외곽선 너비
}
// 글자를 그릴 때 사용할 Paint 객체
private var textPoint = Paint().apply {
textAlign = Paint.Align.CENTER // 글자 가운데 정렬
textSize = 15f
color = Color.parseColor("#A3A3A3")
}
// 직선 그릴때 사용할 Paint 객체
private var linePoint = Paint().apply {
strokeWidth = 3f
color = Color.parseColor("#684923")
}
// 점선 그릴때 사용할 Paint 객체
private var dottedLinePoint = Paint().apply {
strokeWidth = 2f
color = Color.parseColor("#684923")
}
canvas 에 그림은 좌표값으로 그림을 그린다. 좌표값은 뷰의 왼쪽 상단이 (0,0)로 시작한다. 다음 그림과 같다.
canvas로 그릴 수 있는 함수들은 다음과 같다.
class MyCanvas @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null, defStyleAttr: Int = 0)
: View(context, attrs, defStyleAttr){
// 뷰의 내용이 렌더링 될때 호출 된다.
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
// 그리고 싶은대로 그린다.
// 글자 그리기 - text : 쓰고 싶은 글자(String) , startX : x좌표 , startY: Y좌표
canvas.drawText(text, startX, startY, Paint())
// 원 그리기 - startX : x좌표 , startY: Y좌표 , circleRadius : 반지름
canvas.drawCircle(startX, startY, circleRadius, Paint())
// 선 그리기 - startX, endX : x좌표 , startY, endY : Y좌표
canvas.drawLine(startX, startY, endX, endY, Point())
}
}
canvas.drawText()로 글자를 그릴 때, 주의해야할 점이 하나 있다. 앞으로 그릴 글자의 위치는 좌표값이 설정되는 부분이 글자가 차지하는 부분의 정 가운데가 되길 원하지만 실제로는 그렇지 않다.
canvas.drawText("Program", startX, startY, Paint())라면 위 그림의 (x = startX, y = startY) 좌표에 그려진다. 위의 Paint() 객체를 만들때 설정에서 "textAlign = Paint.Align.CENTER" 부분이 있었다. 이 설정은 수평적(가로)으로 가운데로 정렬해줄 수 있다. 따라서 고려해줄건 수직적(세로)으로 가운데 정렬을 해줘야 한다.
좌표값이 설정되는 부분에 글자가 수직적(세로)으로 가운데 정렬을 하기 위해서 canvas.drawText() 가 그려질 글자의 높이(height)를 가져와 y좌표에 "세로 값(height) / 2"를 더하면 된다. 다음은 수직적으로 가운데 정렬된 모양을 갖는 좌표값을 찾는 예시 코드이다.
//val textPaint = Paint()
val exampleText = "Program"
val rect = Rect()
textPoint.getTextBounds(exampleText, 0, exampleText.size, rect)
val textHeight = rect.height() // 높이 값
// y좌표 = Y좌표 + 글자높이 / 2
val y = baseY + (textHeight / 2)
앞으로 그려볼 그래프의 결과는 다음과 같다.
여기서 x 좌표와 y 좌표가 만나는 지점에 원을 그려줘야 한다. 중요한 것은 원이 없다면 그냥 x좌표값과 y좌표값을 이용해서 직선을 그리기만 하면 된다. 하지만, 원 모양으로 데이터 부분이 표시되어야 한다면, 원이 그려지는 만큼 직선이 그려지면 안된다. 이 때, 생각한 것이 학창시절 배웠던 비례식을 생각해봤다.
위의 그림처럼 두 원이 있을 때, 그 사이의 삼각형을 만들고 위의 비례식을 참고하면,
DistanceY = ( Y * R ) / h
DistanceX = ( X * R ) / h
이렇게 두 길이를 알 수 있다. 따라서 우리가 구하고 싶은 값은 아래 그림의 노란색 점과 초록색 점을 구할 수 있다.
물론, 위의 예시는 그래프가 올라가는 방향일 때의 경우이다. 그래프가 내려가는 방향일때는
노란색 점 = ( X1 + DistanceX, Y1 + DistanceY)
초록색 점 = ( X1 + DistanceX, Y1 - DistanceY)
이렇게 될 것이다.
이제 고려해야 할 개념적인 부분은 끝났다. 아래 그림은 그래프를 만들 때, 사용했던 변수의 이름과 정의이다.
개발한 코드
MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val graph = findViewById<MyCanvas>(R.id.my_graph)
graph.setSize()
graph.draw()
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.example.graph.MyCanvas
android:id="@+id/my_graph"
android:layout_centerInParent="true"
android:layout_width="350dp"
android:layout_height="300dp"/>
</RelativeLayout>
MyCanvas 클래스
class MyCanvas @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null, defStyleAttr: Int = 0)
: View(context, attrs, defStyleAttr){
private var mHeight = 0f // 그래프 Y축 길이
private var mWidth = 0f // 그래프 X축 길이
private var mWidthStep = 0f // 그래프 X축 요소 사이의 길이
private var mHeightStep = 0f // 그래프 Y축 요소 사이 길이
private var mYNum = 0 // 그래프 Y축 요소 개수
private var circleRadius = 10f // 원 반지름
private var startTextYPoint = 0f
private var startYPoint = 0f // 그래프 시작 Y 지점
private var startXPoint = 0f // 그래프 시작 X 지점
private var circlePoint = Paint().apply {
color = Color.parseColor("#684923")
style = Paint.Style.STROKE
strokeWidth = 5f
}
private var textPoint = Paint().apply {
textAlign = Paint.Align.CENTER
textSize = dpToPx(13f)
color = Color.parseColor("#A3A3A3")
}
private var linePoint = Paint().apply {
strokeWidth = 3f
color = Color.parseColor("#684923")
}
private var dottedLinePoint = Paint().apply {
strokeWidth = 2f
color = Color.parseColor("#684923")
}
// 뷰의 내용이 렌더링 될때 호출 된다.
@SuppressLint("DrawAllocation")
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
// 그리고 싶은대로 그린다.
val Data = ArrayList<Int>().apply {
for(i in 1..7) add(Random().nextInt(5)) // 0~7
}
val xData = arrayListOf("월", "화", "수", "목", "금", "토", "일")
// x축 그리기
canvas?.drawLine(startXPoint, mHeight, startXPoint + mWidth, mHeight, linePoint)
// y축 그리기
canvas?.drawLine(startXPoint, 0f, startXPoint, mHeight, linePoint)
var dottedStep = mHeight / 100 // 점선에서 점 하나의 길이 , 점은 총 50개로 그린다.
for(i in 0 until xData.size){
val x = startXPoint + (i+1)*mWidthStep
// x축 글자
canvas?.drawText(xData[i], x, startTextYPoint, textPoint)
// 데이터 원 그리기
canvas?.drawCircle(x, startYPoint + (Data[i]*mHeightStep), circleRadius, circlePoint)
// 세로 점선 그리기
for(k in 0..99 step(2)) {
canvas?.drawLine(x, dottedStep * k, x, dottedStep * (k + 1), dottedLinePoint)
}
}
// y축 글자
dottedStep = mWidth / 100 // 점선에서 점 하나의 길이 , 점은 총 50개로 그린다.
for(i in 1..mYNum){
val y = startYPoint + (i-1)*mHeightStep
val x = startXPoint / 2
val rect = Rect()
textPoint.getTextBounds((6-i).toString(), 0, 1, rect)
val textHeight = rect.height()
// y 축 글자 그리기
canvas?.drawText((6-i).toString(), x, y + (textHeight/2), textPoint)
// 가로 점선 그리기
for(k in 0..99 step(2)) {
// 가로 점선 그리기
canvas?.drawLine(startXPoint + dottedStep * k, y, startXPoint + dottedStep * (k + 1), y, dottedLinePoint)
}
}
// 원과 원사이 직선 그리기
for(i in 1 until Data.size){
// 5개면 0~4 Index , 값은 1 ,2, 3, 4 로 4개
val startX = startXPoint + (i*mWidthStep)
val startY = startYPoint + (Data[i-1]*mHeightStep)
val endX = startXPoint + (i + 1) * mWidthStep
val endY = startYPoint + (Data[i]*mHeightStep)
val BigDistanceX = mWidthStep
val BigDistanceY = abs((Data[i-1]*mHeightStep) - (Data[i]*mHeightStep))
val hypotenuse = sqrt(BigDistanceX * BigDistanceX + BigDistanceY * BigDistanceY)
val distanceX = (circleRadius * BigDistanceX) / hypotenuse
val distanceY = (circleRadius * BigDistanceY) / hypotenuse
if(Data[i - 1] > Data[i]){
// 올라가는 모양일때
canvas?.drawLine((startX + distanceX), (startY - distanceY), (endX - distanceX), (endY + distanceY), linePoint)
} else if(Data[i - 1] < Data[i]){
// 내려가는 모양일때
canvas?.drawLine((startX + distanceX), (startY + distanceY), (endX - distanceX), (endY - distanceY), linePoint)
} else {
canvas?.drawLine(startX + distanceX, startY, endX - distanceX, endY, linePoint)
}
}
}
fun setSize(height: Float = 250f, width: Float = 350f, XNum: Int = 8, YNum: Int = 5, textXAreaHeight: Float = 50f) {
// 전체 단위는 dp 로 받는다.
// height : y 축 세로 길이 , width : x축 가로 길이, step : 한 요소 사이 길이(width) , YNum : Y축 요소 개수, textAreaHeight : x축 요소의 공간
mHeight = dpToPx(height)
startXPoint = dpToPx(30f)
mWidth = dpToPx(width - 30f)
mWidthStep = mWidth / XNum
mYNum = YNum
startYPoint = dpToPx(20f)
mHeightStep = (mHeight - startYPoint) / YNum
startTextYPoint = dpToPx(height + (textXAreaHeight / 2))
}
fun draw(){
invalidate()
}
fun dpToPx(dp: Float): Float{
val dm = this.resources.displayMetrics
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, dm)
}
}
< 실행 결과 화면 >
'안드로이드' 카테고리의 다른 글
Button 색상 바꾸기 , 바탕색(windowBackground) (0) | 2022.04.07 |
---|---|
[안드로이드] 쥬디의 찜질방 게임 만들기 - 5. 게임 종료 (0) | 2021.12.27 |
[안드로이드] 쥬디의 찜질방 게임 만들기 - 4. 게임 만들기 (2) (0) | 2021.12.26 |
[안드로이드] 쥬디의 찜질방 게임 만들기 - 4. 게임 만들기 (1) (0) | 2021.12.23 |
[안드로이드] 쥬디의 찜질방 게임 만들기 - 3. 게임 화면 셋팅 (0) | 2021.12.23 |