| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168 |
- 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<AccelerometerEvent>? _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<double> _accelerationHistory = [];
- final List<int> _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;
- }
- }
|