나는 여태껏 일반적인 애니메이션 위젯을 통한 처리와 Rive를 통한 처리를 했을 뿐, Custom Paint에 대한 경험은 없어 굉장히 아쉬웠었다.
커스텀 페인터는 플러터에서 제공하는 방법 중 하나로,
개발자가 Canvas 객체를 통해 직접 그리기 로직을 제어할 수 있다.
CustomPainter 위젯을 사용하면 다음과 같은 고급 그래픽 처리가 가능하다:
직접 그리기:
원, 사각형, 경로(다양한 모양) 등 기본적인 도형부터 시작해 복잡한 그래픽도 자유롭게 그릴 수 있다.
상세 조정 가능:
애니메이션의 정확한 타이밍, 속도 조절, 경로 조정 등을 세밀하게 설정할 수 있다.
대화형 기능:
사용자의 입력에 반응하여 동적으로 그래픽을 변경할 수 있다.
예를 들어, 사용자가 화면을 터치할 때마다 물결 효과를 적용할 수 있다.
아래는 간단한 웨이브 효과의
일반 애니메이션은 플러터의 애니메이션 프레임워크를 사용하여 위젯의 속성(위치, 색상, 투명도 등)을 시간에 따라 변화시킬 수 있다.
이는 AnimatedContainer, AnimatedOpacity 등의 위젯을 사용하여 쉽게 구현할 수 있다:
간편한 구현:
애니메이션을 적용할 속성을 정의하고, 시간에 따라 어떻게 변화할지를 설정하기만 하면 된다.
효율적인 성능:
플러터 엔진이 애니메이션의 렌더링을 최적화하므로, 복잡한 계산 없이도 부드러운 애니메이션 효과를 구현할 수 있다.
결론 : 두 개의 작업을 해보면서 커스텀 페인터 없이 복잡한 곡선과 그라디언트 색상 변화등을 순수 애니메이션 위젯을 통한 구현은 어려웠다. 커스텀 페인터는 직접 그래픽을 캔버스에서 조작이 가능했다.
class SimpleWaveAnimation extends StatefulWidget {
const SimpleWaveAnimation({super.key});
_SimpleWaveAnimationState createState() => _SimpleWaveAnimationState();
}
class _SimpleWaveAnimationState extends State<SimpleWaveAnimation> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 1),
vsync: this,
)..repeat(reverse: true);
_animation = CurvedAnimation(parent: _controller, curve: Curves.easeInOut);
}
void dispose() {
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
body: AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Center(
child: Stack(
children: <Widget>[
Positioned(
top: 50 + 30 * sin(_animation.value * 2 * pi),
left: 50 + 30 * cos(_animation.value * 2 * pi),
child: Container(
width: 100,
height: 100,
decoration: const BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
),
),
),
Positioned(
top: 150 - 30 * sin(_animation.value * 2 * pi),
right: 50 - 30 * cos(_animation.value * 2 * pi),
child: Container(
width: 100,
height: 100,
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
),
),
],
),
);
},
),
);
}
}
import 'dart:math';
import 'package:flutter/material.dart';
class FancyBackgroundApp extends StatefulWidget {
const FancyBackgroundApp({super.key});
_FancyBackgroundAppState createState() => _FancyBackgroundAppState();
}
class _FancyBackgroundAppState extends State<FancyBackgroundApp>
with SingleTickerProviderStateMixin {
late AnimationController _controller; // 애니메이션의 시간과 동작을 제어하는 컨트롤러
late Animation<double> _animation; // 애니메이션 값을 보간하는 객체
void initState() {
super.initState();
// AnimationController 초기화: 반복 애니메이션으로 설정.
_controller = AnimationController(
duration: const Duration(seconds: 1), // 애니메이션 지속 시간을 1초로 설정
vsync: this,
)..repeat(reverse: true); // 애니메이션을 뒤집어서 반복 실행
// 애니메이션의 진행 곡선을 설정.
_animation = CurvedAnimation(parent: _controller, curve: Curves.easeInOut);
}
void dispose() {
// 위젯이 종료될 때 애니메이션 컨트롤러를 정리.
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
// AnimatedBuilder를 사용하여 애니메이션 값이 변경될 때마다 위젯을 재구성.
body: AnimatedBuilder(
animation: _animation,
builder: (context, child) {
// CustomPaint를 사용하여 커스텀 페인터를 화면에 그림.
return CustomPaint(
painter: FancyBackgroundPainter(_animation.value),
child: const Center(
child: Text(
"Hello, CustomPaint!",
style: TextStyle(
fontSize: 32,
color: Colors.amber,
fontWeight: FontWeight.bold),
),
),
);
},
),
);
}
}
// 커스텀 페인터 클래스를 정의.
class FancyBackgroundPainter extends CustomPainter {
final double wavePhase; // 애니메이션 페이즈 값을 받아 저장하는 변수
FancyBackgroundPainter(this.wavePhase);
void paint(Canvas canvas, Size size) {
final paint = Paint()
..shader = const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Colors.purple, Colors.blue],
).createShader(Rect.fromLTRB(0, 0, size.width, size.height));
var path = Path();
double amplitude = 20;
// 경로를 정의하여 복잡한 곡선을 그린다.
path.moveTo(0, size.height * 0.8);
path.quadraticBezierTo(
size.width * 0.25,
size.height * 0.7 + amplitude * sin(wavePhase * 2 * pi),
size.width * 0.5,
size.height * 0.8 + amplitude * cos(wavePhase * 2 * pi),
);
path.quadraticBezierTo(
size.width * 0.75,
size.height * 0.9 - amplitude * sin(wavePhase * 2 * pi),
size.width,
size.height * 0.6 + amplitude * cos(wavePhase * 2 * pi),
);
path.lineTo(size.width, size.height);
path.lineTo(0, size.height);
path.close();
canvas.drawPath(path, paint);
paint.color = Colors.blue.shade900.withOpacity(0.5);
path = Path();
path.moveTo(0, size.height * 0.5);
path.quadraticBezierTo(
size.width * 0.10,
size.height * 0.55 + amplitude * 0.5 * sin(wavePhase * 2 * pi),
size.width * 0.22,
size.height * 0.5 + amplitude * 0.5 * cos(wavePhase * 2 * pi),
);
path.quadraticBezierTo(
size.width * 0.30,
size.height * 0.45 - amplitude * 0.5 * sin(wavePhase * 2 * pi),
size.width * 0.5,
size.height * 0.5 + amplitude * 0.5 * cos(wavePhase * 2 * pi),
);
path.quadraticBezierTo(
size.width * 0.7,
size.height * 0.55 + amplitude * 0.5 * sin(wavePhase * 2 * pi),
size.width * 0.8,
size.height * 0.4 - amplitude * 0.5 * cos(wavePhase * 2 * pi),
);
path.quadraticBezierTo(
size.width * 0.9,
size.height * 0.3 + amplitude * 0.5 * sin(wavePhase * 2 * pi),
size.width,
size.height * 0.2 - amplitude * 0.5 * cos(wavePhase * 2 * pi),
);
path.lineTo(size.width, size.height);
path.lineTo(0, size.height);
path.close();
canvas.drawPath(path, paint);
}
bool shouldRepaint(covariant FancyBackgroundPainter oldDelegate) {
// 애니메이션의 페이즈 값이 바뀌었는지 검사하여 다시 그릴지 결정.
return oldDelegate.wavePhase != wavePhase;
}
}
moveTo는 도화지에서 펜을 특정 위치로 옮기는 것과 같다.
그림을 그리기 시작할 위치를 정하는 것이다.
예를 들어, moveTo(10, 20)은 펜을 x축으로 10, y축으로 20의 위치로 이동시킨다는 뜻이다.
실제로 선을 그리기 전에 시작점을 설정한다.
quadraticBezierTo는 부드러운 곡선을 그리는 데 사용한다.
이 명령은 두 개의 점을 이용한다: 제어점과 끝점이다.
제어점은 곡선이 어떻게 휠지를 결정하는 중간 지점이고, 끝점은 곡선의 끝 위치이다.
곡선의 방향과 모양을 결정한다.
이 점을 통해 곡선이 어디로 휠지 조정할 수 있따.
곡선이 끝나는 최종 위치다..
예를 들어, 곡선을 그릴 때, 시작점에서 곡선이 제어점 방향으로 휘어져 최종적으로 끝점에서 멈춘다.
shouldRepaint는 커스텀 페인터를 다시 그려야 할지 말지를 결정하는 함수다.
화면에 변화가 생겨 그림을 새로 그려야 할 때 true를 반환하고, 그렇지 않으면 false를 반환한다.
예를 들어, 애니메이션처럼 그림이 계속 바뀌는 경우에는 true를 반환해서 계속 그림을 새로 그리도록 할 수 있다.
이 세 가지 기능을 사용하면 플러터에서 복잡한 그림이나 애니메이션을 자유롭게 만들 수 있다.
각 명령은 그림을 그리는 데 필요한 '도구'와 같아서, 어떻게 사용하느냐에 따라 다양한 효과를 낼 수 있다.