完成全部功能
This commit is contained in:
parent
1736ce9159
commit
de09cf25b8
@ -1,3 +1,6 @@
|
|||||||
|
import java.util.Properties
|
||||||
|
import java.io.FileInputStream
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("kotlin-android")
|
id("kotlin-android")
|
||||||
@ -5,6 +8,12 @@ plugins {
|
|||||||
id("dev.flutter.flutter-gradle-plugin")
|
id("dev.flutter.flutter-gradle-plugin")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val keystoreProperties = Properties()
|
||||||
|
val keystorePropertiesFile = rootProject.file("key.properties")
|
||||||
|
if (keystorePropertiesFile.exists()) {
|
||||||
|
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
|
||||||
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.example.marscar_controller"
|
namespace = "com.example.marscar_controller"
|
||||||
compileSdk = flutter.compileSdkVersion
|
compileSdk = flutter.compileSdkVersion
|
||||||
@ -21,7 +30,7 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
// 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.
|
// You can update the following values to match your application needs.
|
||||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||||
minSdk = flutter.minSdkVersion
|
minSdk = flutter.minSdkVersion
|
||||||
@ -30,6 +39,14 @@ android {
|
|||||||
versionName = flutter.versionName
|
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 {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
// TODO: Add your own signing config for the release build.
|
// TODO: Add your own signing config for the release build.
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<application
|
<application
|
||||||
android:label="marscar_controller"
|
android:label="宇宙级飞船奶牛抓捕队"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher">
|
||||||
<activity
|
<activity
|
||||||
@ -47,4 +47,5 @@
|
|||||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
<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_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||||
</manifest>
|
</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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
carIpController.text = '192.168.137.122';
|
carIpController.text = '192.168.137.34';
|
||||||
cameraIpController.text = '192.168.137.138';
|
cameraIpController.text = '192.168.137.121';
|
||||||
GlobalSettingState globalSettingState = context.watch<GlobalSettingState>();
|
GlobalSettingState globalSettingState = context.watch<GlobalSettingState>();
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: globalSettingState.cameraIP != '' && globalSettingState.carIP != '' ?
|
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:media_kit_video/media_kit_video.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:fl_chart/fl_chart.dart';
|
import 'package:fl_chart/fl_chart.dart';
|
||||||
|
import 'package:marscar_controller/pages/square_joystick.dart';
|
||||||
|
|
||||||
class PlayerPage extends StatefulWidget {
|
class PlayerPage extends StatefulWidget {
|
||||||
const PlayerPage({super.key});
|
const PlayerPage({super.key});
|
||||||
@ -26,7 +27,7 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
|
|
||||||
Timer? _reconnectionTimer;
|
Timer? _reconnectionTimer;
|
||||||
|
|
||||||
final int callBackPeriod = 20;
|
final int callBackPeriod = 10;
|
||||||
|
|
||||||
RawDatagramSocket? _udpSocket;
|
RawDatagramSocket? _udpSocket;
|
||||||
|
|
||||||
@ -52,12 +53,11 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
Timer? _connectionHealthTimer;
|
Timer? _connectionHealthTimer;
|
||||||
DateTime? _lastMessageReceivedTime;
|
DateTime? _lastMessageReceivedTime;
|
||||||
|
|
||||||
String _fpsInfo = 'FPS: N/A';
|
|
||||||
String _bitrateInfo = '速率: N/A';
|
String _bitrateInfo = '速率: N/A';
|
||||||
Timer? _infoTimer;
|
Timer? _infoTimer;
|
||||||
|
|
||||||
double maxPawRateScale = 1.0;
|
double maxPawRateScale = 0.35;
|
||||||
Set<double> _pawScaleSelection = {1.0};
|
Set<double> _pawScaleSelection = {0.35};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -196,16 +196,10 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
final nativePlayer = player.platform as NativePlayer;
|
final nativePlayer = player.platform as NativePlayer;
|
||||||
try {
|
try {
|
||||||
final bitrate = await nativePlayer.getProperty('video-bitrate');
|
final bitrate = await nativePlayer.getProperty('video-bitrate');
|
||||||
final fps = await nativePlayer.getProperty('video-fps');
|
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
final bitrateInKbps = (double.tryParse(bitrate) ?? 0) / 1024;
|
final bitrateInKbps = (double.tryParse(bitrate) ?? 0) / 1024;
|
||||||
_bitrateInfo = '速率: ${bitrateInKbps.toStringAsFixed(0)} Kbps';
|
_bitrateInfo = '速率: ${bitrateInKbps.toStringAsFixed(0)} Kbps';
|
||||||
final double? fpsValue = double.tryParse(fps);
|
|
||||||
if (fpsValue != null) {
|
|
||||||
_fpsInfo = 'FPS: ${fpsValue.toStringAsFixed(1)}';
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -317,7 +311,7 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
final avgLatency = _latencyReadings.reduce((a, b) => a + b) / _latencyReadings.length;
|
final avgLatency = _latencyReadings.reduce((a, b) => a + b) / _latencyReadings.length;
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_pingResult = '图传延迟: ${avgLatency.toStringAsFixed(0)} ms';
|
_pingResult = '图传信号延迟: ${avgLatency.toStringAsFixed(0)} ms';
|
||||||
|
|
||||||
_videoLatencyHistory.add(FlSpot(DateTime.now().millisecondsSinceEpoch.toDouble(), avgLatency));
|
_videoLatencyHistory.add(FlSpot(DateTime.now().millisecondsSinceEpoch.toDouble(), avgLatency));
|
||||||
if (_videoLatencyHistory.length > _maxHistoryCount) {
|
if (_videoLatencyHistory.length > _maxHistoryCount) {
|
||||||
@ -412,7 +406,7 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
left: 0,
|
left: 0,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 300,
|
width: 400,
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
@ -422,7 +416,7 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
collapsedIconColor: Colors.white,
|
collapsedIconColor: Colors.white,
|
||||||
iconColor: Colors.white,
|
iconColor: Colors.white,
|
||||||
title: Text(
|
title: Text(
|
||||||
'$_pingResult $_bitrateInfo $_fpsInfo\n$_controlLatencyResult',
|
'$_pingResult $_bitrateInfo\n$_controlLatencyResult',
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 12),
|
style: const TextStyle(color: Colors.white, fontSize: 12),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
@ -438,7 +432,7 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 120,
|
height: 80,
|
||||||
child: LineChart(
|
child: LineChart(
|
||||||
LineChartData(
|
LineChartData(
|
||||||
lineBarsData: [
|
lineBarsData: [
|
||||||
@ -505,7 +499,7 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 120,
|
height: 80,
|
||||||
child: LineChart(
|
child: LineChart(
|
||||||
LineChartData(
|
LineChartData(
|
||||||
lineBarsData: [
|
lineBarsData: [
|
||||||
@ -569,160 +563,86 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
Positioned(
|
Positioned(
|
||||||
left: 64,
|
left: 64,
|
||||||
bottom: 32,
|
bottom: 32,
|
||||||
child: Joystick(
|
child: SizedBox(
|
||||||
includeInitialAnimation: false,
|
width: 160,
|
||||||
period: Duration(milliseconds: callBackPeriod),
|
height: 160,
|
||||||
stick: CircleAvatar(
|
child: SquareJoystick(
|
||||||
radius: 20,
|
onChanged: (Offset position) {
|
||||||
backgroundColor: Color.fromARGB(200, 255, 255, 255),
|
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(
|
Positioned(
|
||||||
right: 128,
|
right: 128,
|
||||||
bottom: 180,
|
bottom: 180,
|
||||||
child: Joystick(
|
child: SizedBox(
|
||||||
includeInitialAnimation: false,
|
width: 160,
|
||||||
period: Duration(milliseconds: callBackPeriod),
|
height: 160,
|
||||||
stick: CircleAvatar(
|
child: Joystick2D(
|
||||||
radius: 20,
|
onChanged: (Offset position) {
|
||||||
backgroundColor: Color.fromARGB(200, 255, 255, 255),
|
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(
|
Positioned(
|
||||||
bottom: 180,
|
bottom: 180,
|
||||||
right: 64,
|
right: 54,
|
||||||
child: Joystick(
|
child: SizedBox(
|
||||||
includeInitialAnimation: false,
|
width: 60,
|
||||||
period: Duration(milliseconds: callBackPeriod),
|
height: 160,
|
||||||
mode: JoystickMode.vertical,
|
child: VerticalJoystick(
|
||||||
stick: CircleAvatar(
|
onChanged: (double y) {
|
||||||
radius: 20,
|
setState(() {
|
||||||
backgroundColor: Color.fromARGB(200, 255, 255, 255),
|
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(
|
Positioned(
|
||||||
bottom: 100,
|
bottom: 90,
|
||||||
right: 64,
|
right: 64,
|
||||||
child: Joystick(
|
child: SizedBox(
|
||||||
includeInitialAnimation: false,
|
width: 160,
|
||||||
period: Duration(milliseconds: callBackPeriod),
|
height: 60,
|
||||||
mode: JoystickMode.horizontal,
|
child: HorizontalJoystick(
|
||||||
stick: CircleAvatar(
|
onChanged: (double x) {
|
||||||
radius: 20,
|
setState(() {
|
||||||
backgroundColor: Color.fromARGB(200, 255, 255, 255),
|
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(
|
Positioned(
|
||||||
bottom: 40,
|
bottom: 30,
|
||||||
right: 64,
|
right: 64,
|
||||||
child: Joystick(
|
child: SizedBox(
|
||||||
includeInitialAnimation: false,
|
width: 160,
|
||||||
period: Duration(milliseconds: callBackPeriod),
|
height: 60,
|
||||||
mode: JoystickMode.horizontal,
|
child: HorizontalJoystick(
|
||||||
stick: CircleAvatar(
|
onChanged: (double x) {
|
||||||
radius: 20,
|
setState(() {
|
||||||
backgroundColor: Color.fromARGB(200, 255, 255, 255),
|
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(
|
Positioned(
|
||||||
@ -799,9 +719,9 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
child: SegmentedButton<double>(
|
child: SegmentedButton<double>(
|
||||||
segments: const <ButtonSegment<double>>[
|
segments: const <ButtonSegment<double>>[
|
||||||
ButtonSegment<double>(value: 0.05, label: Text('精确')),
|
ButtonSegment<double>(value: 0.05, label: Text('精确')),
|
||||||
ButtonSegment<double>(value: 0.2, label: Text('缓慢')),
|
ButtonSegment<double>(value: 0.10, label: Text('缓慢')),
|
||||||
ButtonSegment<double>(value: 0.5, label: Text('默认')),
|
ButtonSegment<double>(value: 0.35, label: Text('默认')),
|
||||||
ButtonSegment<double>(value: 1.0, label: Text('狂暴')),
|
ButtonSegment<double>(value: 0.5, label: Text('狂暴')),
|
||||||
],
|
],
|
||||||
selected: _pawScaleSelection,
|
selected: _pawScaleSelection,
|
||||||
onSelectionChanged: (Set<double> newSelection) {
|
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"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
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:
|
clock:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -142,6 +158,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.2"
|
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:
|
flutter_lints:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@ -184,6 +208,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.5.4"
|
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:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -565,6 +597,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.5.0"
|
version: "6.5.0"
|
||||||
|
yaml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: yaml
|
||||||
|
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.3"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.8.1 <4.0.0"
|
dart: ">=3.8.1 <4.0.0"
|
||||||
flutter: ">=3.27.4"
|
flutter: ">=3.27.4"
|
||||||
|
@ -54,6 +54,7 @@ dev_dependencies:
|
|||||||
# package. See that file for information about deactivating specific lint
|
# package. See that file for information about deactivating specific lint
|
||||||
# rules and activating additional ones.
|
# rules and activating additional ones.
|
||||||
flutter_lints: ^5.0.0
|
flutter_lints: ^5.0.0
|
||||||
|
flutter_launcher_icons: "^0.14.4"
|
||||||
|
|
||||||
# For information on the generic Dart part of this file, see the
|
# For information on the generic Dart part of this file, see the
|
||||||
# following page: https://dart.dev/tools/pub/pubspec
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
@ -67,9 +68,8 @@ flutter:
|
|||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|
||||||
# To add assets to your application, add an assets section, like this:
|
# To add assets to your application, add an assets section, like this:
|
||||||
# assets:
|
assets:
|
||||||
# - images/a_dot_burr.jpeg
|
- assets/images/
|
||||||
# - images/a_dot_ham.jpeg
|
|
||||||
|
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
# https://flutter.dev/to/resolution-aware-images
|
# 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