proxybuy-flutter/lib/screens/mains/hunt/widgets/hint_camera_widget.dart

1239 lines
42 KiB
Dart

import 'dart:async';
import 'dart:math' as math;
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:geolocator/geolocator.dart';
import 'package:latlong2/latlong.dart';
import 'package:lba/gen/assets.gen.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:flutter_compass/flutter_compass.dart';
import 'package:lba/res/colors.dart';
import '../services/location_service.dart';
import 'package:flutter_map/flutter_map.dart';
class HintCameraWidget extends StatefulWidget {
final double targetLatitude;
final double targetLongitude;
final String hintDescription;
final VoidCallback onHintFound;
final VoidCallback onClose;
final Duration? remainingTime;
final String? cardTitle;
const HintCameraWidget({
super.key,
required this.targetLatitude,
required this.targetLongitude,
required this.hintDescription,
required this.onHintFound,
required this.onClose,
this.remainingTime,
this.cardTitle,
});
@override
State<HintCameraWidget> createState() => _HintCameraWidgetState();
}
class _HintCameraWidgetState extends State<HintCameraWidget>
with TickerProviderStateMixin {
MobileScannerController? _controller;
StreamSubscription<CompassEvent>? _compassSubscription;
StreamSubscription<Position>? _locationSubscription;
Timer? _countdownTimer;
final MapController _miniMapController = MapController();
double _deviceHeading = 0.0;
double _bearingToTarget = 0.0;
double _distanceToTarget = 0.0;
Position? _currentPosition;
bool _isNearTarget = false;
bool _wasNearTarget = false;
bool _isMapVisible = false;
Duration _currentRemainingTime = Duration.zero;
late AnimationController _rotationController;
late AnimationController _scanController;
late AnimationController _entranceController;
late Animation<double> _rotationAnimation;
late Animation<double> _scanAnimation;
late Animation<double> _entranceOpacityAnimation;
late Animation<double> _entranceScaleAnimation;
late Animation<Offset> _entranceSlideAnimation;
late AnimationController _transformController;
late Animation<double> _transformAnimation;
final double _cameraHorizontalFov = 60.0;
@override
void initState() {
super.initState();
_initializeCamera();
_setupAnimations();
_startCompassListening();
_startLocationUpdates();
_initializeTimer();
}
void _initializeTimer() {
if (widget.remainingTime != null) {
_currentRemainingTime = widget.remainingTime!.inSeconds == 0
? const Duration(hours: 12)
: widget.remainingTime!;
_startCountdownTimer();
}
}
void _startCountdownTimer() {
_countdownTimer?.cancel();
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (mounted) {
setState(() {
if (_currentRemainingTime.inSeconds > 0) {
_currentRemainingTime =
Duration(seconds: _currentRemainingTime.inSeconds - 1);
} else {
timer.cancel();
}
});
}
});
}
void _initializeCamera() {
_controller = MobileScannerController(
detectionSpeed: DetectionSpeed.noDuplicates,
facing: CameraFacing.back,
);
}
void _setupAnimations() {
_rotationController = AnimationController(
duration: const Duration(seconds: 8),
vsync: this,
)..repeat();
_scanController = AnimationController(
duration: const Duration(milliseconds: 3500),
vsync: this,
)..repeat();
_entranceController = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
);
_rotationAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _rotationController, curve: Curves.linear),
);
_scanAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _scanController, curve: Curves.easeInOutSine),
);
_entranceOpacityAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _entranceController,
curve: const Interval(0.0, 0.8, curve: Curves.easeOut),
),
);
_entranceScaleAnimation = Tween<double>(begin: 0.3, end: 1.0).animate(
CurvedAnimation(
parent: _entranceController,
curve: const Interval(0.0, 0.8, curve: Curves.elasticOut),
),
);
_entranceSlideAnimation = Tween<Offset>(
begin: const Offset(0.0, 0.5),
end: Offset.zero,
).animate(
CurvedAnimation(
parent: _entranceController,
curve: const Interval(0.2, 1.0, curve: Curves.elasticOut),
),
);
_transformController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_transformAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _transformController,
curve: Curves.elasticOut,
),
);
_entranceController.forward();
}
void _startCompassListening() {
_compassSubscription = FlutterCompass.events?.listen((CompassEvent event) {
if (!mounted) return;
setState(() {
_deviceHeading = event.heading ?? 0.0;
});
});
}
void _startLocationUpdates() {
_locationSubscription =
LocationService.getPositionStream()?.listen((Position? position) {
if (position != null && mounted) {
final bearing = LocationService.getBearing(
position.latitude,
position.longitude,
widget.targetLatitude,
widget.targetLongitude,
);
final distance = LocationService.calculateDistance(
position.latitude,
position.longitude,
widget.targetLatitude,
widget.targetLongitude,
);
final isNear = distance <= 10.0;
setState(() {
_currentPosition = position;
_bearingToTarget = bearing;
_distanceToTarget = distance;
if (_isNearTarget != isNear) {
_wasNearTarget = _isNearTarget;
_isNearTarget = isNear;
if (isNear && !_wasNearTarget) {
_transformController.forward();
} else if (!isNear && _wasNearTarget) {
_transformController.reverse();
}
} else {
_isNearTarget = isNear;
}
});
if (_isMapVisible) {
_updateMiniMapCamera();
}
if (isNear) {
widget.onHintFound();
}
}
});
}
void _updateMiniMapCamera() {
if (_currentPosition != null) {
final userLocation =
LatLng(_currentPosition!.latitude, _currentPosition!.longitude);
final targetLocation =
LatLng(widget.targetLatitude, widget.targetLongitude);
_miniMapController.fitCamera(
CameraFit.bounds(
bounds: LatLngBounds(userLocation, targetLocation),
padding: const EdgeInsets.all(60.0),
),
);
}
}
String _formatDistance(double distance) {
if (distance >= 100) {
return '${distance.round()}m';
} else {
return '${distance.toStringAsFixed(1)}m';
}
}
double _getDistanceFontSize(double distance) {
if (distance >= 10000) {
return 20;
} else if (distance >= 1000) {
return 26;
} else {
return 32;
}
}
void _toggleMapVisibility() {
setState(() {
_isMapVisible = !_isMapVisible;
if (_isMapVisible) {
Future.delayed(const Duration(milliseconds: 100), () {
_updateMiniMapCamera();
});
}
});
}
@override
void dispose() {
_compassSubscription?.cancel();
_locationSubscription?.cancel();
_countdownTimer?.cancel();
_controller?.dispose();
_rotationController.dispose();
_scanController.dispose();
_entranceController.dispose();
_transformController.dispose();
_miniMapController.dispose();
super.dispose();
}
void _showARHintInfoDialog() {
showDialog(
context: context,
barrierDismissible: true,
barrierColor: Colors.black.withOpacity(0.7),
builder: (BuildContext context) {
return const _ARHintInfoDialog();
},
);
}
Widget _buildMiniMap() {
if (_currentPosition == null) return const SizedBox.shrink();
final userLocation =
LatLng(_currentPosition!.latitude, _currentPosition!.longitude);
final targetLocation =
LatLng(widget.targetLatitude, widget.targetLongitude);
return AnimatedPositioned(
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOutCubic,
bottom: _isMapVisible ? 100 : -220,
left: 20,
right: 20,
child: ClipRRect(
borderRadius: BorderRadius.circular(25),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15),
child: Container(
height: 200,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(25),
border: Border.all(color: Colors.white.withOpacity(0.2)),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(24),
child: FlutterMap(
mapController: _miniMapController,
options: MapOptions(
initialCenter: userLocation,
initialZoom: 15,
interactionOptions:
const InteractionOptions(flags: InteractiveFlag.none),
),
children: [
TileLayer(
urlTemplate:
'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png',
subdomains: const ['a', 'b', 'c', 'd'],
),
MarkerLayer(
markers: [
Marker(
point: userLocation,
width: 40,
height: 40,
child: const PulsingMarker(isUser: true),
),
Marker(
point: targetLocation,
width: 40,
height: 40,
child: const PulsingMarker(isUser: false),
),
],
),
],
),
),
),
),
),
);
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _entranceController,
builder: (context, child) {
return FadeTransition(
opacity: _entranceOpacityAnimation,
child: SlideTransition(
position: _entranceSlideAnimation,
child: ScaleTransition(
scale: _entranceScaleAnimation,
child: Scaffold(
backgroundColor: Colors.black,
extendBodyBehindAppBar: true,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(kToolbarHeight + 10),
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withOpacity(0.8),
Colors.black.withOpacity(0.4),
Colors.transparent,
],
),
),
child: ClipRRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12),
child: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
toolbarHeight: kToolbarHeight + 10,
leading: Container(
margin: const EdgeInsets.all(8),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.white.withOpacity(0.08),
Colors.white.withOpacity(0.05),
],
),
shape: BoxShape.circle,
border: Border.all(
color: Colors.white.withOpacity(0.3),
width: 1.5,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 8,
spreadRadius: 1,
),
],
),
child: IconButton(
onPressed: widget.onClose,
icon: SvgPicture.asset(Assets.icons.back.path,
colorFilter:
const ColorFilter.mode(Colors.white, BlendMode.srcIn)),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
),
title: AnimatedBuilder(
animation: _entranceController,
builder: (context, child) {
return Transform.scale(
scale: 0.8 + (0.2 * _entranceScaleAnimation.value),
child: Container(
padding:
const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppColors.primary.withOpacity(0.3 * _entranceOpacityAnimation.value),
AppColors.primary.withOpacity(0.1 * _entranceOpacityAnimation.value),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(25),
border: Border.all(
color: AppColors.primary.withOpacity(0.4 * _entranceOpacityAnimation.value),
width: 1.5,
),
boxShadow: [
BoxShadow(
color: AppColors.primary.withOpacity(0.2 * _entranceOpacityAnimation.value),
blurRadius: 15,
spreadRadius: 2,
),
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Text(
'AR Hint',
style: TextStyle(
color: Colors.white.withOpacity(_entranceOpacityAnimation.value),
fontSize: 15,
fontWeight: FontWeight.w700,
letterSpacing: 1.2,
fontFamily: 'monospace'),
),
),
);
},
),
centerTitle: true,
actions: [
Container(
margin: const EdgeInsets.all(10),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.white.withOpacity(0.10),
Colors.white.withOpacity(0.05),
],
),
shape: BoxShape.circle,
border: Border.all(
color: Colors.white.withOpacity(0.3),
width: 1.5,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 8,
spreadRadius: 1,
),
],
),
child: IconButton(
onPressed: _showARHintInfoDialog,
icon: SvgPicture.asset(Assets.icons.infoCircle.path,
colorFilter: const ColorFilter.mode(
Colors.white, BlendMode.srcIn)),
),
),
],
),
),
),
),
),
body: Stack(
children: [
if (_controller != null)
MobileScanner(controller: _controller!)
else
Container(
color: Colors.black,
child: Center(
child: CircularProgressIndicator(color: AppColors.primary),
),
),
BackdropFilter(
filter: ImageFilter.blur(sigmaX: 0.5, sigmaY: 0.5),
child: Container(color: Colors.black.withOpacity(0.1)),
),
if (widget.remainingTime != null) _buildTimerWidget(),
_buildAROverlay(),
_buildMiniMap(),
],
),
floatingActionButton: AnimatedBuilder(
animation: _entranceController,
builder: (context, child) {
return Transform.translate(
offset: Offset(0, 100 * (1 - _entranceScaleAnimation.value)),
child: Opacity(
opacity: _entranceOpacityAnimation.value,
child: Transform.scale(
scale: _entranceScaleAnimation.value,
child: Padding(
padding: const EdgeInsets.only(right: 8.0),
child: _buildMapFAB(),
),
),
),
);
},
),
),
),
),
);
},
);
}
Widget _buildTimerWidget() {
if (widget.remainingTime == null) return const SizedBox.shrink();
final Duration timeToShow = _currentRemainingTime.inSeconds <= 0
? (widget.remainingTime!.inSeconds == 0
? const Duration(hours: 12)
: Duration.zero)
: _currentRemainingTime;
final hours = timeToShow.inHours;
final minutes = timeToShow.inMinutes % 60;
final seconds = timeToShow.inSeconds % 60;
final isExpired =
_currentRemainingTime.inSeconds <= 0 && widget.remainingTime!.inSeconds > 0;
return AnimatedBuilder(
animation: _entranceController,
builder: (context, child) {
return Positioned(
top: kToolbarHeight + 60,
right: 16 - (100 * (1 - _entranceOpacityAnimation.value)),
child: Opacity(
opacity: _entranceOpacityAnimation.value,
child: Transform.scale(
scale: _entranceScaleAnimation.value,
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 25, sigmaY: 25),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: Colors.white.withOpacity(0.2 * _entranceOpacityAnimation.value),
width: 0.5,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
isExpired
? 'Expired'
: hours > 0
? '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'
: '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}',
style: TextStyle(
color: (isExpired ? const Color(0xFFFF3B30) : Colors.white)
.withOpacity(_entranceOpacityAnimation.value),
fontSize: 15,
fontWeight: FontWeight.w600,
letterSpacing: 0.5,
),
),
],
),
),
),
),
),
),
);
},
);
}
Widget _buildMapFAB() {
return ClipRRect(
borderRadius: BorderRadius.circular(18),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
decoration: BoxDecoration(
color: _isMapVisible
? AppColors.primary.withOpacity(0.5)
: Colors.white.withOpacity(0.15),
borderRadius: BorderRadius.circular(18),
border: Border.all(color: Colors.white.withOpacity(0.2)),
),
child: FloatingActionButton(
onPressed: _toggleMapVisibility,
backgroundColor: Colors.transparent,
elevation: 0,
highlightElevation: 0,
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (child, animation) {
return ScaleTransition(scale: animation, child: child);
},
child: Icon(
_isMapVisible ? Icons.map_outlined : Icons.map,
key: ValueKey<bool>(_isMapVisible),
color: Colors.white,
),
),
),
),
),
);
}
Widget _buildAROverlay() {
if (_currentPosition == null) {
return AnimatedBuilder(
animation: _entranceController,
builder: (context, child) {
return Center(
child: FadeTransition(
opacity: _entranceOpacityAnimation,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0.0, -0.3),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _entranceController,
curve: const Interval(0.4, 1.0, curve: Curves.elasticOut),
)),
child: const Text(
'ACQUIRING GPS SIGNAL...',
style: TextStyle(
color: Colors.white70,
fontSize: 16,
fontWeight: FontWeight.bold,
letterSpacing: 1.5,
),
),
),
),
);
},
);
}
double normalizeAngle(double angle) => (angle + 180) % 360 - 180;
double deviceHeadingNormalized = normalizeAngle(_deviceHeading);
double bearingToTargetNormalized = normalizeAngle(_bearingToTarget);
double angleDifference = bearingToTargetNormalized - deviceHeadingNormalized;
if (angleDifference > 180) angleDifference -= 360;
if (angleDifference < -180) angleDifference += 360;
bool isVisible = angleDifference.abs() < (_cameraHorizontalFov / 2);
return AnimatedBuilder(
animation: _entranceController,
builder: (context, child) {
return Opacity(
opacity: _entranceOpacityAnimation.value,
child: Stack(
children: [
Transform.scale(
scale: _entranceScaleAnimation.value,
child: CustomPaint(
painter: HudPainter(animationValue: _entranceOpacityAnimation.value),
size: Size.infinite,
),
),
if (!isVisible)
_buildDirectionalOverlay(angleDifference),
if (!isVisible)
_buildOffScreenIndicator(angleDifference)
else
_buildOnScreenHint(angleDifference),
],
),
);
},
);
}
Widget _buildDirectionalOverlay(double angleDifference) {
final isLeft = angleDifference < 0;
final overlayColor = _isNearTarget
? const Color(0xFF00E676)
: AppColors.primary;
return Align(
alignment: isLeft ? Alignment.centerLeft : Alignment.centerRight,
child: Container(
width: MediaQuery.of(context).size.width * 0.13,
height: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: isLeft ? Alignment.centerLeft : Alignment.centerRight,
end: isLeft ? Alignment.centerRight : Alignment.centerLeft,
stops: const [0.0, 0.2, 0.4, 0.7, 1.0],
colors: [
overlayColor.withOpacity(0.45),
overlayColor.withOpacity(0.30),
overlayColor.withOpacity(0.15),
overlayColor.withOpacity(0.05),
Colors.transparent,
],
),
),
),
);
}
Widget _buildOffScreenIndicator(double angleDifference) {
final isLeft = angleDifference < 0;
final arrowColor = _isNearTarget
? const Color(0xFF00E676)
: AppColors.primary;
return Align(
alignment: isLeft ? Alignment.centerLeft : Alignment.centerRight,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Icon(
isLeft
? Icons.arrow_back_ios_new_rounded
: Icons.arrow_forward_ios_rounded,
color: arrowColor.withOpacity(0.8),
size: 48,
shadows: [
Shadow(color: arrowColor.withOpacity(0.5), blurRadius: 15),
],
),
),
);
}
Widget _buildOnScreenHint(double angleDifference) {
final double screenX = angleDifference / (_cameraHorizontalFov / 2);
final double distance = _distanceToTarget;
return AnimatedPositioned(
duration: const Duration(milliseconds: 100),
curve: Curves.easeOut,
top: 0,
bottom: 0,
left: (1 + screenX) * (MediaQuery.of(context).size.width / 2) - 100,
child: AnimatedBuilder(
animation: Listenable.merge([_rotationAnimation, _scanAnimation, _transformAnimation]),
builder: (context, child) {
return SizedBox(
width: 200,
height: 200,
child: CustomPaint(
painter: AdvancedTargetPainter(
isNear: _isNearTarget,
distance: distance,
pulseValue: 1.0,
rotationValue: _rotationAnimation.value,
scanValue: _scanAnimation.value,
transformValue: _transformAnimation.value,
),
child: Center(
child: ClipRRect(
borderRadius: BorderRadius.circular(100),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: Container(
width: 140,
height: 140,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.05),
shape: BoxShape.circle,
border: Border.all(
color: (_isNearTarget
? const Color(0xFF00E676)
: const Color(0xFF2196F3))
.withOpacity(0.4),
width: 1,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Flexible(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
_isNearTarget ? "TARGET\nACQUIRED" : "SCANNING",
textAlign: TextAlign.center,
style: TextStyle(
color: _isNearTarget
? const Color(0xFF00E676)
: const Color(0xFF2196F3),
fontWeight: FontWeight.w600,
fontSize: _isNearTarget ? 9 : 12,
letterSpacing: _isNearTarget ? 0.5 : 1.5,
height: _isNearTarget ? 1.1 : 1.0,
fontFamily: 'monospace'),
),
),
),
),
const SizedBox(height: 12),
Text(
_formatDistance(distance),
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w800,
fontSize: _getDistanceFontSize(distance),
letterSpacing: 0.5,
fontFamily: 'monospace'),
),
],
),
),
),
),
),
),
);
},
),
);
}
}
class PulsingMarker extends StatefulWidget {
final bool isUser;
const PulsingMarker({super.key, required this.isUser});
@override
_PulsingMarkerState createState() => _PulsingMarkerState();
}
class _PulsingMarkerState extends State<PulsingMarker>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
)..repeat();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final color = widget.isUser ? Colors.blue.shade400 : Colors.yellow.shade700;
return FadeTransition(
opacity:
Tween<double>(begin: 1.0, end: 0.3).animate(_animationController),
child: ScaleTransition(
scale:
Tween<double>(begin: 0.5, end: 1.0).animate(_animationController),
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: color.withOpacity(0.5),
border: Border.all(color: color, width: 2),
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: SvgPicture.asset(
widget.isUser
? Assets.icons.gps.path
: Assets.icons.locationCross.path,
colorFilter:
const ColorFilter.mode(Colors.black, BlendMode.srcIn),
width: 10,
),
),
),
),
);
}
}
class HudPainter extends CustomPainter {
final double animationValue;
HudPainter({required this.animationValue});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = AppColors.primary.withOpacity(0.3 + (animationValue * 0.2))
..style = PaintingStyle.stroke
..strokeWidth = 2.0;
const bracketSize = 30.0;
const margin = 20.0;
canvas.drawLine(
Offset(margin, margin), Offset(margin + bracketSize, margin), paint);
canvas.drawLine(
Offset(margin, margin), Offset(margin, margin + bracketSize), paint);
canvas.drawLine(Offset(size.width - margin, margin),
Offset(size.width - margin - bracketSize, margin), paint);
canvas.drawLine(Offset(size.width - margin, margin),
Offset(size.width - margin, margin + bracketSize), paint);
canvas.drawLine(Offset(margin, size.height - margin),
Offset(margin + bracketSize, size.height - margin), paint);
canvas.drawLine(Offset(margin, size.height - margin),
Offset(margin, size.height - margin - bracketSize), paint);
canvas.drawLine(
Offset(size.width - margin, size.height - margin),
Offset(size.width - margin - bracketSize, size.height - margin),
paint);
canvas.drawLine(
Offset(size.width - margin, size.height - margin),
Offset(size.width - margin, size.height - margin - bracketSize),
paint);
}
@override
bool shouldRepaint(covariant HudPainter oldDelegate) =>
oldDelegate.animationValue != animationValue;
}
class AdvancedTargetPainter extends CustomPainter {
final bool isNear;
final double distance;
final double pulseValue;
final double rotationValue;
final double scanValue;
final double transformValue;
AdvancedTargetPainter({
required this.isNear,
required this.distance,
required this.pulseValue,
required this.rotationValue,
required this.scanValue,
required this.transformValue,
});
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final paint = Paint()..style = PaintingStyle.stroke;
final scannerColor = const Color(0xFF2196F3);
final targetColor = const Color(0xFF00E676);
final primaryColor = Color.lerp(scannerColor, targetColor, transformValue)!;
final baseRadius = math.max(60.0, math.min(85.0, 100 - (distance / 2)));
paint
..color = primaryColor.withOpacity(0.1 * pulseValue)
..strokeWidth = 20.0
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 10);
canvas.drawCircle(center, baseRadius, paint);
paint
..color = primaryColor.withOpacity(0.8)
..strokeWidth = 2.5
..maskFilter = null;
canvas.drawArc(Rect.fromCircle(center: center, radius: baseRadius),
rotationValue * 2 * math.pi, 2.5, false, paint);
canvas.drawArc(Rect.fromCircle(center: center, radius: baseRadius),
rotationValue * 2 * math.pi + 3, 2.5, false, paint);
paint
..color = primaryColor.withOpacity(0.4)
..strokeWidth = 1.5;
canvas.drawCircle(center, baseRadius - 15, paint);
if (!isNear || transformValue < 1.0) {
final sweepAngle = scanValue * 2 * math.pi;
final scannerOpacity = isNear ? (1.0 - transformValue) : 1.0;
paint
..color = scannerColor.withOpacity(scannerOpacity)
..strokeWidth = 4.0;
canvas.drawArc(
Rect.fromCircle(center: center, radius: baseRadius - 8),
sweepAngle,
math.pi / 4,
false,
paint,
);
}
if (isNear || transformValue > 0.0) {
final targetOpacity = isNear ? transformValue : (1.0 - transformValue);
paint
..color = targetColor.withOpacity((1.0 - pulseValue) * targetOpacity)
..strokeWidth = 4.0
..style = PaintingStyle.stroke;
canvas.drawCircle(center, baseRadius + (pulseValue * 15), paint);
final crosshairScale = transformValue;
final crosshairSize = 20.0 * crosshairScale;
paint
..color = targetColor.withOpacity(targetOpacity)
..strokeWidth = 3.0;
canvas.drawLine(
Offset(center.dx - crosshairSize, center.dy),
Offset(center.dx + crosshairSize, center.dy),
paint,
);
canvas.drawLine(
Offset(center.dx, center.dy - crosshairSize),
Offset(center.dx, center.dy + crosshairSize),
paint,
);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}
class _ARHintInfoDialog extends StatefulWidget {
const _ARHintInfoDialog({Key? key}) : super(key: key);
@override
State<_ARHintInfoDialog> createState() => _ARHintInfoDialogState();
}
class _ARHintInfoDialogState extends State<_ARHintInfoDialog>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scaleAnimation;
late Animation<double> _fadeAnimation;
late Animation<double> _iconScaleAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 600),
);
_scaleAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.elasticOut,
));
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _controller,
curve: const Interval(0.0, 0.8, curve: Curves.easeOut),
));
_iconScaleAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _controller,
curve: const Interval(0.2, 1.0, curve: Curves.bounceOut),
));
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return FadeTransition(
opacity: _fadeAnimation,
child: ScaleTransition(
scale: _scaleAnimation,
child: Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
elevation: 10,
backgroundColor: AppColors.surface,
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.topCenter,
children: [
Padding(
padding: const EdgeInsets.only(
top: 50,
left: 20,
right: 20,
bottom: 20,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Text(
"AR Hint Guide",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 15),
Text(
"• Point camera around to locate target\n• Blue circle shows target direction\n• Arrows guide you when target is off-screen\n• Circle turns green when target found",
style: TextStyle(
color: AppColors.popupText,
fontSize: 15,
height: 1.5,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.left,
),
const SizedBox(height: 25),
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primary,
foregroundColor: AppColors.textPrimary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.symmetric(vertical: 12),
),
onPressed: () async {
await _controller.reverse();
if (mounted) Navigator.of(context).pop();
},
child: const Text("Got it"),
),
),
],
),
),
Positioned(
top: -40,
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColors.surface,
boxShadow: [
BoxShadow(
color: AppColors.shadowColor,
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: CircleAvatar(
backgroundColor: AppColors.surface,
radius: 40,
child: ScaleTransition(
scale: _iconScaleAnimation,
child: SvgPicture.asset(
Assets.icons.ar.path,
colorFilter: ColorFilter.mode(
AppColors.primary,
BlendMode.srcIn,
),
width: 35,
),
),
),
),
),
],
),
),
),
);
}
}