주변을 훑으며 감지하는 레이더, 감지된 여러 개의 객체를 렌더링 하는데에는 AnimationController하나면 충분합니다.
Widget
스위퍼가 원을 훑을거기 때문에 최대 값을 2pi로 설정해 초기화합니다.
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 5000),
upperBound: math.pi * 2
);
탐지 대상의 타입을 레코드 패턴을 이용해 정의하고 랜덤으로 초기화합니다.
late List<TargetPosition> _objects;
...
_objects = List.generate(5 + random.nextInt(5), (i) {
return (
radian: math.pi * 2 * random.nextDouble(),
distance: 30.0 + random.nextInt(100)
);
});
CustomPainter
선을 그려 레이더의 뼈대를 그린 후, 스위퍼, 탐지 대상을 그리는 순서로 진행합니다.
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width/2, size.height/2);
_drawLine(canvas, size, center);
_drawSweeper(canvas, size, center);
_drawTarget(canvas, size, center);
}
_drawLine
정중앙을 중심으로 동심원 4개와 십자선을 그려 그럴싸한 레이더 ui를 만듭니다.

_drawSweeper
void _drawSweeper(Canvas canvas, Size size, Offset center) {
// 1. 감지 범위를 360 * 1/10, 36도로 설정합니다.
final sweepAngle = math.pi/5;
// 2. 0 - 2pi로 증가하는 값을 삼각함수를 이용해 계산하면
// (radius, 0)좌표부터 시계 방향으로 회전하며 원을 그립니다.
final toX = set.radius * math.cos(animation.value);
final toY = set.radius * math.sin(animation.value);
// 3. Path의 arcTo 메소드를 이용해 앞서 정의한 sweepAngle각도를 가진 부채꼴을 만듭니다.
final path = Path()
..moveTo(center.dx, center.dy)
..lineTo(toX, toY)
..arcTo(Rect.fromCircle(center: center, radius: set.radius), animation.value - sweepAngle, sweepAngle, true)
..lineTo(center.dx, center.dy);
// 4. SweepGradient를 이용해 부채꼴에 그라데이션 효과를 추가해줍니다.
final paint = Paint()
..shader = SweepGradient(
colors: [
set.sweeperGradientColor, set.sweeperColor
],
startAngle: animation.value - sweepAngle,
endAngle: animation.value,
tileMode: TileMode.mirror
).createShader(Rect.fromCircle(center: center, radius: set.diameter));
canvas.drawPath(path, paint);
}
_drawTarget
스위퍼가 회전하며 특정 각도에 진입 했을 때 탐지 물체가 깜빡이는 애니메이션 효과를 주기 위해 2가지 과정을 거쳐 animation.value를 가공합니다.

void _drawTarget(Canvas canvas, Size size, Offset center) {
// 탐지 범위를 20%로 설정합니다
final detectionRange = 0.2;
for (int i = 0; i < targets.length; i++) {
final t = targets[i];
// 1번 과정으로 대상의 위치를 _controller값에서 찾습니다.
double a = animation.value;
double b = t.radian % (2 * math.pi);
double diff = (a - b) / (2 * math.pi);
// animation.value는 0부터 2π까지 증가하면서 순환하고, t.radian은
// 2π에 가까운 값일 수 있습니다.
// 이때 단순히 animation.value - t.radian을 계산하면 음수가 되어,
// 타겟이 스위퍼와 가까움에도 불구하고 멀리 떨어진 것으로 오인됩니다.
// 이를 방지하기 위해 +1을 통해 시계 방향 거리로 보정합니다.
if (diff < 0) diff += 1;
if (diff > detectionRange) continue;
// 2번 과정으로 0 - 1 - 0의 값을 갖기 위해 절대값을 이용해 보정합니다.
final v = -((diff - 0.1).abs()) * 10 + 1;
final alpha = (255 * v).toInt().clamp(0, 255);
// 삼각함수를 이용해 위치 값 계산
final x = math.cos(t.radian) * t.distance;
final y = math.sin(t.radian) * t.distance;
final pos = center.translate(x, y);
canvas.drawCircle(pos, 6.0, Paint()..color = set.sweeperColor.withAlpha(alpha));
}
}
마무리
Tween을 이용해 Animation객체를 추가 생성해 해결할 수도 있었지만 주어진 값을 수학적으로 접근해 해결할 수도 있습니다. 어려운 수학 개념이나 공식이 쓰이진 않았으나 시간 기반 애니메이션에서 공간적 로직을 수학으로 푸는 접근은 여러 분야에 재활용될 수 있을 거라 생각합니다.