Skip to content

Commit 0e5e093

Browse files
committed
feat: 파이 차트 생성하는 CustomPainter 구현
1 parent 781ca15 commit 0e5e093

File tree

1 file changed

+260
-0
lines changed

1 file changed

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

0 commit comments

Comments
 (0)