Skip to content

Commit 58b3515

Browse files
authored
feat: 파이 차트 생성하는 CustomPainter 구현 (#21)
* feat: 파이 차트 생성하는 CustomPainter 구현 * fix: 리스트 비교 오류 수정
1 parent 2bcd0c7 commit 58b3515

File tree

1 file changed

+261
-0
lines changed

1 file changed

+261
-0
lines changed
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter/foundation.dart';
3+
import 'dart:math';
4+
5+
class CustomChartPainter extends CustomPainter {
6+
final List<double> percentages;
7+
8+
CustomChartPainter({required this.percentages});
9+
10+
@override
11+
void paint(Canvas canvas, Size size) {
12+
final Offset center = Offset(size.width / 2, size.height / 2);
13+
final double outerRadius = size.width / 2;
14+
final double innerRadius = outerRadius * 0.65;
15+
16+
final int segmentCount = percentages.length;
17+
final double totalAngle = 2 * pi;
18+
final double gapAngle = 0.185;
19+
final double totalGaps = gapAngle * segmentCount;
20+
21+
final double totalPercentage = percentages.reduce((a, b) => a + b);
22+
final List<double> normalizedPercentages = percentages
23+
.map((p) => p / totalPercentage)
24+
.toList();
25+
26+
final List<double> segmentAngles = normalizedPercentages
27+
.map((p) => p * (totalAngle - totalGaps))
28+
.toList();
29+
30+
final LinearGradient gradient = LinearGradient(
31+
begin: Alignment.topRight,
32+
end: Alignment.bottomLeft,
33+
colors: [Color(0xFFFD6C01), Color(0xFFBFECFF)],
34+
stops: [0.1, 0.9],
35+
);
36+
37+
final shader = gradient.createShader(
38+
Rect.fromCircle(center: center, radius: outerRadius),
39+
);
40+
final double outerCornerRadius = 10.0;
41+
final double innerCornerRadius = outerCornerRadius * 0.65;
42+
43+
Paint shadowPaint = Paint()
44+
..color = Colors.black26
45+
..strokeWidth = outerRadius - innerRadius + 35
46+
..style = PaintingStyle.stroke
47+
..maskFilter = MaskFilter.blur(BlurStyle.normal, 4.0);
48+
49+
Paint whitePaint = Paint()
50+
..color = Colors.white
51+
..strokeWidth = outerRadius - innerRadius + 35
52+
..style = PaintingStyle.stroke;
53+
54+
Offset shadowOffset = Offset(center.dx, center.dy + 2);
55+
canvas.drawArc(
56+
Rect.fromCircle(
57+
center: shadowOffset,
58+
radius: (outerRadius + innerRadius) / 2,
59+
),
60+
0,
61+
2 * pi,
62+
false,
63+
shadowPaint,
64+
);
65+
66+
canvas.drawArc(
67+
Rect.fromCircle(center: center, radius: (outerRadius + innerRadius) / 2),
68+
0,
69+
2 * pi,
70+
false,
71+
whitePaint,
72+
);
73+
74+
double currentAngle = -pi / 2 + gapAngle / 2;
75+
76+
for (int i = 0; i < segmentCount; i++) {
77+
final double startAngle = currentAngle;
78+
final double endAngle = startAngle + segmentAngles[i];
79+
currentAngle = endAngle + gapAngle;
80+
81+
Path path = Path();
82+
83+
path.arcTo(
84+
Rect.fromCircle(center: center, radius: outerRadius),
85+
startAngle,
86+
segmentAngles[i],
87+
false,
88+
);
89+
90+
path.arcTo(
91+
Rect.fromCircle(center: center, radius: innerRadius),
92+
endAngle,
93+
-segmentAngles[i],
94+
false,
95+
);
96+
97+
final Offset startInner = Offset(
98+
center.dx + innerRadius * cos(startAngle),
99+
center.dy + innerRadius * sin(startAngle),
100+
);
101+
final Offset startOuter = Offset(
102+
center.dx + outerRadius * cos(startAngle),
103+
center.dy + outerRadius * sin(startAngle),
104+
);
105+
106+
final Offset endInner = Offset(
107+
center.dx + innerRadius * cos(endAngle),
108+
center.dy + innerRadius * sin(endAngle),
109+
);
110+
final Offset endOuter = Offset(
111+
center.dx + outerRadius * cos(endAngle),
112+
center.dy + outerRadius * sin(endAngle),
113+
);
114+
115+
path.moveTo(endOuter.dx, endOuter.dy);
116+
path.addOval(
117+
Rect.fromCircle(
118+
center: getPointAtDistance(endOuter, endInner, outerCornerRadius),
119+
radius: outerCornerRadius,
120+
),
121+
);
122+
path.addOval(
123+
Rect.fromCircle(
124+
center: getPointAtDistance(endInner, endOuter, innerCornerRadius),
125+
radius: innerCornerRadius,
126+
),
127+
);
128+
129+
path.addOval(
130+
Rect.fromCircle(
131+
center: getPointAtDistance(startOuter, startInner, outerCornerRadius),
132+
radius: outerCornerRadius,
133+
),
134+
);
135+
path.addOval(
136+
Rect.fromCircle(
137+
center: getPointAtDistance(startInner, startOuter, innerCornerRadius),
138+
radius: innerCornerRadius,
139+
),
140+
);
141+
142+
Offset ovalEndOuter = moveAlongCircle(
143+
center: center,
144+
radius: outerRadius - outerCornerRadius,
145+
startAngle: endAngle,
146+
distance: outerCornerRadius,
147+
);
148+
Offset ovalEndInner = moveAlongCircle(
149+
center: center,
150+
radius: innerRadius + innerCornerRadius,
151+
startAngle: endAngle,
152+
distance: innerCornerRadius,
153+
);
154+
155+
Offset ovalStartOuter = moveAlongCircle(
156+
center: center,
157+
radius: outerRadius - outerCornerRadius,
158+
startAngle: startAngle,
159+
distance: 0,
160+
);
161+
Offset ovalStartInner = moveAlongCircle(
162+
center: center,
163+
radius: innerRadius + innerCornerRadius,
164+
startAngle: startAngle,
165+
distance: 0,
166+
);
167+
168+
path.addPath(
169+
buildSkewedRect(
170+
ovalEndOuter,
171+
ovalEndInner,
172+
outerCornerRadius,
173+
innerCornerRadius,
174+
),
175+
Offset.zero,
176+
);
177+
178+
path.addPath(
179+
buildSkewedRect(
180+
ovalStartOuter,
181+
ovalStartInner,
182+
outerCornerRadius,
183+
innerCornerRadius,
184+
),
185+
Offset.zero,
186+
);
187+
path.close();
188+
189+
final Paint segmentPaint = Paint()
190+
..shader = shader
191+
..style = PaintingStyle.fill;
192+
193+
canvas.drawPath(path, segmentPaint);
194+
}
195+
}
196+
197+
Offset getPointAtDistance(Offset p1, Offset p2, double distance) {
198+
final dx = p2.dx - p1.dx;
199+
final dy = p2.dy - p1.dy;
200+
final len = sqrt(dx * dx + dy * dy);
201+
202+
if (len == 0) return p1; // 두 점이 같을 경우
203+
204+
final ratio = distance / len;
205+
return Offset(p1.dx + dx * ratio, p1.dy + dy * ratio);
206+
}
207+
208+
Path buildSkewedRect(
209+
Offset topLeft,
210+
Offset bottomLeft,
211+
double outerWidth,
212+
double innerWidth,
213+
) {
214+
final dx = bottomLeft.dx - topLeft.dx;
215+
final dy = bottomLeft.dy - topLeft.dy;
216+
final length = sqrt(dx * dx + dy * dy);
217+
218+
final nx = -dy / length;
219+
final ny = dx / length;
220+
221+
final topRight = Offset(
222+
topLeft.dx + nx * outerWidth,
223+
topLeft.dy + ny * outerWidth,
224+
);
225+
final bottomRight = Offset(
226+
bottomLeft.dx + nx * innerWidth,
227+
bottomLeft.dy + ny * innerWidth,
228+
);
229+
230+
final path = Path()
231+
..moveTo(topLeft.dx, topLeft.dy)
232+
..lineTo(bottomLeft.dx, bottomLeft.dy)
233+
..lineTo(bottomRight.dx, bottomRight.dy)
234+
..lineTo(topRight.dx, topRight.dy)
235+
..close();
236+
237+
return path;
238+
}
239+
240+
Offset moveAlongCircle({
241+
required Offset center,
242+
required double radius,
243+
required double startAngle,
244+
required double distance,
245+
}) {
246+
if (radius == 0) return center;
247+
248+
final double deltaAngle = distance / radius;
249+
final double newAngle = startAngle + deltaAngle;
250+
251+
return Offset(
252+
center.dx + radius * cos(newAngle),
253+
center.dy + radius * sin(newAngle),
254+
);
255+
}
256+
257+
@override
258+
bool shouldRepaint(covariant CustomChartPainter oldDelegate) {
259+
return !listEquals(percentages, oldDelegate.percentages);
260+
}
261+
}

0 commit comments

Comments
 (0)