CustomPainter — Canvas로 직접 그리기

기본 위젯만으로 표현하기 어려운 커스텀 그래픽, 차트, 게이지 등을 만들 때 CustomPainter를 사용합니다. HTML Canvas나 Android의 Canvas와 비슷한 개념입니다.


CustomPainter 기본 구조

DART
class MyPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    // 여기서 그리기 작업 수행
    final paint = Paint()
      ..color = Colors.blue
      ..strokeWidth = 2
      ..style = PaintingStyle.stroke;  // stroke: 테두리, fill: 채우기

    canvas.drawRect(
      Rect.fromLTWH(10, 10, size.width - 20, size.height - 20),
      paint,
    );
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return false;  // 다시 그릴 필요가 없으면 false
  }
}

// 사용
CustomPaint(
  painter: MyPainter(),
  size: const Size(300, 200),
)

Paint 속성

DART
final paint = Paint()
  ..color = Colors.blue                    // 색상
  ..strokeWidth = 3                         // 선 두께
  ..style = PaintingStyle.fill             // fill 또는 stroke
  ..strokeCap = StrokeCap.round            // 선 끝 모양
  ..strokeJoin = StrokeJoin.round          // 선 교차점 모양
  ..isAntiAlias = true                      // 안티앨리어싱
  ..shader = LinearGradient(               // 그라데이션
    colors: [Colors.blue, Colors.red],
  ).createShader(Rect.fromLTWH(0, 0, 200, 200))
  ..maskFilter = const MaskFilter.blur(    // 블러
    BlurStyle.normal, 5.0,
  );

기본 도형 그리기

DART
class ShapesPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.blue
      ..style = PaintingStyle.fill;

    // 원
    canvas.drawCircle(
      Offset(size.width / 2, 80),  // 중심점
      50,                            // 반지름
      paint,
    );

    // 사각형
    paint.color = Colors.red;
    canvas.drawRRect(
      RRect.fromRectAndRadius(
        const Rect.fromLTWH(50, 150, 200, 80),
        const Radius.circular(12),
      ),
      paint,
    );

    // 선
    paint
      ..color = Colors.green
      ..style = PaintingStyle.stroke
      ..strokeWidth = 3;
    canvas.drawLine(
      const Offset(0, 250),
      Offset(size.width, 250),
      paint,
    );

    // 타원
    paint
      ..color = Colors.orange
      ..style = PaintingStyle.fill;
    canvas.drawOval(
      const Rect.fromLTWH(50, 270, 200, 60),
      paint,
    );
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

Path로 복잡한 도형

DART
class PathPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.purple
      ..style = PaintingStyle.fill;

    // 삼각형
    final path = Path()
      ..moveTo(size.width / 2, 20)  // 시작점
      ..lineTo(20, size.height - 20)  // 좌하단
      ..lineTo(size.width - 20, size.height - 20)  // 우하단
      ..close();  // 경로 닫기

    canvas.drawPath(path, paint);

    // 베지어 곡선
    final curvePaint = Paint()
      ..color = Colors.teal
      ..style = PaintingStyle.stroke
      ..strokeWidth = 3;

    final curvePath = Path()
      ..moveTo(0, size.height / 2)
      ..quadraticBezierTo(
        size.width / 2, 0,          // 제어점
        size.width, size.height / 2,  // 끝점
      );

    canvas.drawPath(curvePath, curvePaint);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

실전 예제: 원형 프로그레스바

DART
class CircularProgressPainter extends CustomPainter {
  final double progress;  // 0.0 ~ 1.0
  final Color progressColor;
  final Color backgroundColor;
  final double strokeWidth;

  CircularProgressPainter({
    required this.progress,
    this.progressColor = Colors.blue,
    this.backgroundColor = Colors.grey,
    this.strokeWidth = 10,
  });

  @override
  void paint(Canvas canvas, Size size) {
    final center = Offset(size.width / 2, size.height / 2);
    final radius = (size.width - strokeWidth) / 2;

    // 배경 원
    final bgPaint = Paint()
      ..color = backgroundColor.withOpacity(0.3)
      ..style = PaintingStyle.stroke
      ..strokeWidth = strokeWidth;
    canvas.drawCircle(center, radius, bgPaint);

    // 진행률 호
    final progressPaint = Paint()
      ..color = progressColor
      ..style = PaintingStyle.stroke
      ..strokeWidth = strokeWidth
      ..strokeCap = StrokeCap.round;

    canvas.drawArc(
      Rect.fromCircle(center: center, radius: radius),
      -3.14159 / 2,           // 12시 방향에서 시작
      2 * 3.14159 * progress,  // 진행 각도
      false,
      progressPaint,
    );
  }

  @override
  bool shouldRepaint(covariant CircularProgressPainter oldDelegate) {
    return oldDelegate.progress != progress;
  }
}

// 사용
class CircularProgress extends StatelessWidget {
  final double progress;
  const CircularProgress({super.key, required this.progress});

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: 120,
      height: 120,
      child: CustomPaint(
        painter: CircularProgressPainter(
          progress: progress,
          progressColor: Colors.blue,
        ),
        child: Center(
          child: Text(
            '${(progress * 100).toInt()}%',
            style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
          ),
        ),
      ),
    );
  }
}

텍스트 그리기

DART
@override
void paint(Canvas canvas, Size size) {
  final textPainter = TextPainter(
    text: const TextSpan(
      text: 'Canvas 텍스트',
      style: TextStyle(
        color: Colors.black,
        fontSize: 20,
        fontWeight: FontWeight.bold,
      ),
    ),
    textDirection: TextDirection.ltr,
  );

  textPainter.layout(maxWidth: size.width);
  textPainter.paint(canvas, const Offset(10, 10));
}

shouldRepaint 최적화

DART
class OptimizedPainter extends CustomPainter {
  final double value;
  final Color color;

  OptimizedPainter({required this.value, required this.color});

  @override
  void paint(Canvas canvas, Size size) {
    // 그리기 로직
  }

  @override
  bool shouldRepaint(covariant OptimizedPainter oldDelegate) {
    // 값이 바뀔 때만 다시 그리기
    return oldDelegate.value != value || oldDelegate.color != color;
  }
}

면접 포인트: shouldRepaint에서 불필요한 리페인트를 방지하는 것이 성능의 핵심입니다. 항상 true를 반환하면 매 프레임마다 다시 그립니다.


정리

  • CustomPainter로 Canvas에 원, 사각형, 선, 경로 등을 직접 그릴 수 있습니다
  • Paint 객체로 색상, 두께, 채우기/테두리, 그라데이션 등을 설정합니다
  • Path로 복잡한 도형과 베지어 곡선을 그릴 수 있습니다
  • shouldRepaint를 적절히 구현하여 불필요한 리페인트를 방지하세요
  • 차트, 게이지, 커스텀 UI에 활용됩니다
댓글 로딩 중...