import 'dart:async'; import 'dart:math' as math; import 'package:sensors_plus/sensors_plus.dart'; class TiltDetector { static final TiltDetector _instance = TiltDetector._internal(); factory TiltDetector() => _instance; TiltDetector._internal(); StreamSubscription? _accelerometerSubscription; Function(double)? onTiltChanged; Function()? onShakeDetected; bool _isListening = false; double _currentTilt = 0.0; // Filtering parameters to smooth out noise static const double _alpha = 0.8; // Low-pass filter constant double _filteredX = 0.0; // Shake detection parameters static const double _shakeThreshold = 18.0; // Acceleration threshold for shake static const int _shakeTimeWindow = 500; // ms window for shake detection static const int _shakeDebounceTime = 1000; // ms debounce between shake events final List _accelerationHistory = []; final List _accelerationTimestamps = []; int _lastShakeTime = 0; bool get isListening => _isListening; double get currentTilt => _currentTilt; /// Start listening to accelerometer data /// [onTiltChanged] callback receives tilt angle in degrees /// [onShakeDetected] callback is called when shake is detected /// Positive values = device tilted right, negative = tilted left void startListening({ Function(double)? onTiltChanged, Function()? onShakeDetected, }) { if (_isListening) return; this.onTiltChanged = onTiltChanged; this.onShakeDetected = onShakeDetected; _isListening = true; _accelerometerSubscription = accelerometerEventStream( samplingPeriod: const Duration(milliseconds: 100), // Increased frequency for shake detection ).listen(_onAccelerometerEvent); } /// Stop listening to accelerometer data void stopListening() { if (!_isListening) return; _accelerometerSubscription?.cancel(); _accelerometerSubscription = null; _isListening = false; onTiltChanged = null; onShakeDetected = null; _currentTilt = 0.0; _filteredX = 0.0; _accelerationHistory.clear(); _accelerationTimestamps.clear(); } void _onAccelerometerEvent(AccelerometerEvent event) { final currentTime = DateTime.now().millisecondsSinceEpoch; // Calculate total acceleration magnitude for shake detection final acceleration = math.sqrt( event.x * event.x + event.y * event.y + event.z * event.z ); // Add to history for shake detection _accelerationHistory.add(acceleration); _accelerationTimestamps.add(currentTime); // Remove old entries outside the time window while (_accelerationTimestamps.isNotEmpty && currentTime - _accelerationTimestamps.first > _shakeTimeWindow) { _accelerationHistory.removeAt(0); _accelerationTimestamps.removeAt(0); } // Check for shake pattern _checkForShake(); // Apply low-pass filter to smooth out noise for tilt detection _filteredX = _alpha * _filteredX + (1 - _alpha) * event.x; // Calculate tilt angle in degrees // The accelerometer X axis represents left-right tilt // Clamp to reasonable tilt range (-45 to +45 degrees) final tiltRadians = math.atan2(_filteredX, 9.8); _currentTilt = (tiltRadians * 180 / math.pi).clamp(-45.0, 45.0); // Only trigger callback if tilt is significant (> 5 degrees) if (_currentTilt.abs() > 5.0) { onTiltChanged?.call(_currentTilt); } } void _checkForShake() { if (_accelerationHistory.length < 3) return; // Look for rapid changes in acceleration that indicate shaking double maxAcceleration = _accelerationHistory.reduce(math.max); double minAcceleration = _accelerationHistory.reduce(math.min); // Check if we have significant acceleration variation (shake pattern) if (maxAcceleration - minAcceleration > _shakeThreshold) { // Check for multiple peaks to confirm shake int peaks = 0; for (int i = 1; i < _accelerationHistory.length - 1; i++) { if (_accelerationHistory[i] > _accelerationHistory[i - 1] && _accelerationHistory[i] > _accelerationHistory[i + 1] && _accelerationHistory[i] > 12.0) { // Peak threshold peaks++; } } // If we have at least 2 peaks, it's likely a shake if (peaks >= 2) { _handleShakeDetected(); } } } /// Get normalized tilt strength from -1.0 to 1.0 /// -1.0 = maximum left tilt, +1.0 = maximum right tilt double get normalizedTilt { return (_currentTilt / 45.0).clamp(-1.0, 1.0); } /// Check if device is significantly tilted bool get isTilted => _currentTilt.abs() > 5.0; /// Check if device is tilted left bool get isTiltedLeft => _currentTilt < -5.0; /// Check if device is tilted right bool get isTiltedRight => _currentTilt > 5.0; /// Handle shake detection with debouncing void _handleShakeDetected() { final currentTime = DateTime.now().millisecondsSinceEpoch; // Only trigger shake if enough time has passed since last shake if (currentTime - _lastShakeTime >= _shakeDebounceTime) { _lastShakeTime = currentTime; onShakeDetected?.call(); // Clear history to prevent multiple rapid shake detections _accelerationHistory.clear(); _accelerationTimestamps.clear(); } } /// Get time since last shake in milliseconds int get timeSinceLastShake { return DateTime.now().millisecondsSinceEpoch - _lastShakeTime; } /// Check if shake is currently debounced bool get isShakeDebounced { return timeSinceLastShake < _shakeDebounceTime; } }