Flutter CustomPainter 및 차트 그려보기
- custom painter란?
- canvas, paint
- drawLine 등 메소드 사용해보기
- 그래프 그려보기
이 글에 쓰인 소스 코드는 Github 에서찾아보실 수 있습니다.
플러터를 쓰다보니 입맛에 딱 맞는 그래프 라이브러리가 없더군요.
그래서 커스텀 페인터를 공부해서 직접 차트를 만들어보고 있습니다.
그 과정에서 정리한 걸 공유하려고 합니다!
글을 마칠 때는 아래 같은 차트를 그릴 수 있게 될 거에요.
그럼 시작해 볼께요.
1.CustomPainter 란?
커스텀 페인터는 화면에 직접 UI를 그릴 때 사용합니다. 기존의 UI로 만들기 어려운 화면을 만들고 싶을 때 유용해요.
개인적으론 그래프를 그릴 때 사용했습니다.
직접 UI를 그릴려면 CustomPaint와 CustomPainter 클래스가 있어야 합니다.
용어가 비슷해서 헷갈리는데 정리해볼께요.
1.1CustomPainter와 CustomPaint
CustomPaint는 Container나, Center 같은 위젯이에요.
CustomPaint는 CustomPainter를 담는 그릇이라고 보면 됩니다.
CustomPaint
- Container나, Center 같은 위젯입니다.
- painter를 갖고 있습니다. painter에게 화면을 그리도록 합니다.
- painter -> child -> foregroundPainter 순으로 화면을 그립니다.
CustomPainter
- canvas, paint, size등을 통해 실제 화면을 그릴 때 쓰인다.
- 선 그리기 (drawLine), 원 그리기 (drawCircle) 등 다양한 그리기 함수를 지원합니다.
정의만 보니 어떤 뜻인지 잘 모르겠죠?
실제 코드로 확인해 볼께요.
화면에 직선을 그리는 코드입니다.
완성되면 아래 그림처럼 나올 거에요.
1.2.1 CustomPainter - 스크린에 선 긋기
xclass MyPainter extends CustomPainter {
void paint(Canvas canvas, Size size) {
Paint paint = Paint() // Paint 클래스는 어떤 식으로 화면을 그릴지 정할 때 쓰임.
..color = Colors.deepPurpleAccent // 색은 보라색
..strokeCap = StrokeCap.round // 선의 끝은 둥글게 함.
..strokeWidth = 4.0; // 선의 굵기는 4.0
Offset p1 = Offset(0.0, 0.0); // 선을 그리기 위한 좌표값을 만듬.
Offset p2 = Offset(size.width, size.height);
canvas.drawLine(p1, p2, paint); // 선을 그림.
}
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}
일단 CustomPainter 클래스를 정의해볼께요.
CustomPainter는 paint(Canvas canvas, Size size) 와 shouldRepaint(CustomPainter oldDelegate)를 구현해야합니다.
- paint()는 화면을 그릴 때 사용합니다. 화면을 새로 그릴때마다 호출되죠.
- shouldRepaint()는 화면을 새로 그릴지 말지 정합니다. 예전에 위젯의 좌표값과 비교해, 좌표값이 변했을 때 그린다든지 원하는 대로 조건을 줄 수 있죠.
paint(Canvas canvas, Size size) 은 canvas 객체를 써서 화면을 그립니다.
canvas는 다양한 그리기용 함수를 지원합니다. 선그리기, 사각형 그리기 등 다양하죠.
xxxxxxxxxx
canvas.drawLine(p1, p2, paint);
canvas.drawRect(rect, paint);
canvas는 항상 Paint클래스를 객체로 받는데요. Paint 클래스는 화면이 어떤식으로 그려질지 정합니다.
xxxxxxxxxx
Paint paint = Paint() // Paint 클래스는 어떤 식으로 화면을 그릴지 정할 때 쓰임.
..color = Colors.red // 색은 빨강색
..strokeWidth = 8.0 // 굵기는 8.0
..strokeCap = StrokeCap.round; // 선의 끝은 둥글게
그럼 방금 만든 MyPainter() 클래스를 사용해 보겠습니다.
1.2.2 CustomPaint 위젯
xxxxxxxxxx
class PainterPage extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Chart Page"),
),
body: CustomPaint(
size: Size(200, 200), // 위젯의 크기를 정함.
painter: MyPainter(), // painter에 그리기를 담당할 클래스를 넣음.
),
);
}
}
CustomPaint() 위젯에 좀 전에 만든 MyPainter() 클래스를 넣어주면 됩니다.
그림처럼 나오면 성공입니다.
1.2.3 CustomPaint 위젯 - painter와 foregroundPainter의 차이
CustomPaint()는 위젯의 크기를 정하고, 어떤 painter를 쓸지 결정합니다.
painter는 2 종류가 있습니다.
foregroundPainter, painter인데요.
위젯이 그려지는 순서와 관련이 있습니다.
- painter : painter -> child 순으로 그려진다. 나중에 그려지는 위젯이 맨앞에 보인다.
xxxxxxxxxx
CustomPaint(
size: Size(200, 200),
painter: MyPainter(), // painter -> child 순으로 그려진다. MyPainter()에서 선을 그림
child: Container( // 더 늦게 그려지니 화면에 보인다.
width: 200,
height: 200,
color: Colors.red,
),
)
선이 보이지 않네요. child 위젯이 가려서 그렇습니다.
- foregroundPainter: child -> foregroundPainter 순으로 그려짐.
xxxxxxxxxx
CustomPaint(
size: Size(200, 200),
foregroundPainter: MyPainter(), // child -> foregroundPainter 순으로 그려진다. MyPainter()에서 선을 그림
child: Container( // child가 먼저 그려지니, 선이 화면에 보이게 된다.
width: 200,
height: 200,
color: Colors.red,
),
)
선이 잘 보이죠? CustomPainter(커스텀 페인터)가 child에 가려지는 걸 원치 않으면 foregroundPainter를 써주세요.
1.2.4 CustomPaint 위젯 - 위젯 크기 정하기
커스텀 페인트을 쓰다보면 헷갈리는 부분이 있습니다.
바로 위젯의 크기입니다.
크기가 부모나 자식 위젯때문에 쉽게 변하기에 혼란스럽죠.
위젯의 크기는 기본적으로 부모 > 자식(child) > size (CustomPaint의 속성) 값을 따릅니다.
부모가 크기가 제일 중요하고, 그 다음이 자식이고, 아무것도 없으면 size 값에 따라 위젯의 크기를 정합니다. size도 없으면 위젯이 그려지지 않습니다.
부모, 자식, Size()가 다 있는 경우
xxxxxxxxxx
Container(
width: 300,
height: 300,
child: CustomPaint(
size: Size(200, 200),
foregroundPainter: MyPainter(),
child: Container(
width: 150,
height: 150,
),
),
)
부모가 있을 때는 부모의 크기 (넓이 300, 높이 300)로 위젯이 그려집니다.
자식과 Size()만 있는 경우
xxxxxxxxxx
CustomPaint(
size: Size(200, 200),
foregroundPainter: MyPainter(),
child: Container(
width: 150,
height: 150,
),
)
자식이 있을 때는 자식의 크기 (넓이 150, 높이 150)로 위젯이 그려집니다.
Size()만 있는 경우
xxxxxxxxxx
CustomPaint(
size: Size(200, 200),
foregroundPainter: MyPainter(),
)
Size만 있을 때는 Size의 크기에 따라 (넓이 150, 높이 150)로 위젯이 그려집니다.
Size가 우선순위가 제일 낮습니다.
2.그래프(차트 ) 그려보기
2.1 - 파이 차트 그려보기
CustomPainter를 써서 직접 차트를 그려볼께요.
가장 먼저 그릴 차트는 파이 (원) 차트 입니다.
완성되면 아래 그림처럼 나올거에요.
그림 2-1. pie chart
프로젝트 구조는 아래와 같습니다.
ui와 chart만 있는 간단한 구조입니다.
일단 main을 정의해주세요.
2.1 main.dart
xxxxxxxxxx
import 'package:flutter/material.dart';
import 'package:flutter_sketcher_app/ui/chart_page.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
title: 'Chart Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: ChartPage(),
debugShowCheckedModeBanner: false,
);
}
}
대부분의 코드는 chart_page.dart에 작성할 거에요.
2.1 chart_page.dart
x
class ChartPage extends StatelessWidget {
List<double> points = [50, 0, 73, 100,150, 120, 200, 80];
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Chart Page"),
),
body: Container(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Center(
child: Column(
children: <Widget>[
Container(
child: CustomPaint( // CustomPaint를 그리고 이 안에 차트를 그려줍니다..
size: Size(150, 150), // CustomPaint의 크기는 가로 세로 150, 150으로 합니다.
painter: PieChart(percentage: 50, // 파이 차트가 얼마나 칠해져 있는지 정하는 변수입니다.
textScaleFactor: 1.0, // 파이 차트에 들어갈 텍스트 크기를 정합니다.
textColor: Colors.blueGrey),
),
),
],
),
),
),
);
}
}
파이 차트는 원을 2개 그리고, 그 안에 텍스트를 그려서 만듭니다.
원을 그릴 때는 drawCircle() 와 drawArc() 함수를 사용합니다.
drawCircle()는 정해진 위치에 원을 그릴 때 쓰고,
drawArc()는 타원이나, 열린 원(끝이 닫히지 않은 원)을 그릴 때 씁니다.
2.1 pie_chart.dart
xxxxxxxxxx
class PieChart extends CustomPainter {
int percentage = 0;
double textScaleFactor = 1.0;
void paint(Canvas canvas, Size size) {
Paint paint = Paint() // 화면에 그릴 때 쓸 Paint를 정의합니다.
..color = Colors.orangeAccent
..strokeWidth = 10.0 // 선의 길이를 정합니다.
..style = PaintingStyle.stroke // 선의 스타일을 정합니다. stroke면 외곽선만 그리고, fill이면 다 채웁니다.
..strokeCap = StrokeCap.round; // stroke의 스타일을 정합니다. round를 고르면 stroke의 끝이 둥글게 됩니다.
double radius = min(size.width / 2 - paint.strokeWidth / 2 , size.height / 2 - paint.strokeWidth/2); // 원의 반지름을 구함. 선의 굵기에 영향을 받지 않게 보정함.
Offset center = Offset(size.width / 2, size.height/ 2); // 원이 위젯의 가운데에 그려지게 좌표를 정함.
canvas.drawCircle(center, radius, paint); // 원을 그림.
double arcAngle = 2 * pi * (percentage / 100); // 호(arc)의 각도를 정함. 정해진 각도만큼만 그리도록 함.
paint..color = Colors.deepPurpleAccent; // 호를 그릴 때는 색을 바꿔줌.
canvas.drawArc(Rect.fromCircle(center: center, radius: radius), -pi / 2, arcAngle, false, paint); // 호(arc)를 그림.
drawText(canvas, size, "$percentage / 100"); // 텍스트를 화면에 표시함.
}
// 원의 중앙에 텍스트를 적음.
void drawText(Canvas canvas, Size size, String text) {
double fontSize = getFontSize(size, text);
TextSpan sp = TextSpan(style: TextStyle(fontSize: fontSize, fontWeight: FontWeight.bold, color: Colors.black), text: text); // TextSpan은 Text위젯과 거의 동일하다.
TextPainter tp = TextPainter(text: sp, textDirection: TextDirection.ltr);
tp.layout(); // 필수! 텍스트 페인터에 그려질 텍스트의 크기와 방향를 정함.
double dx = size.width / 2 - tp.width / 2;
double dy = size.height / 2 - tp.height / 2;
Offset offset = Offset(dx, dy);
tp.paint(canvas, offset);
}
// 화면 크기에 비례하도록 텍스트 폰트 크기를 정함.
double getFontSize(Size size, String text) {
return size.width / text.length * textScaleFactor;
}
bool shouldRepaint(PieChart old) {
return old.percentage != percentage;
}
}
텍스트를 그리는 부분만 볼께요.
CustomPainter에 텍스트를 적으려면 꼭 TextPainter를 써야 합니다.
TextPainter()는 텍스트의 좌표를 정하는데 쓰입니다.
TextPainter를 쓸 땐 꼭 layout() 함수를 호출해줘야 합니다. 그래야 텍스트의 크기와 방향을 에서 알 수 있습니다.
TextSpan()은 기존의 Text() 위젯과 거의 동일합니다.
원과 글자가 제대로 나오나요?
그럼 다음에 그릴 차트는 라인 차트(선) 차트입니다.
2.2 라인 차트 그려보기
라인 차트는 아래의 그림처럼 생겼습니다.
각각의 값이 점(포인트)로 표현되고, 점들을 연결하고 있습니다.
또한 최저값이랑 최고값을 표시합니다.
라인 차트는 파이 차트랑 비슷한데요.
drawArc() 대신에 drawPath()와 drawPoints() 함수를 사용합니다.
drawPath(Path path, Paint paint) 선을 그릴 때 씁니다. path에 있는 좌표를 따라서 선을 그립니다.
x
Paint paint = Paint()
..strokeWidth = 5.0
..style = PaintingStyle.stroke
..color = Colors.red;
Path path = Path();
path.moveTo(0.0, 0.0); // (0.0, 0.0) 좌표로 이동
path.lineTo(120.0, 120.0); // 시작점에서 (120.0, 120.0) 까지 선을 그음
canvas.drawPath(path, paint);
drawPoints(PointMode pointMode, List
points, Paint paint) 은 점을 그릴 때 씁니다. x
Paint paint = Paint()
..strokeCap = StrokeCap.round // 선이 연결되는 지점이 둥글게 되도록 합니다. 점을 원으로 찍기 위해서 씁니다.
..color = Colors.yellow
..strokeWidth = 30;
List<Offset> offsetPoints = [];
offsetPoints.add(Offset(10, 10)); // 점의 위치를 더합니다.
offsetPoints.add(Offset(60, 60));
offsetPoints.add(Offset(160, 160));
canvas.drawPoints(PointMode.points, offsetPoints, paint); // 점을 화면에 그립니다.
라인 차트에서 쓰는 주요 함수를 알아봤으니 직접 차트를 그려볼께요.
일단 chart_page.dart 에 라인 차트를 포함해주세요.
2.2 chart_page.dart
xxxxxxxxxx
class ChartPage extends StatelessWidget {
List<double> points = [50, 0, 73, 100, 150, 120, 200, 80];
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Chart Page"),
),
body: Container(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Center(
child: Column(
children: <Widget>[
Container(
child: CustomPaint(
size: Size(200, 200),
foregroundPainter: LineChart(
points: points,
pointSize: 15.0, // 점의 크기를 정합니다.
lineWidth: 5.0, // 선의 굵기를 정합니다.
lineColor: Colors.purpleAccent, // 선의 색을 정합니다.
pointColor: Colors.purpleAccent)), // 점의 색을 정합니다.
)
],
),
),
),
);
}
}
line_chart는 크게 4개의 함수로 되어 있습니다.
- getCoordinates() : 좌표 구하기
- drawText() : 텍스트 그리기
- drawLines(): 선 그리기
- drawPoints(): 점 그리기
2.2 line_chart.dart
xxxxxxxxxx
class LineChart extends CustomPainter {
List<double> points;
double lineWidth;
double pointSize;
Color lineColor;
Color pointColor;
int maxValueIndex;
int minValueIndex;
double fontSize = 18.0;
LineChart({this.points, this.pointSize, this.lineWidth, this.lineColor, this.pointColor});
void paint(Canvas canvas, Size size) {
List<Offset> offsets = getCoordinates(points, size); // 점들이 그려질 좌표를 구합니다.
drawText(canvas, offsets); // 텍스트를 그립니다. 최저값과 최고값 위아래에 적은 텍스트입니다.
drawLines(canvas, size, offsets); // 구한 좌표를 바탕으로 선을 그립니다.
drawPoints(canvas, size, offsets); // 좌표에 따라 점을 그립니다.
}
void drawLines(Canvas canvas, Size size, List<Offset> offsets) {
Paint paint = Paint()
..color = lineColor
..strokeWidth = lineWidth
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
Path path = Path();
double dx = offsets[0].dx;
double dy = offsets[0].dy;
path.moveTo(dx, dy);
offsets.map((offset) => path.lineTo(offset.dx , offset.dy)).toList();
canvas.drawPath(path, paint);
}
void drawPoints(Canvas canvas, Size size, List<Offset> offsets) {
Paint paint = Paint()
..color = pointColor
..strokeCap = StrokeCap.round
..strokeWidth = pointSize;
canvas.drawPoints(PointMode.points, offsets, paint);
}
List<Offset> getCoordinates(List<double> points, Size size) {
List<Offset> coordinates = [];
double spacing = size.width / (points.length - 1); // 좌표를 일정 간격으로 벌리지 위한 값을 구합니다.
double maxY = points.reduce(max); // 데이터 중 최소값을 구합니다.
double minY = points.reduce(min); // 데이터 중 최대값을 구합니다.
double bottomPadding = fontSize * 2; // 텍스트가 들어갈 패딩(아랫쪽)을 구합니다.
double topPadding = bottomPadding * 2; // 텍스트가 들어갈 패딩(위쪽)을 구합니다.
double h = size.height - topPadding; // 패딩을 제외한 화면의 높이를 구합니다.
for (int index = 0; index < points.length; index++) {
double x = spacing * index; // x축 좌표를 구합니다.
double normalizedY = points[index] / maxY; // 정규화한다. 정규화란 [0 ~ 1] 사이가 나오게 값을 변경하는 것.
double y = getYPos(h, bottomPadding, normalizedY); // Y축 좌표를 구합니다. 높이에 비례한 값입니다.
Offset coord = Offset(x, y);
coordinates.add(coord);
findMaxIndex(points, index, maxY, minY); // 텍스트(최대값)를 적기 위해, 최대값의 인덱스를 구해놓습니다.
findMinIndex(points, index, maxY, minY); // 텍스트(최소값)를 적기 위해, 최대값의 인덱스를 구해놓습니다.
}
return coordinates;
}
double getYPos(double h, double bottomPadding, double normalizedY) => (h + bottomPadding) - (normalizedY * h);
void findMaxIndex(List<double> points, int index, double maxY, double minY) {
if (maxY == points[index]) {
maxValueIndex = index;
}
}
void findMinIndex(List<double> points, int index, double maxY,double minY) {
if (minY == points[index]) {
minValueIndex = index;
}
}
void drawText(Canvas canvas, List<Offset> offsets) {
String maxValue = points.reduce(max).toString();
String minValue = points.reduce(min).toString();
drawTextValue(canvas, minValue, offsets[minValueIndex], false);
drawTextValue(canvas, maxValue, offsets[maxValueIndex], true);
}
void drawTextValue(Canvas canvas, String text, Offset pos, bool textUpward) {
TextSpan maxSpan = TextSpan(style: TextStyle(fontSize: fontSize, color: Colors.black, fontWeight: FontWeight.bold), text: text);
TextPainter tp = TextPainter(text: maxSpan, textDirection: TextDirection.ltr);
tp.layout();
double y = textUpward ? -tp.height * 1.5 : tp.height * 0.5; // 텍스트의 방향을 고려해 y축 값을 보정해줍니다.
double dx = pos.dx - tp.width / 2; // 텍스트의 위치를 고려해 x축 값을 보정해줍니다.
double dy = pos.dy + y;
Offset offset = Offset(dx, dy);
tp.paint(canvas, offset);
}
bool shouldRepaint(LineChart oldDelegate) {
return oldDelegate.points != points;
}
}
차트가 잘 그려지나요?
이번엔 바 차트(막대 그래프)를 만들어보겠습니다
2.3. 바 차트 그려보기
바 차트는 가로와 세로에 텍스트를 적을 수 있게 만들었습니다.
연도별 제품 판매량이나 생산량 같은 걸 나타내기에 좋습니다.
2.3 chart_page.dart
xxxxxxxxxx
class ChartPage extends StatelessWidget {
List<double> points = [50, 90, 1003, 500, 150, 120, 200, 80];
List<String> labels = [ // 가로축에 적을 텍스트(레이블)
"2012",
"2013",
"2014",
"2015",
"2016",
"2017",
"2018",
"2019",
];
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Chart Page"),
),
body: Container(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Center(
child: Column(
children: <Widget>[
Container(
child: CustomPaint(
size: Size(300, 300),
foregroundPainter: BarChart(
data: points,
labels: labels,
color: Colors.pinkAccent)), // color - 막대 그래프의 색깔
)
],
),
),
),
);
}
}
바 차트는 3부분으로 되어 있습니다.
- 좌표를 구하는 부분 - 어디다 그래프를 그릴지 정합니다.
- 그래프를 그리는 부분 - 실제 막대 그래프를 그립니다. 그래프를 그릴 때는 drawRect() 함수를 사용합니다.
- 텍스트를 그리는 부분 - x축과 y축에 텍스트를 그립니다. 이번엔 정해진 폰트 크기가 아니라, 화면 크기에 비례해 폰트 크기가 정해지도록 계산합니다.
바 차트는 길어서 설명 보다는 주석으로 대체하였습니다.
코드를 따라하면서 익히시기를 권합니다.
2.3 bar_chart.dart
x
class BarChart extends CustomPainter {
Color color;
double textScaleFactorXAxis = 1.0; // x축 텍스트의 비율을 정함.
double textScaleFactorYAxis = 1.2; // y축 텍스트의 비율을 정함.
List<double> data = [];
List<String> labels = [];
double bottomPadding = 0.0;
double leftPadding = 0.0;
BarChart({this.data, this.labels, this.color = Colors.blue});
void paint(Canvas canvas, Size size) {
setTextPadding(size); // 텍스트를 공간을 미리 정함.
List<Offset> coordinates = getCoordinates(size);
drawBar(canvas, size, coordinates);
drawXLabels(canvas, size, coordinates);
drawYLabels(canvas, size, coordinates);
drawLines(canvas, size, coordinates);
}
void setTextPadding(Size size) {
bottomPadding = size.height / 10; // 세로 크기의 1/10만큼만 텍스트 패딩을 줌
leftPadding = size.width / 10; // 가로 길이의 1/10만큼 텍스트 패딩을 줌
}
void drawBar(Canvas canvas, Size size, List<Offset> coordinates) {
Paint paint = Paint()
..color = color
..style = PaintingStyle.fill
..strokeCap = StrokeCap.round;
double barWidthMargin = (size.width * 0.09); // 막대 그래프가 겹치지 않게 간격을 줌.
for (var index = 0; index < coordinates.length; index++) {
Offset offset = coordinates[index];
double left = offset.dx;
double right = offset.dx + barWidthMargin; // 간격만큼 가로로 이동
double top = offset.dy;
double bottom = size.height - bottomPadding; // 텍스트 크기만큼 패딩을 빼줘서, 텍스트와 겹치지 않게 함.
Rect rect = Rect.fromLTRB(right, top, left, bottom);
canvas.drawRect(rect, paint);
}
}
// X축 텍스트(레이블)을 그림.
void drawXLabels(Canvas canvas, Size size, List<Offset> coordinates) {
double fontSize = calculateFontSize(labels[0], size, xAxis: true); // 화면 크기에 유동적으로 폰트 크기를 계산함.
for (int index = 0; index < labels.length; index++) {
TextSpan span = TextSpan(
style: TextStyle(
color: Colors.black,
fontSize: fontSize,
fontFamily: 'Roboto',
fontWeight: FontWeight.w400),
text: labels[index]);
TextPainter tp = TextPainter(text: span, textDirection: TextDirection.ltr);
tp.layout();
Offset offset = coordinates[index];
double dx = offset.dx;
double dy = size.height - tp.height;
tp.paint(canvas, Offset(dx, dy));
}
}
// Y축 텍스트(레이블)을 그림. 최저값과 최고값을 Y축에 표시함.
void drawYLabels(Canvas canvas, Size size, List<Offset> coordinates) {
double bottomY = coordinates[0].dy;
double topY = coordinates[0].dy;
int indexOfMax = 0;
int indexOfMin = 0;
for (int index = 0; index < coordinates.length; index++) {
double dy = coordinates[index].dy;
if (bottomY < dy) {
bottomY = dy;
indexOfMin = index;
}
if (topY > dy) {
topY = dy;
indexOfMax = index;
}
}
String maxValue = "${data[indexOfMax].toInt()}";
String minValue = "${data[indexOfMin].toInt()}";
double fontSize = calculateFontSize(maxValue, size, xAxis: false);
drawYText(canvas, maxValue, fontSize, topY);
drawYText(canvas, minValue, fontSize, bottomY);
}
// 화면 크기에 비례해 폰트 크기를 계산.
double calculateFontSize(String value, Size size, {bool xAxis}) {
int numberOfCharacters = value.length; // 글자수에 따라 폰트 크기를 계산하기 위함.
double fontSize = (size.width / numberOfCharacters) / data.length; // width가 600일 때 100글자를 적어야 한다면, fontSize는 글자 하나당 6이어야겠죠.
if (xAxis) {
fontSize *= textScaleFactorXAxis;
} else {
fontSize *= textScaleFactorYAxis;
}
return fontSize;
}
// x축과 y축을 구분하는 선을 긋습니다.
void drawLines(Canvas canvas, Size size, List<Offset> coordinates) {
Paint paint = Paint()
..color = Colors.blueGrey[100]
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke
..strokeWidth = 1.8;
double bottom = size.height - bottomPadding;
double left = coordinates[0].dx;
Path path = Path();
path.moveTo(left, 0);
path.lineTo(left, bottom);
path.lineTo(size.width, bottom);
canvas.drawPath(path, paint);
}
void drawYText(Canvas canvas, String text, double fontSize, double y) {
TextSpan span = TextSpan(
style: TextStyle(
fontSize: fontSize,
color: Colors.black,
fontFamily: 'Roboto',
fontWeight: FontWeight.w600),
text: text,
);
TextPainter tp = TextPainter(text: span, textDirection: TextDirection.ltr);
tp.layout();
Offset offset = Offset(0.0, y);
tp.paint(canvas, offset);
}
List<Offset> getCoordinates(Size size) {
List<Offset> coordinates = [];
double maxData = data.reduce(max);
double width = size.width - leftPadding;
double minBarWidth = width / data.length;
for (var index = 0; index < data.length; index++) {
double left = minBarWidth * (index) + leftPadding; // 그래프의 가로 위치를 정합니다.
double normalized = data[index] / maxData; // 그래프의 높이가 [0~1] 사이가 되도록 정규화 합니다.
double height = size.height - bottomPadding; // x축에 표시되는 글자들과 겹치지 않게 높이에서 패딩을 제외합니다.
double top = height - normalized * height; // 정규화된 값을 통해 높이를 구해줍니다.
Offset offset = Offset(left, top);
coordinates.add(offset);
}
return coordinates;
}
bool shouldRepaint(BarChart old) {
return old.data != data;
}
}
이제 커스텀 페인터를 쓰는 데 익숙해 졌나요?
이번 글에선 직접 차트를 만들어 보았습니다.
다음 번엔 애니메이션에 대해 알아보도록 하겠습니다.
이 글에 쓰인 소스 코드는 Github 에서찾아보실 수 있습니다.
'플러터(Flutter)' 카테고리의 다른 글
Flutter - Provider 패턴에 대해서 알아보자 (11) | 2019.08.06 |
---|---|
Flutter - 다국어 지원하기 (Localization and Internalization) (4) | 2019.07.02 |
Flutter - 유닛 테스트 해보기 (1) | 2019.04.22 |
Flutter - Zone이란? 프로그램 종료되지 않게 예외처리 하기. (1) | 2019.04.15 |
Flutter - 플러터에서 리액티브 프로그래밍, Stream과 Bloc 패턴 적용하기 (11) | 2018.11.16 |