diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index 749fe70..8fa1f0e 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -1,3 +1,6 @@
+import java.util.Properties
+import java.io.FileInputStream
+
plugins {
id("com.android.application")
id("kotlin-android")
@@ -5,6 +8,12 @@ plugins {
id("dev.flutter.flutter-gradle-plugin")
}
+val keystoreProperties = Properties()
+val keystorePropertiesFile = rootProject.file("key.properties")
+if (keystorePropertiesFile.exists()) {
+ keystoreProperties.load(FileInputStream(keystorePropertiesFile))
+}
+
android {
namespace = "com.example.marscar_controller"
compileSdk = flutter.compileSdkVersion
@@ -21,7 +30,7 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
- applicationId = "com.example.marscar_controller"
+ applicationId = "cn.meowdream.marscar_controller"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
@@ -30,6 +39,14 @@ android {
versionName = flutter.versionName
}
+ signingConfigs {
+ create("release") {
+ keyAlias = keystoreProperties["keyAlias"] as String
+ keyPassword = keystoreProperties["keyPassword"] as String
+ storeFile = keystoreProperties["storeFile"]?.let { file(it) }
+ storePassword = keystoreProperties["storePassword"] as String
+ }
+ }
buildTypes {
release {
// TODO: Add your own signing config for the release build.
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 71d2a1d..6a928d0 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -1,6 +1,6 @@
+
diff --git a/android/build/reports/problems/problems-report.html b/android/build/reports/problems/problems-report.html
index f888738..023a344 100644
--- a/android/build/reports/problems/problems-report.html
+++ b/android/build/reports/problems/problems-report.html
@@ -650,7 +650,7 @@ code + .copy-button {
diff --git a/assets/images/logo.png b/assets/images/logo.png
new file mode 100644
index 0000000..bba8451
Binary files /dev/null and b/assets/images/logo.png differ
diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart
index d6d370a..6bfed04 100644
--- a/lib/pages/home_page.dart
+++ b/lib/pages/home_page.dart
@@ -34,8 +34,8 @@ class _HomePageState extends State {
@override
Widget build(BuildContext context) {
- carIpController.text = '192.168.137.122';
- cameraIpController.text = '192.168.137.138';
+ carIpController.text = '192.168.137.34';
+ cameraIpController.text = '192.168.137.121';
GlobalSettingState globalSettingState = context.watch();
return Scaffold(
body: globalSettingState.cameraIP != '' && globalSettingState.carIP != '' ?
diff --git a/lib/pages/player_page.dart b/lib/pages/player_page.dart
index ca92d64..f785e03 100644
--- a/lib/pages/player_page.dart
+++ b/lib/pages/player_page.dart
@@ -10,6 +10,7 @@ import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart';
import 'package:provider/provider.dart';
import 'package:fl_chart/fl_chart.dart';
+import 'package:marscar_controller/pages/square_joystick.dart';
class PlayerPage extends StatefulWidget {
const PlayerPage({super.key});
@@ -26,7 +27,7 @@ class _PlayerPageState extends State {
Timer? _reconnectionTimer;
- final int callBackPeriod = 20;
+ final int callBackPeriod = 10;
RawDatagramSocket? _udpSocket;
@@ -52,12 +53,11 @@ class _PlayerPageState extends State {
Timer? _connectionHealthTimer;
DateTime? _lastMessageReceivedTime;
- String _fpsInfo = 'FPS: N/A';
String _bitrateInfo = '速率: N/A';
Timer? _infoTimer;
- double maxPawRateScale = 1.0;
- Set _pawScaleSelection = {1.0};
+ double maxPawRateScale = 0.35;
+ Set _pawScaleSelection = {0.35};
@override
void initState() {
@@ -196,16 +196,10 @@ class _PlayerPageState extends State {
final nativePlayer = player.platform as NativePlayer;
try {
final bitrate = await nativePlayer.getProperty('video-bitrate');
- final fps = await nativePlayer.getProperty('video-fps');
-
if (mounted) {
setState(() {
final bitrateInKbps = (double.tryParse(bitrate) ?? 0) / 1024;
_bitrateInfo = '速率: ${bitrateInKbps.toStringAsFixed(0)} Kbps';
- final double? fpsValue = double.tryParse(fps);
- if (fpsValue != null) {
- _fpsInfo = 'FPS: ${fpsValue.toStringAsFixed(1)}';
- }
});
}
} catch (e) {
@@ -317,7 +311,7 @@ class _PlayerPageState extends State {
final avgLatency = _latencyReadings.reduce((a, b) => a + b) / _latencyReadings.length;
if (mounted) {
setState(() {
- _pingResult = '图传延迟: ${avgLatency.toStringAsFixed(0)} ms';
+ _pingResult = '图传信号延迟: ${avgLatency.toStringAsFixed(0)} ms';
_videoLatencyHistory.add(FlSpot(DateTime.now().millisecondsSinceEpoch.toDouble(), avgLatency));
if (_videoLatencyHistory.length > _maxHistoryCount) {
@@ -412,7 +406,7 @@ class _PlayerPageState extends State {
left: 0,
child: Center(
child: Container(
- width: 300,
+ width: 400,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
@@ -422,7 +416,7 @@ class _PlayerPageState extends State {
collapsedIconColor: Colors.white,
iconColor: Colors.white,
title: Text(
- '$_pingResult $_bitrateInfo $_fpsInfo\n$_controlLatencyResult',
+ '$_pingResult $_bitrateInfo\n$_controlLatencyResult',
style: const TextStyle(color: Colors.white, fontSize: 12),
textAlign: TextAlign.center,
),
@@ -438,7 +432,7 @@ class _PlayerPageState extends State {
),
const SizedBox(height: 8),
SizedBox(
- height: 120,
+ height: 80,
child: LineChart(
LineChartData(
lineBarsData: [
@@ -505,7 +499,7 @@ class _PlayerPageState extends State {
),
const SizedBox(height: 8),
SizedBox(
- height: 120,
+ height: 80,
child: LineChart(
LineChartData(
lineBarsData: [
@@ -569,160 +563,86 @@ class _PlayerPageState extends State {
Positioned(
left: 64,
bottom: 32,
- child: Joystick(
- includeInitialAnimation: false,
- period: Duration(milliseconds: callBackPeriod),
- stick: CircleAvatar(
- radius: 20,
- backgroundColor: Color.fromARGB(200, 255, 255, 255),
+ child: SizedBox(
+ width: 160,
+ height: 160,
+ child: SquareJoystick(
+ onChanged: (Offset position) {
+ setState(() {
+ lx = position.dx * 127 + 127.5;
+ ly = position.dy * 127 + 127.5;
+ });
+ },
),
- base: JoystickSquareBase(
- size: 160,
- decoration: JoystickBaseDecoration(
- color: Color.fromARGB(60, 200, 200, 200),
- )
- ),
- stickOffsetCalculator: const RectangleStickOffsetCalculator(),
- listener: (details) {
- setState(() {
- lx = details.x * 127 + 127.5;
- ly = details.y * 127 + 127.5;
- });
- },
),
),
Positioned(
right: 128,
bottom: 180,
- child: Joystick(
- includeInitialAnimation: false,
- period: Duration(milliseconds: callBackPeriod),
- stick: CircleAvatar(
- radius: 20,
- backgroundColor: Color.fromARGB(200, 255, 255, 255),
+ child: SizedBox(
+ width: 160,
+ height: 160,
+ child: Joystick2D(
+ onChanged: (Offset position) {
+ if(position.dx == 0 && position.dy == 0) {
+ left = right = forward = backward = 0;
+ } else {
+ var angle = -atan2(position.dy, position.dx), pi_18 = pi / 18.0;
+ setState(() {
+ left = angle < - pi_18 * 12 || angle > pi_18 * 12 ? position.dx * maxPawRateScale : 0;
+ right = angle > - pi_18 * 6 && angle < pi_18 * 6 ? position.dx * maxPawRateScale : 0;
+ forward = angle > pi_18 * 3 && angle < pi_18 * 15 ? -position.dy * maxPawRateScale : 0;
+ backward = angle > - pi_18 * 15 && angle < - pi_18 * 3 ? -position.dy * maxPawRateScale : 0;
+ });
+ }
+ },
),
- base: Container(
- width: 160,
- height: 160,
- decoration: BoxDecoration(
- color: Color.fromARGB(60, 200, 200, 200),
- shape: BoxShape.circle,
- border: BoxBorder.all(
- color: Theme.of(context).colorScheme.outline,
- width: 1
- )
- ),
- ),
- listener: (details) {
- if(details.x == 0 && details.y == 0) {
- left = right = forward = backward = 0;
- } else {
- var angle = -atan2(details.y, details.x), pi_18 = pi / 18.0;
- setState(() {
- left = angle < - pi_18 * 12 || angle > pi_18 * 12 ? details.x * maxPawRateScale : 0;
- right = angle > - pi_18 * 6 && angle < pi_18 * 6 ? details.x * maxPawRateScale : 0;
- forward = angle > pi_18 * 3 && angle < pi_18 * 15 ? -details.y * maxPawRateScale : 0;
- backward = angle > - pi_18 * 15 && angle < - pi_18 * 3 ? -details.y * maxPawRateScale : 0;
- });
- }
- },
),
),
Positioned(
bottom: 180,
- right: 64,
- child: Joystick(
- includeInitialAnimation: false,
- period: Duration(milliseconds: callBackPeriod),
- mode: JoystickMode.vertical,
- stick: CircleAvatar(
- radius: 20,
- backgroundColor: Color.fromARGB(200, 255, 255, 255),
+ right: 54,
+ child: SizedBox(
+ width: 60,
+ height: 160,
+ child: VerticalJoystick(
+ onChanged: (double y) {
+ setState(() {
+ up = y < 0 ? -y * maxPawRateScale * 0.6 : 0;
+ down = y > 0 ? -y * maxPawRateScale * 0.6 : 0;
+ });
+ },
),
- base: Container(
- width: 32,
- height: 160,
- decoration: BoxDecoration(
- color: Color.fromARGB(60, 255, 255, 255),
- borderRadius: const BorderRadius.all(
- Radius.circular(20)
- ),
- border: BoxBorder.all(
- color: Theme.of(context).colorScheme.outline,
- width: 1
- )
- )
- ),
- listener: (details) {
- setState(() {
- up = details.y < 0 ? -details.y * maxPawRateScale * 0.6 : 0;
- down = details.y > 0 ? -details.y * maxPawRateScale * 0.6 : 0;
- });
- },
),
),
Positioned(
- bottom: 100,
+ bottom: 90,
right: 64,
- child: Joystick(
- includeInitialAnimation: false,
- period: Duration(milliseconds: callBackPeriod),
- mode: JoystickMode.horizontal,
- stick: CircleAvatar(
- radius: 20,
- backgroundColor: Color.fromARGB(200, 255, 255, 255),
+ child: SizedBox(
+ width: 160,
+ height: 60,
+ child: HorizontalJoystick(
+ onChanged: (double x) {
+ setState(() {
+ ry = x * 127 + 127.5;
+ });
+ },
),
- base: Container(
- width: 160,
- height: 32,
- decoration: BoxDecoration(
- color: Color.fromARGB(60, 255, 255, 255),
- borderRadius: const BorderRadius.all(
- Radius.circular(20)
- ),
- border: BoxBorder.all(
- color: Theme.of(context).colorScheme.outline,
- width: 1
- )
- )
- ),
- listener: (details) {
- setState(() {
- ry = details.x * 127 + 127.5;
- });
- },
),
),
Positioned(
- bottom: 40,
+ bottom: 30,
right: 64,
- child: Joystick(
- includeInitialAnimation: false,
- period: Duration(milliseconds: callBackPeriod),
- mode: JoystickMode.horizontal,
- stick: CircleAvatar(
- radius: 20,
- backgroundColor: Color.fromARGB(200, 255, 255, 255),
+ child: SizedBox(
+ width: 160,
+ height: 60,
+ child: HorizontalJoystick(
+ onChanged: (double x) {
+ setState(() {
+ rx = x * 127 / 2 + 127.5;
+ });
+ },
),
- base: Container(
- width: 160,
- height: 32,
- decoration: BoxDecoration(
- color: Color.fromARGB(60, 255, 255, 255),
- borderRadius: const BorderRadius.all(
- Radius.circular(20)
- ),
- border: BoxBorder.all(
- color: Theme.of(context).colorScheme.outline,
- width: 1
- )
- )
- ),
- listener: (details) {
- setState(() {
- rx = details.x * 127 / 2 + 127.5;
- });
- },
),
),
Positioned(
@@ -799,9 +719,9 @@ class _PlayerPageState extends State {
child: SegmentedButton(
segments: const >[
ButtonSegment(value: 0.05, label: Text('精确')),
- ButtonSegment(value: 0.2, label: Text('缓慢')),
- ButtonSegment(value: 0.5, label: Text('默认')),
- ButtonSegment(value: 1.0, label: Text('狂暴')),
+ ButtonSegment(value: 0.10, label: Text('缓慢')),
+ ButtonSegment(value: 0.35, label: Text('默认')),
+ ButtonSegment(value: 0.5, label: Text('狂暴')),
],
selected: _pawScaleSelection,
onSelectionChanged: (Set newSelection) {
diff --git a/lib/pages/square_joystick.dart b/lib/pages/square_joystick.dart
new file mode 100644
index 0000000..f8eb3d5
--- /dev/null
+++ b/lib/pages/square_joystick.dart
@@ -0,0 +1,478 @@
+import 'package:flutter/material.dart';
+
+class Joystick2D extends StatefulWidget {
+ final ValueChanged onChanged;
+
+ const Joystick2D({
+ super.key,
+ required this.onChanged,
+ });
+
+ @override
+ State createState() => _Joystick2DState();
+}
+
+class _Joystick2DState extends State {
+ Offset _stickPosition = Offset.zero;
+ bool _active = false;
+
+ void _onPanUpdate(Offset localPosition, Size size) {
+ final center = size.center(Offset.zero);
+ final delta = localPosition - center;
+ final distance = delta.distance;
+ final maxDistance = size.width / 2;
+
+ final constrainedDelta = distance > maxDistance
+ ? Offset(
+ delta.dx * maxDistance / distance,
+ delta.dy * maxDistance / distance,
+ )
+ : delta;
+
+ setState(() {
+ _stickPosition = constrainedDelta;
+ _active = true;
+ });
+
+ final normalized = Offset(
+ constrainedDelta.dx / maxDistance,
+ constrainedDelta.dy / maxDistance,
+ );
+
+ widget.onChanged(normalized);
+ }
+
+ void _resetJoystick() {
+ setState(() {
+ _stickPosition = Offset.zero;
+ _active = false;
+ });
+ widget.onChanged(Offset.zero);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return GestureDetector(
+ behavior: HitTestBehavior.opaque,
+ onPanStart: (details) {
+ final box = context.findRenderObject() as RenderBox;
+ final localPosition = box.globalToLocal(details.globalPosition);
+ _onPanUpdate(localPosition, box.size);
+ },
+ onPanUpdate: (details) {
+ final box = context.findRenderObject() as RenderBox;
+ final localPosition = box.globalToLocal(details.globalPosition);
+ _onPanUpdate(localPosition, box.size);
+ },
+ onPanEnd: (_) => _resetJoystick(),
+ onPanCancel: _resetJoystick,
+ child: CustomPaint(
+ painter: JoystickPainter(
+ stickPosition: _stickPosition,
+ active: _active,
+ ),
+ ),
+ );
+ }
+}
+
+class JoystickPainter extends CustomPainter {
+ final Offset stickPosition;
+ final bool active;
+
+ JoystickPainter({required this.stickPosition, required this.active});
+
+ @override
+ void paint(Canvas canvas, Size size) {
+ final center = size.center(Offset.zero);
+ final radius = size.width / 2;
+ const stickRadius = 20.0;
+
+ canvas.drawCircle(
+ center,
+ radius,
+ Paint()
+ ..color = active ? Colors.white.withValues(alpha: 0.3) : Colors.white.withValues(alpha: 0.1)
+ ..style = PaintingStyle.fill,
+ );
+
+ canvas.drawCircle(
+ center,
+ radius,
+ Paint()
+ ..color = Colors.grey
+ ..style = PaintingStyle.stroke
+ ..strokeWidth = 2.0,
+ );
+
+ canvas.drawCircle(
+ center + stickPosition,
+ stickRadius,
+ Paint()
+ ..color = active ? Colors.white : Colors.grey
+ ..style = PaintingStyle.fill,
+ );
+ }
+
+ @override
+ bool shouldRepaint(JoystickPainter oldDelegate) {
+ return stickPosition != oldDelegate.stickPosition || active != oldDelegate.active;
+ }
+}
+
+class SquareJoystick extends StatefulWidget {
+ final ValueChanged onChanged;
+
+ const SquareJoystick({
+ super.key,
+ required this.onChanged,
+ });
+
+ @override
+ State createState() => _SquareJoystickState();
+}
+
+class _SquareJoystickState extends State {
+ Offset _stickPosition = Offset.zero;
+ bool _active = false;
+
+ void _onPanUpdate(Offset localPosition, Size size) {
+ final center = size.center(Offset.zero);
+ var dx = localPosition.dx - center.dx;
+ var dy = localPosition.dy - center.dy;
+
+ final maxDistance = size.width / 2;
+ dx = dx.clamp(-maxDistance, maxDistance);
+ dy = dy.clamp(-maxDistance, maxDistance);
+
+ setState(() {
+ _stickPosition = Offset(dx, dy);
+ _active = true;
+ });
+
+ final normalized = Offset(
+ dx / maxDistance,
+ dy / maxDistance,
+ );
+
+ widget.onChanged(normalized);
+ }
+
+ void _resetJoystick() {
+ setState(() {
+ _stickPosition = Offset.zero;
+ _active = false;
+ });
+ widget.onChanged(Offset.zero);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return GestureDetector(
+ behavior: HitTestBehavior.opaque,
+ onPanStart: (details) {
+ final box = context.findRenderObject() as RenderBox;
+ final localPosition = box.globalToLocal(details.globalPosition);
+ _onPanUpdate(localPosition, box.size);
+ },
+ onPanUpdate: (details) {
+ final box = context.findRenderObject() as RenderBox;
+ final localPosition = box.globalToLocal(details.globalPosition);
+ _onPanUpdate(localPosition, box.size);
+ },
+ onPanEnd: (_) => _resetJoystick(),
+ onPanCancel: _resetJoystick,
+ child: CustomPaint(
+ painter: SquareJoystickPainter(
+ stickPosition: _stickPosition,
+ active: _active,
+ ),
+ ),
+ );
+ }
+}
+
+class SquareJoystickPainter extends CustomPainter {
+ final Offset stickPosition;
+ final bool active;
+
+ SquareJoystickPainter({required this.stickPosition, required this.active});
+
+ @override
+ @override
+ void paint(Canvas canvas, Size size) {
+ final center = size.center(Offset.zero);
+ const stickRadius = 20.0;
+ final squareRect = Rect.fromCenter(
+ center: center,
+ width: size.width,
+ height: size.height,
+ );
+
+ final roundedRect = RRect.fromRectAndRadius(
+ squareRect,
+ Radius.circular(size.width / 8),
+ );
+
+ canvas.drawRRect(
+ roundedRect,
+ Paint()
+ ..color =
+ active ? Colors.white.withValues(alpha: 0.3) : Colors.white.withValues(alpha: 0.1)
+ ..style = PaintingStyle.fill,
+ );
+
+ canvas.drawRRect(
+ roundedRect,
+ Paint()
+ ..color = Colors.grey
+ ..style = PaintingStyle.stroke
+ ..strokeWidth = 2.0,
+ );
+
+ canvas.drawCircle(
+ center + stickPosition,
+ stickRadius,
+ Paint()
+ ..color = active ? Colors.white : Colors.grey
+ ..style = PaintingStyle.fill,
+ );
+ }
+
+ @override
+ bool shouldRepaint(SquareJoystickPainter oldDelegate) {
+ return stickPosition != oldDelegate.stickPosition || active != oldDelegate.active;
+ }
+}
+
+class HorizontalJoystick extends StatefulWidget {
+ final ValueChanged onChanged;
+
+ const HorizontalJoystick({
+ super.key,
+ required this.onChanged,
+ });
+
+ @override
+ State createState() => _HorizontalJoystickState();
+}
+
+class _HorizontalJoystickState extends State {
+ double _stickPosition = 0.0;
+ bool _active = false;
+
+ void _onPanUpdate(Offset localPosition, Size size) {
+ final centerX = size.width / 2;
+ final dx = (localPosition.dx - centerX).clamp(-centerX, centerX);
+
+ setState(() {
+ _stickPosition = dx;
+ _active = true;
+ });
+
+ final normalized = dx / centerX;
+ widget.onChanged(normalized);
+ }
+
+ void _resetJoystick() {
+ setState(() {
+ _stickPosition = 0.0;
+ _active = false;
+ });
+ widget.onChanged(0.0);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return GestureDetector(
+ behavior: HitTestBehavior.opaque,
+ onPanStart: (details) {
+ final box = context.findRenderObject() as RenderBox;
+ _onPanUpdate(box.globalToLocal(details.globalPosition), box.size);
+ },
+ onPanUpdate: (details) {
+ final box = context.findRenderObject() as RenderBox;
+ _onPanUpdate(box.globalToLocal(details.globalPosition), box.size);
+ },
+ onPanEnd: (_) => _resetJoystick(),
+ onPanCancel: _resetJoystick,
+ child: CustomPaint(
+ painter: HorizontalJoystickPainter(
+ stickPosition: _stickPosition,
+ active: _active,
+ ),
+ ),
+ );
+ }
+}
+
+class HorizontalJoystickPainter extends CustomPainter {
+ final double stickPosition;
+ final bool active;
+
+ HorizontalJoystickPainter({required this.stickPosition, required this.active});
+
+ @override
+ void paint(Canvas canvas, Size size) {
+ final center = size.center(Offset.zero);
+ const stickRadius = 15.0;
+ final trackHeight = size.height / 2;
+
+ canvas.drawRRect(
+ RRect.fromRectAndRadius(
+ Rect.fromCenter(
+ center: center,
+ width: size.width,
+ height: trackHeight,
+ ),
+ Radius.circular(trackHeight / 2),
+ ),
+ Paint()
+ ..color = active ? Colors.white.withValues(alpha: 0.3) : Colors.white.withValues(alpha: 0.1),
+ );
+
+ canvas.drawRRect(
+ RRect.fromRectAndRadius(
+ Rect.fromCenter(
+ center: center,
+ width: size.width,
+ height: trackHeight,
+ ),
+ Radius.circular(trackHeight / 2),
+ ),
+ Paint()
+ ..color = Colors.grey
+ ..style = PaintingStyle.stroke
+ ..strokeWidth = 2.0,
+ );
+
+ canvas.drawCircle(
+ Offset(center.dx + stickPosition, center.dy),
+ stickRadius * 1.333,
+ Paint()
+ ..color = active ? Colors.white : Colors.grey
+ ..style = PaintingStyle.fill,
+ );
+ }
+
+ @override
+ bool shouldRepaint(HorizontalJoystickPainter oldDelegate) {
+ return stickPosition != oldDelegate.stickPosition || active != oldDelegate.active;
+ }
+}
+
+class VerticalJoystick extends StatefulWidget {
+ final ValueChanged onChanged;
+
+ const VerticalJoystick({
+ super.key,
+ required this.onChanged,
+ });
+
+ @override
+ State createState() => _VerticalJoystickState();
+}
+
+class _VerticalJoystickState extends State {
+ double _stickPosition = 0.0;
+ bool _active = false;
+
+ void _onPanUpdate(Offset localPosition, Size size) {
+ final centerY = size.height / 2;
+ final dy = (localPosition.dy - centerY).clamp(-centerY, centerY);
+
+ setState(() {
+ _stickPosition = dy;
+ _active = true;
+ });
+
+ final normalized = dy / centerY;
+ widget.onChanged(normalized);
+ }
+
+ void _resetJoystick() {
+ setState(() {
+ _stickPosition = 0.0;
+ _active = false;
+ });
+ widget.onChanged(0.0);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return GestureDetector(
+ behavior: HitTestBehavior.opaque,
+ onPanStart: (details) {
+ final box = context.findRenderObject() as RenderBox;
+ _onPanUpdate(box.globalToLocal(details.globalPosition), box.size);
+ },
+ onPanUpdate: (details) {
+ final box = context.findRenderObject() as RenderBox;
+ _onPanUpdate(box.globalToLocal(details.globalPosition), box.size);
+ },
+ onPanEnd: (_) => _resetJoystick(),
+ onPanCancel: _resetJoystick,
+ child: CustomPaint(
+ painter: VerticalJoystickPainter(
+ stickPosition: _stickPosition,
+ active: _active,
+ ),
+ ),
+ );
+ }
+}
+
+class VerticalJoystickPainter extends CustomPainter {
+ final double stickPosition;
+ final bool active;
+
+ VerticalJoystickPainter({required this.stickPosition, required this.active});
+
+ @override
+ void paint(Canvas canvas, Size size) {
+ final center = size.center(Offset.zero);
+ const stickRadius = 15.0;
+ final trackWidth = size.width / 2;
+
+ canvas.drawRRect(
+ RRect.fromRectAndRadius(
+ Rect.fromCenter(
+ center: center,
+ width: trackWidth,
+ height: size.height,
+ ),
+ Radius.circular(trackWidth / 2),
+ ),
+ Paint()
+ ..color = active ? Colors.white.withValues(alpha: 0.3) : Colors.white.withValues(alpha: 0.1),
+ );
+
+ canvas.drawRRect(
+ RRect.fromRectAndRadius(
+ Rect.fromCenter(
+ center: center,
+ width: trackWidth,
+ height: size.height,
+ ),
+ Radius.circular(trackWidth / 2),
+ ),
+ Paint()
+ ..color = Colors.grey
+ ..style = PaintingStyle.stroke
+ ..strokeWidth = 2.0,
+ );
+
+ canvas.drawCircle(
+ Offset(center.dx, center.dy + stickPosition),
+ stickRadius * 1.333,
+ Paint()
+ ..color = active ? Colors.white : Colors.grey
+ ..style = PaintingStyle.fill,
+ );
+ }
+
+ @override
+ bool shouldRepaint(VerticalJoystickPainter oldDelegate) {
+ return stickPosition != oldDelegate.stickPosition || active != oldDelegate.active;
+ }
+}
\ No newline at end of file
diff --git a/pubspec.lock b/pubspec.lock
index fc37608..eb86177 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -41,6 +41,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
+ checked_yaml:
+ dependency: transitive
+ description:
+ name: checked_yaml
+ sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.4"
+ cli_util:
+ dependency: transitive
+ description:
+ name: cli_util
+ sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.4.2"
clock:
dependency: transitive
description:
@@ -142,6 +158,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.2.2"
+ flutter_launcher_icons:
+ dependency: "direct dev"
+ description:
+ name: flutter_launcher_icons
+ sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.14.4"
flutter_lints:
dependency: "direct dev"
description:
@@ -184,6 +208,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.5.4"
+ json_annotation:
+ dependency: transitive
+ description:
+ name: json_annotation
+ sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.9.0"
leak_tracker:
dependency: transitive
description:
@@ -565,6 +597,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.5.0"
+ yaml:
+ dependency: transitive
+ description:
+ name: yaml
+ sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.1.3"
sdks:
dart: ">=3.8.1 <4.0.0"
flutter: ">=3.27.4"
diff --git a/pubspec.yaml b/pubspec.yaml
index e2ade93..a95d9fe 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -54,6 +54,7 @@ dev_dependencies:
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^5.0.0
+ flutter_launcher_icons: "^0.14.4"
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
@@ -67,9 +68,8 @@ flutter:
uses-material-design: true
# To add assets to your application, add an assets section, like this:
- # assets:
- # - images/a_dot_burr.jpeg
- # - images/a_dot_ham.jpeg
+ assets:
+ - assets/images/
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
diff --git a/test/widget_test.dart b/test/widget_test.dart
deleted file mode 100644
index 467e587..0000000
--- a/test/widget_test.dart
+++ /dev/null
@@ -1,30 +0,0 @@
-// This is a basic Flutter widget test.
-//
-// To perform an interaction with a widget in your test, use the WidgetTester
-// utility in the flutter_test package. For example, you can send tap and scroll
-// gestures. You can also use WidgetTester to find child widgets in the widget
-// tree, read text, and verify that the values of widget properties are correct.
-
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-
-import 'package:marscar_controller/main.dart';
-
-void main() {
- testWidgets('Counter increments smoke test', (WidgetTester tester) async {
- // Build our app and trigger a frame.
- await tester.pumpWidget(const MyApp());
-
- // Verify that our counter starts at 0.
- expect(find.text('0'), findsOneWidget);
- expect(find.text('1'), findsNothing);
-
- // Tap the '+' icon and trigger a frame.
- await tester.tap(find.byIcon(Icons.add));
- await tester.pump();
-
- // Verify that our counter has incremented.
- expect(find.text('0'), findsNothing);
- expect(find.text('1'), findsOneWidget);
- });
-}