import 'package:flutter/material.dart'; import 'dart:math'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: MyHomePage(), ); } } class MyHomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: Padding( padding: EdgeInsets.all(16), child: AnimatedEngineMeter(targetValue: 0.3), ), ); } } class AnimatedEngineMeter extends StatelessWidget { const AnimatedEngineMeter({required this.targetValue}); final double targetValue; @override Widget build(BuildContext context) { return TweenAnimationBuilder( tween: Tween(begin: 0.0, end: targetValue), duration: const Duration(seconds: 2), builder: (context, value, child) { return Tachometer(rpmValue: value); }, ); } } class Tachometer extends StatelessWidget { const Tachometer({required this.rpmValue}); final double rpmValue; // 0.0から1.0までの値。例:0.5は50%のRPMを示す @override Widget build(BuildContext context) { return CustomPaint( painter: TachometerPainter(rpmValue), size: const Size(300, 200), // こちらのサイズは適宜調整してください ); } } class TachometerPainter extends CustomPainter { const TachometerPainter(this.rpmValue); final double rpmValue; @override void paint(Canvas canvas, Size size) { final center = Offset(size.width / 2, size.height); final radius = size.width / 2 - 20; // 半円背景の描画 final bgPaint = Paint()..color = Colors.black; canvas.drawArc( Rect.fromCircle(center: center, radius: radius), pi, pi, false, bgPaint, ); // 数値メモリとラベルの描画 final memPaint = Paint() ..color = Colors.white ..strokeWidth = 2.0; final textPainter = TextPainter( textDirection: TextDirection.ltr, ); const totalMarks = 9; const startAngle = 1.1 * pi; // 開始角度を調整 const endAngle = 1.9 * pi; // 終了角度を調整 for (int i = 0; i <= totalMarks; i++) { // メモリの線を描画 var angle = startAngle + (endAngle - startAngle) * (i / totalMarks); final start = Offset( center.dx + (radius - 10) * cos(angle), center.dy + (radius - 10) * sin(angle), ); final end = Offset( center.dx + radius * cos(angle), center.dy + radius * sin(angle), ); canvas.drawLine(start, end, memPaint); // メモリの数値ラベルの描画 textPainter.text = TextSpan( text: '${i * 20}', style: const TextStyle(color: Colors.white, fontSize: 12.0), ); textPainter.layout(); textPainter.paint( canvas, Offset( center.dx + (radius - 30) * cos(angle) - textPainter.width / 2, center.dy + (radius - 30) * sin(angle) - textPainter.height / 2, ), ); } // 針の描画 final angle = startAngle + (endAngle - startAngle) * rpmValue; final needleEnd = Offset( center.dx + (radius - 20) * cos(angle), center.dy + (radius - 20) * sin(angle), ); final needlePaint = Paint() ..color = Colors.red ..style = PaintingStyle.stroke ..strokeWidth = 5.0; canvas.drawLine(center, needleEnd, needlePaint); } @override bool shouldRepaint(TachometerPainter oldDelegate) { print("oldDelegate.rpmValue ${oldDelegate.rpmValue}"); return oldDelegate.rpmValue != rpmValue; } }