完成全部功能
This commit is contained in:
parent
1736ce9159
commit
de09cf25b8
@ -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.
|
||||
|
@ -1,6 +1,6 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application
|
||||
android:label="marscar_controller"
|
||||
android:label="宇宙级飞船奶牛抓捕队"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
@ -47,4 +47,5 @@
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
</manifest>
|
||||
|
File diff suppressed because one or more lines are too long
BIN
assets/images/logo.png
Normal file
BIN
assets/images/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 87 KiB |
@ -34,8 +34,8 @@ class _HomePageState extends State<HomePage> {
|
||||
|
||||
@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<GlobalSettingState>();
|
||||
return Scaffold(
|
||||
body: globalSettingState.cameraIP != '' && globalSettingState.carIP != '' ?
|
||||
|
@ -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<PlayerPage> {
|
||||
|
||||
Timer? _reconnectionTimer;
|
||||
|
||||
final int callBackPeriod = 20;
|
||||
final int callBackPeriod = 10;
|
||||
|
||||
RawDatagramSocket? _udpSocket;
|
||||
|
||||
@ -52,12 +53,11 @@ class _PlayerPageState extends State<PlayerPage> {
|
||||
Timer? _connectionHealthTimer;
|
||||
DateTime? _lastMessageReceivedTime;
|
||||
|
||||
String _fpsInfo = 'FPS: N/A';
|
||||
String _bitrateInfo = '速率: N/A';
|
||||
Timer? _infoTimer;
|
||||
|
||||
double maxPawRateScale = 1.0;
|
||||
Set<double> _pawScaleSelection = {1.0};
|
||||
double maxPawRateScale = 0.35;
|
||||
Set<double> _pawScaleSelection = {0.35};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -196,16 +196,10 @@ class _PlayerPageState extends State<PlayerPage> {
|
||||
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<PlayerPage> {
|
||||
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<PlayerPage> {
|
||||
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<PlayerPage> {
|
||||
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<PlayerPage> {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
height: 120,
|
||||
height: 80,
|
||||
child: LineChart(
|
||||
LineChartData(
|
||||
lineBarsData: [
|
||||
@ -505,7 +499,7 @@ class _PlayerPageState extends State<PlayerPage> {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
height: 120,
|
||||
height: 80,
|
||||
child: LineChart(
|
||||
LineChartData(
|
||||
lineBarsData: [
|
||||
@ -569,160 +563,86 @@ class _PlayerPageState extends State<PlayerPage> {
|
||||
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<PlayerPage> {
|
||||
child: SegmentedButton<double>(
|
||||
segments: const <ButtonSegment<double>>[
|
||||
ButtonSegment<double>(value: 0.05, label: Text('精确')),
|
||||
ButtonSegment<double>(value: 0.2, label: Text('缓慢')),
|
||||
ButtonSegment<double>(value: 0.5, label: Text('默认')),
|
||||
ButtonSegment<double>(value: 1.0, label: Text('狂暴')),
|
||||
ButtonSegment<double>(value: 0.10, label: Text('缓慢')),
|
||||
ButtonSegment<double>(value: 0.35, label: Text('默认')),
|
||||
ButtonSegment<double>(value: 0.5, label: Text('狂暴')),
|
||||
],
|
||||
selected: _pawScaleSelection,
|
||||
onSelectionChanged: (Set<double> newSelection) {
|
||||
|
478
lib/pages/square_joystick.dart
Normal file
478
lib/pages/square_joystick.dart
Normal file
@ -0,0 +1,478 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class Joystick2D extends StatefulWidget {
|
||||
final ValueChanged<Offset> onChanged;
|
||||
|
||||
const Joystick2D({
|
||||
super.key,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
State<Joystick2D> createState() => _Joystick2DState();
|
||||
}
|
||||
|
||||
class _Joystick2DState extends State<Joystick2D> {
|
||||
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<Offset> onChanged;
|
||||
|
||||
const SquareJoystick({
|
||||
super.key,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SquareJoystick> createState() => _SquareJoystickState();
|
||||
}
|
||||
|
||||
class _SquareJoystickState extends State<SquareJoystick> {
|
||||
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<double> onChanged;
|
||||
|
||||
const HorizontalJoystick({
|
||||
super.key,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
State<HorizontalJoystick> createState() => _HorizontalJoystickState();
|
||||
}
|
||||
|
||||
class _HorizontalJoystickState extends State<HorizontalJoystick> {
|
||||
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<double> onChanged;
|
||||
|
||||
const VerticalJoystick({
|
||||
super.key,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
State<VerticalJoystick> createState() => _VerticalJoystickState();
|
||||
}
|
||||
|
||||
class _VerticalJoystickState extends State<VerticalJoystick> {
|
||||
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;
|
||||
}
|
||||
}
|
40
pubspec.lock
40
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"
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue
Block a user