tilt_detector.dart 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. import 'dart:async';
  2. import 'dart:math' as math;
  3. import 'package:sensors_plus/sensors_plus.dart';
  4. class TiltDetector {
  5. static final TiltDetector _instance = TiltDetector._internal();
  6. factory TiltDetector() => _instance;
  7. TiltDetector._internal();
  8. StreamSubscription<AccelerometerEvent>? _accelerometerSubscription;
  9. Function(double)? onTiltChanged;
  10. Function()? onShakeDetected;
  11. bool _isListening = false;
  12. double _currentTilt = 0.0;
  13. // Filtering parameters to smooth out noise
  14. static const double _alpha = 0.8; // Low-pass filter constant
  15. double _filteredX = 0.0;
  16. // Shake detection parameters
  17. static const double _shakeThreshold = 18.0; // Acceleration threshold for shake
  18. static const int _shakeTimeWindow = 500; // ms window for shake detection
  19. static const int _shakeDebounceTime = 1000; // ms debounce between shake events
  20. final List<double> _accelerationHistory = [];
  21. final List<int> _accelerationTimestamps = [];
  22. int _lastShakeTime = 0;
  23. bool get isListening => _isListening;
  24. double get currentTilt => _currentTilt;
  25. /// Start listening to accelerometer data
  26. /// [onTiltChanged] callback receives tilt angle in degrees
  27. /// [onShakeDetected] callback is called when shake is detected
  28. /// Positive values = device tilted right, negative = tilted left
  29. void startListening({
  30. Function(double)? onTiltChanged,
  31. Function()? onShakeDetected,
  32. }) {
  33. if (_isListening) return;
  34. this.onTiltChanged = onTiltChanged;
  35. this.onShakeDetected = onShakeDetected;
  36. _isListening = true;
  37. _accelerometerSubscription = accelerometerEventStream(
  38. samplingPeriod: const Duration(milliseconds: 100), // Increased frequency for shake detection
  39. ).listen(_onAccelerometerEvent);
  40. }
  41. /// Stop listening to accelerometer data
  42. void stopListening() {
  43. if (!_isListening) return;
  44. _accelerometerSubscription?.cancel();
  45. _accelerometerSubscription = null;
  46. _isListening = false;
  47. onTiltChanged = null;
  48. onShakeDetected = null;
  49. _currentTilt = 0.0;
  50. _filteredX = 0.0;
  51. _accelerationHistory.clear();
  52. _accelerationTimestamps.clear();
  53. }
  54. void _onAccelerometerEvent(AccelerometerEvent event) {
  55. final currentTime = DateTime.now().millisecondsSinceEpoch;
  56. // Calculate total acceleration magnitude for shake detection
  57. final acceleration = math.sqrt(
  58. event.x * event.x + event.y * event.y + event.z * event.z
  59. );
  60. // Add to history for shake detection
  61. _accelerationHistory.add(acceleration);
  62. _accelerationTimestamps.add(currentTime);
  63. // Remove old entries outside the time window
  64. while (_accelerationTimestamps.isNotEmpty &&
  65. currentTime - _accelerationTimestamps.first > _shakeTimeWindow) {
  66. _accelerationHistory.removeAt(0);
  67. _accelerationTimestamps.removeAt(0);
  68. }
  69. // Check for shake pattern
  70. _checkForShake();
  71. // Apply low-pass filter to smooth out noise for tilt detection
  72. _filteredX = _alpha * _filteredX + (1 - _alpha) * event.x;
  73. // Calculate tilt angle in degrees
  74. // The accelerometer X axis represents left-right tilt
  75. // Clamp to reasonable tilt range (-45 to +45 degrees)
  76. final tiltRadians = math.atan2(_filteredX, 9.8);
  77. _currentTilt = (tiltRadians * 180 / math.pi).clamp(-45.0, 45.0);
  78. // Only trigger callback if tilt is significant (> 5 degrees)
  79. if (_currentTilt.abs() > 5.0) {
  80. onTiltChanged?.call(_currentTilt);
  81. }
  82. }
  83. void _checkForShake() {
  84. if (_accelerationHistory.length < 3) return;
  85. // Look for rapid changes in acceleration that indicate shaking
  86. double maxAcceleration = _accelerationHistory.reduce(math.max);
  87. double minAcceleration = _accelerationHistory.reduce(math.min);
  88. // Check if we have significant acceleration variation (shake pattern)
  89. if (maxAcceleration - minAcceleration > _shakeThreshold) {
  90. // Check for multiple peaks to confirm shake
  91. int peaks = 0;
  92. for (int i = 1; i < _accelerationHistory.length - 1; i++) {
  93. if (_accelerationHistory[i] > _accelerationHistory[i - 1] &&
  94. _accelerationHistory[i] > _accelerationHistory[i + 1] &&
  95. _accelerationHistory[i] > 12.0) { // Peak threshold
  96. peaks++;
  97. }
  98. }
  99. // If we have at least 2 peaks, it's likely a shake
  100. if (peaks >= 2) {
  101. _handleShakeDetected();
  102. }
  103. }
  104. }
  105. /// Get normalized tilt strength from -1.0 to 1.0
  106. /// -1.0 = maximum left tilt, +1.0 = maximum right tilt
  107. double get normalizedTilt {
  108. return (_currentTilt / 45.0).clamp(-1.0, 1.0);
  109. }
  110. /// Check if device is significantly tilted
  111. bool get isTilted => _currentTilt.abs() > 5.0;
  112. /// Check if device is tilted left
  113. bool get isTiltedLeft => _currentTilt < -5.0;
  114. /// Check if device is tilted right
  115. bool get isTiltedRight => _currentTilt > 5.0;
  116. /// Handle shake detection with debouncing
  117. void _handleShakeDetected() {
  118. final currentTime = DateTime.now().millisecondsSinceEpoch;
  119. // Only trigger shake if enough time has passed since last shake
  120. if (currentTime - _lastShakeTime >= _shakeDebounceTime) {
  121. _lastShakeTime = currentTime;
  122. onShakeDetected?.call();
  123. // Clear history to prevent multiple rapid shake detections
  124. _accelerationHistory.clear();
  125. _accelerationTimestamps.clear();
  126. }
  127. }
  128. /// Get time since last shake in milliseconds
  129. int get timeSinceLastShake {
  130. return DateTime.now().millisecondsSinceEpoch - _lastShakeTime;
  131. }
  132. /// Check if shake is currently debounced
  133. bool get isShakeDebounced {
  134. return timeSinceLastShake < _shakeDebounceTime;
  135. }
  136. }