|
|
|
|
@ -1,3 +1,5 @@
|
|
|
|
|
import 'dart:math';
|
|
|
|
|
|
|
|
|
|
import 'package:diplomaticquarterapp/widgets/others/app_scaffold_widget.dart';
|
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
import 'package:flutter_svg/flutter_svg.dart';
|
|
|
|
|
@ -10,13 +12,17 @@ import 'components/scan_condition_checklist.dart';
|
|
|
|
|
import 'components/scan_status.dart';
|
|
|
|
|
import 'components/start_button.dart';
|
|
|
|
|
import 'result_screen.dart';
|
|
|
|
|
import 'package:lottie/lottie.dart';
|
|
|
|
|
|
|
|
|
|
final UserInfo userInfo = UserInfo(
|
|
|
|
|
age: 30,
|
|
|
|
|
gender: Gender.male,
|
|
|
|
|
weight: 75, // kg, Optional
|
|
|
|
|
height: 164, // cm, Optional
|
|
|
|
|
waistCircumference: 83, // cm Optional
|
|
|
|
|
weight: 75,
|
|
|
|
|
// kg, Optional
|
|
|
|
|
height: 164,
|
|
|
|
|
// cm, Optional
|
|
|
|
|
waistCircumference: 83,
|
|
|
|
|
// cm Optional
|
|
|
|
|
userId: 'dbd13e86-47f4-4a43-85f6-cf62fa750117');
|
|
|
|
|
|
|
|
|
|
final VitalSignCameraConfig config = VitalSignCameraConfig(
|
|
|
|
|
@ -24,7 +30,9 @@ final VitalSignCameraConfig config = VitalSignCameraConfig(
|
|
|
|
|
serverId: ServerId.awsEnterpriseProd);
|
|
|
|
|
|
|
|
|
|
class VitalSigns extends StatefulWidget {
|
|
|
|
|
const VitalSigns({super.key});
|
|
|
|
|
const VitalSigns({super.key, required this.userInfo});
|
|
|
|
|
|
|
|
|
|
final UserInfo userInfo;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
State<VitalSigns> createState() => _VitalSignsState();
|
|
|
|
|
@ -49,9 +57,9 @@ class _VitalSignsState extends State<VitalSigns> with RouteAware {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool startedScanning =
|
|
|
|
|
false; // set true when start button is pressed, set false when health result is tapped or error occurs
|
|
|
|
|
false; // set true when start button is pressed, set false when health result is tapped or error occurs
|
|
|
|
|
bool isAllConditionsMet =
|
|
|
|
|
false; // check for the 6 scan conditions before enabling the start button
|
|
|
|
|
false; // check for the 6 scan conditions before enabling the start button
|
|
|
|
|
|
|
|
|
|
ScanConditions? _conditions;
|
|
|
|
|
GetHealthStage? _scanningStage;
|
|
|
|
|
@ -95,63 +103,71 @@ class _VitalSignsState extends State<VitalSigns> with RouteAware {
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
final deviceSize = MediaQuery.of(context).size;
|
|
|
|
|
return AppScaffold(
|
|
|
|
|
isShowAppBar: true,
|
|
|
|
|
appBarTitle: TranslationBase.of(context).scanning,
|
|
|
|
|
showNewAppBar: true,
|
|
|
|
|
showNewAppBarTitle: true,
|
|
|
|
|
overrideUserLogin: true,
|
|
|
|
|
backgroundColor: Color(0xffF8F8F8),
|
|
|
|
|
body: Column(
|
|
|
|
|
children: [
|
|
|
|
|
if (_conditions != null)
|
|
|
|
|
ScanConditionChecklist(
|
|
|
|
|
deviceSize: deviceSize, conditions: _conditions!),
|
|
|
|
|
Expanded(
|
|
|
|
|
child: Stack(
|
|
|
|
|
children: [
|
|
|
|
|
VitalSignCamera(
|
|
|
|
|
onCreated: _onVitalSignCameraCreated,
|
|
|
|
|
isActive: isCameraActive,
|
|
|
|
|
userInfo: userInfo,
|
|
|
|
|
config: config,
|
|
|
|
|
device: cameraDevice,
|
|
|
|
|
onVideoFrameProcessed: _onVideoFrameProcessed),
|
|
|
|
|
Positioned.fill(
|
|
|
|
|
// child: CustomPaint(
|
|
|
|
|
// painter: OvalOverlayPainter(),
|
|
|
|
|
// ),
|
|
|
|
|
child: SvgPicture.asset(
|
|
|
|
|
"assets/images/svg/overlay.svg",
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
if (_scanningStage !=
|
|
|
|
|
GetHealthStage
|
|
|
|
|
.idle) // show remaining time count down during scan
|
|
|
|
|
ScanStatus(
|
|
|
|
|
stage: _scanningStage, remainingTime: _remainingTime),
|
|
|
|
|
Column(children: [
|
|
|
|
|
ScanConditionChecklist(
|
|
|
|
|
deviceSize: deviceSize, conditions: _conditions!),
|
|
|
|
|
if (_scanningStage == GetHealthStage.idle &&
|
|
|
|
|
isCameraActive) // show start button only when it is not scanning
|
|
|
|
|
StartButton(
|
|
|
|
|
onPressed: () {
|
|
|
|
|
setState(() {
|
|
|
|
|
_vitalSignCameraController.startScanning();
|
|
|
|
|
startedScanning = true;
|
|
|
|
|
_healthResult = null;
|
|
|
|
|
_showedHealthResult = false;
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
disabled: !isAllConditionsMet,
|
|
|
|
|
),
|
|
|
|
|
])
|
|
|
|
|
],
|
|
|
|
|
return Stack(
|
|
|
|
|
children: [
|
|
|
|
|
VitalSignCamera(
|
|
|
|
|
onCreated: _onVitalSignCameraCreated,
|
|
|
|
|
isActive: isCameraActive,
|
|
|
|
|
userInfo: widget.userInfo,
|
|
|
|
|
config: config,
|
|
|
|
|
device: cameraDevice,
|
|
|
|
|
onVideoFrameProcessed: _onVideoFrameProcessed),
|
|
|
|
|
if (_scanningStage !=
|
|
|
|
|
GetHealthStage.idle) // show remaining time count down during scan
|
|
|
|
|
Align(
|
|
|
|
|
alignment: Alignment.bottomCenter,
|
|
|
|
|
child: Padding(
|
|
|
|
|
padding: const EdgeInsets.only(bottom: 16.0),
|
|
|
|
|
child: ScanStatus(
|
|
|
|
|
stage: _scanningStage, remainingTime: _remainingTime),
|
|
|
|
|
)),
|
|
|
|
|
if (mounted)
|
|
|
|
|
Column(children: [
|
|
|
|
|
Expanded(
|
|
|
|
|
child: CircularOverlay(
|
|
|
|
|
state: (_scanningStage == null ||
|
|
|
|
|
_scanningStage == GetHealthStage.idle)
|
|
|
|
|
? AnimationState.stop
|
|
|
|
|
: AnimationState.playing,
|
|
|
|
|
)),
|
|
|
|
|
if (_conditions != null)
|
|
|
|
|
ScanConditionChecklist(
|
|
|
|
|
deviceSize: deviceSize, conditions: _conditions!),
|
|
|
|
|
SizedBox(
|
|
|
|
|
height: 16,
|
|
|
|
|
),
|
|
|
|
|
if (_scanningStage == GetHealthStage.idle &&
|
|
|
|
|
isCameraActive) // show start button only when it is not scanning
|
|
|
|
|
...{
|
|
|
|
|
StartButton(
|
|
|
|
|
onPressed: () {
|
|
|
|
|
setState(() {
|
|
|
|
|
_vitalSignCameraController.startScanning();
|
|
|
|
|
startedScanning = true;
|
|
|
|
|
_healthResult = null;
|
|
|
|
|
_showedHealthResult = false;
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
disabled: !isAllConditionsMet,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
} else
|
|
|
|
|
SizedBox(
|
|
|
|
|
height: 55,
|
|
|
|
|
)
|
|
|
|
|
]),
|
|
|
|
|
Padding(
|
|
|
|
|
padding: const EdgeInsetsDirectional.only(top: 32.0, start: 14),
|
|
|
|
|
child: GestureDetector(
|
|
|
|
|
onTap: () {
|
|
|
|
|
Navigator.pop(context);
|
|
|
|
|
},
|
|
|
|
|
child: SvgPicture.asset(
|
|
|
|
|
'assets/images/svg/cross.svg',
|
|
|
|
|
width: 32,
|
|
|
|
|
height: 32,
|
|
|
|
|
)),
|
|
|
|
|
)
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
// return Scaffold(
|
|
|
|
|
// body: Stack(
|
|
|
|
|
@ -283,11 +299,118 @@ class _VitalSignsState extends State<VitalSigns> with RouteAware {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
enum AnimationState { playing, stop }
|
|
|
|
|
|
|
|
|
|
class CircularOverlay extends StatefulWidget {
|
|
|
|
|
final AnimationState state;
|
|
|
|
|
|
|
|
|
|
const CircularOverlay({Key? key, this.state = AnimationState.stop})
|
|
|
|
|
: super(key: key);
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
State<CircularOverlay> createState() => _CircularOverlayState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _CircularOverlayState extends State<CircularOverlay>
|
|
|
|
|
with SingleTickerProviderStateMixin {
|
|
|
|
|
late AnimationController _controller;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void didChangeDependencies() {
|
|
|
|
|
if (widget.state == AnimationState.playing) {
|
|
|
|
|
_controller.forward(from: 0);
|
|
|
|
|
} else {
|
|
|
|
|
_controller.stop();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void didUpdateWidget(CircularOverlay oldWidget) {
|
|
|
|
|
if (widget.state == AnimationState.playing) {
|
|
|
|
|
_controller.repeat();
|
|
|
|
|
} else {
|
|
|
|
|
_controller.stop();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void initState() {
|
|
|
|
|
super.initState();
|
|
|
|
|
_controller = AnimationController(
|
|
|
|
|
vsync: this,
|
|
|
|
|
duration: const Duration(seconds: 8),
|
|
|
|
|
);
|
|
|
|
|
_controller.stop();
|
|
|
|
|
if (widget.state == AnimationState.playing) {
|
|
|
|
|
_controller.forward(from: 0);
|
|
|
|
|
} else {
|
|
|
|
|
_controller.stop();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void dispose() {
|
|
|
|
|
_controller.dispose();
|
|
|
|
|
super.dispose();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
return AnimatedBuilder(
|
|
|
|
|
animation: _controller,
|
|
|
|
|
builder: (context, child) {
|
|
|
|
|
return CustomPaint(
|
|
|
|
|
painter: CircularBarsPainter(animationValue: _controller.value),
|
|
|
|
|
size: Size(MediaQuery.sizeOf(context).width * .9,
|
|
|
|
|
MediaQuery.sizeOf(context).height * .8),
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class CircularBarsPainter extends CustomPainter {
|
|
|
|
|
final double animationValue;
|
|
|
|
|
|
|
|
|
|
CircularBarsPainter({required this.animationValue});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void paint(Canvas canvas, Size size) {
|
|
|
|
|
final center = Offset(size.width / 2, size.height / 1.8);
|
|
|
|
|
final radius = min(size.width / 2, size.height / 2);
|
|
|
|
|
final barPaint = Paint()
|
|
|
|
|
..color = Colors.white
|
|
|
|
|
..strokeWidth = 4
|
|
|
|
|
..style = PaintingStyle.stroke;
|
|
|
|
|
|
|
|
|
|
const numberOfBars = 64;
|
|
|
|
|
final angleIncrement = 2 * pi / numberOfBars;
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < numberOfBars; i++) {
|
|
|
|
|
final angle = i * angleIncrement + (animationValue * 2 * pi);
|
|
|
|
|
final start = Offset(
|
|
|
|
|
center.dx + radius * cos(angle),
|
|
|
|
|
center.dy + radius * sin(angle),
|
|
|
|
|
);
|
|
|
|
|
final end = Offset(
|
|
|
|
|
center.dx + (radius - 20) * cos(angle),
|
|
|
|
|
center.dy + (radius - 20) * sin(angle),
|
|
|
|
|
);
|
|
|
|
|
canvas.drawLine(start, end, barPaint);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
bool shouldRepaint(CircularBarsPainter oldDelegate) {
|
|
|
|
|
return oldDelegate.animationValue != animationValue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class OvalOverlayPainter extends CustomPainter {
|
|
|
|
|
@override
|
|
|
|
|
void paint(Canvas canvas, Size size) {
|
|
|
|
|
final Paint overlayPaint = Paint()
|
|
|
|
|
..color = Colors.white.withOpacity(0.7)
|
|
|
|
|
..color = Colors.white.withOpacity(0.1)
|
|
|
|
|
..style = PaintingStyle.fill;
|
|
|
|
|
|
|
|
|
|
canvas.drawRect(
|
|
|
|
|
@ -298,7 +421,7 @@ class OvalOverlayPainter extends CustomPainter {
|
|
|
|
|
final Rect ovalRect = Rect.fromCenter(
|
|
|
|
|
center: Offset(size.width / 2, size.height / 2),
|
|
|
|
|
width: size.width * 0.9,
|
|
|
|
|
height: size.height * 0.7,
|
|
|
|
|
height: size.height,
|
|
|
|
|
);
|
|
|
|
|
final Path ovalPath = Path()..addOval(ovalRect);
|
|
|
|
|
|
|
|
|
|
|