概要
Flutterでレーダーアニメーションを実装した際の備忘録。
プロジェクト作成
FVMについてはこちら
fvm flutter create radar_animation
グリッド作成
lib/radar_grid_painter.dart
import 'package:flutter/material.dart';
import 'dart:math' as math;
import 'dart:math';
class RadarGridPainter extends CustomPainter {
final int divisions;
final Color gridColor;
RadarGridPainter({
this.divisions = 4,
this.gridColor = Colors.green,
});
@override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..color = gridColor
..style = PaintingStyle.stroke
..strokeWidth = 1;
// Draw the circles
double radiusStep = size.width / (2 * divisions);
for (int i = 1; i <= divisions; i++) {
canvas.drawCircle(size.center(Offset.zero), i * radiusStep, paint);
}
// Draw the spokes
double angleStep = 2 * math.pi / divisions;
for (int i = 0; i < divisions; i++) {
double angle = i * angleStep;
Offset start = size.center(Offset.zero);
Offset end = size.center(Offset(math.cos(angle) * size.width / 2, math.sin(angle) * size.width / 2));
canvas.drawLine(start, end, paint);
}
final pointPaint = Paint()
..color = Colors.red
..style = PaintingStyle.fill;
final random = Random();
for (int i = 0; i < 5; i++) {
final angle = random.nextDouble() * 2 * pi;
final distance = random.nextDouble() * size.width / 2;
final x = cos(angle) * distance;
final y = sin(angle) * distance;
final offset = Offset(x, y);
canvas.drawCircle(size.center(offset), 5, pointPaint);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
}
main.dart
import 'package:flutter/material.dart';
import 'package:radar_animation/radar_grid_painter.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Center(
// レダーグリッドを描画する
child: CustomPaint(
size: const Size(300, 300),
painter: RadarGridPainter(),
),
),
);
}
}
レーダっぽくする
main.dart
import 'package:flutter/material.dart';
import 'package:radar_animation/radar_grid_painter.dart';
import 'dart:math';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
final List<Map<String, num>> points = [
{'fixedAngle': Random().nextDouble() * 2 * pi, 'radius': 1},
{'fixedAngle': Random().nextDouble() * 2 * pi, 'radius': 2},
{'fixedAngle': Random().nextDouble() * 2 * pi, 'radius': 3},
{'fixedAngle': Random().nextDouble() * 2 * pi, 'radius': 2},
{'fixedAngle': Random().nextDouble() * 2 * pi, 'radius': 1},
];
@override
void initState(){
_controller = AnimationController(vsync: this, duration: const Duration(seconds: 20))
..repeat();
super.initState();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
darkTheme: ThemeData(
brightness: Brightness.dark,
),
home: Expanded(
flex: 2,
child: Center(
child: Stack(
alignment: Alignment.center,
children: [
// The grid background
CustomPaint(
painter: RadarGridPainter(points: points.map((point) => point.map((key, value) => MapEntry(key, value.toDouble()))).toList()),
size: Size(MediaQuery.of(context).size.width, MediaQuery.of(context).size.width),
),
// The rotating radar sweep
RotationTransition(
turns: Tween(begin: 0.0, end: 3.0).animate(_controller),
child:Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.width,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: SweepGradient(
center: Alignment.center,
startAngle: 0.0,
endAngle: 2 * pi,
colors: [
Colors.transparent,
Colors.green.withOpacity(0.75),
Colors.transparent
],
stops: const [0.0, 0.05, 0.1],
),
),
),
),
],
),
),
),
);
}
}
radar_grid_painter.dart
import 'package:flutter/material.dart';
import 'dart:math' as math;
import 'dart:math';
class RadarGridPainter extends CustomPainter {
final int divisions;
final Color gridColor;
final List<Map<String, double>> points;
RadarGridPainter({
required this.points,
this.divisions = 4,
this.gridColor = Colors.green,
});
@override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..color = gridColor
..style = PaintingStyle.stroke
..strokeWidth = 1;
// Draw the circles
double radiusStep = size.width / (2 * divisions);
for (int i = 1; i <= divisions; i++) {
canvas.drawCircle(size.center(Offset.zero), i * radiusStep, paint);
}
// Draw the spokes
double angleStep = 2 * math.pi / divisions;
for (int i = 0; i < divisions; i++) {
double angle = i * angleStep;
Offset start = size.center(Offset.zero);
Offset end = size.center(Offset(math.cos(angle) * size.width / 2, math.sin(angle) * size.width / 2));
canvas.drawLine(start, end, paint);
}
final pointPaint = Paint()
..color = Colors.red
..style = PaintingStyle.fill;
for (int i = 0; i < points.length; i++) {
final fixedAngle = points[i]['fixedAngle']!;
final radius = points[i]['radius']! * min(size.width, size.height) / (2 * divisions);
final point = Offset(size.width / 2 + radius * cos(fixedAngle - pi / 2), size.height / 2 + radius * sin(fixedAngle - pi / 2));
canvas.drawCircle(point, 6.0, pointPaint);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
}
この記事は個人の見解であり所属する組織を代表しません。