fix: compass arrow jitter and instability during outdoor use
Current behaviour
The compass arrow on the finding screen spins erratically and is too jittery to be useful in real-world outdoor testing. The arrow rotates unpredictably even when the user is standing still facing the same direction. Distance updates also cause sudden arrow jumps.
Root causes identified
-
Raw sensor data applied directly — onSensorChanged fires every few milliseconds. Without smoothing, each raw accelerometer and magnetometer reading is applied directly to the arrow rotation, causing rapid visible oscillation.
-
Arrow rotation takes the longest path — without shortest-path correction, rotating from 350° to 10° causes a 340° clockwise spin instead of a 20° counter-clockwise correction.
-
No animation interpolation — arrow snaps instantly between rotation values rather than animating smoothly between them.
-
GPS bearing unstable when stationary — Location.bearingTo() becomes unreliable when the user is not moving. Small GPS coordinate jitter (sub-metre noise) causes large bearing swings when the calculated distance to the friend is short.
Fixes required
1 — Low-pass filter on compass heading
Apply exponential smoothing in onSensorChanged before updating the StateFlow:
private val alpha = 0.15f // lower = smoother, higher = more responsive
smoothedHeading = alpha * rawHeading + (1 - alpha) * smoothedHeading
_compassHeading.value = smoothedHeading
alpha = 0.15f is the recommended starting value. If still jittery
increase to 0.20f, if too sluggish reduce to 0.10f.
2 — Shortest rotation path correction
Before updating arrow rotation, calculate the shortest angular path:
fun shortestRotation(from: Float, to: Float): Float {
var diff = (to - from + 360) % 360
if (diff > 180) diff -= 360
return from + diff
}
Use the corrected value as the target for animation.
3 — Animated arrow rotation in CompassScreen
Replace direct Modifier.rotate(arrowRotation) with animated state:
val animatedRotation by animateFloatAsState(
targetValue = arrowRotation,
animationSpec = tween(durationMillis = 300, easing = LinearEasing)
)
Icon(
imageVector = Icons.Default.Navigation,
modifier = Modifier.rotate(animatedRotation)
)
4 — Only update bearing on significant movement
GPS bearing is meaningless when stationary. Gate bearing updates behind a minimum movement threshold:
if (newLocation.distanceTo(lastLocation) > 2f) {
updateBearing(newLocation)
lastLocation = newLocation
}
2 metres is the recommended threshold. Below this distance the GPS noise exceeds the real movement and the bearing becomes random.
Files to modify
presentation/compass/CompassViewModel.kt — low-pass filter,
shortest rotation, movement threshold
presentation/compass/CompassScreen.kt — animateFloatAsState
Testing
- Test outdoors with two devices at least 20 metres apart
- Arrow should hold a stable direction when user is stationary
- Arrow should rotate smoothly (not snap) when user turns
- Rotating 350° → 10° should take the short 20° path, not the long 340° path
- Walking toward the friend should show distance decreasing steadily
Notes for report
Raw sensor fusion on Android requires smoothing — this is a known characteristic of the platform rather than a bug in the implementation.
The low-pass filter approach is documented in the Android Sensor documentation and is the standard solution.
Worth mentioning in the implementation chapter as a real-world calibration challenge.
Labels: bug sensors ux
fix: compass arrow jitter and instability during outdoor use
Current behaviour
The compass arrow on the finding screen spins erratically and is too jittery to be useful in real-world outdoor testing. The arrow rotates unpredictably even when the user is standing still facing the same direction. Distance updates also cause sudden arrow jumps.
Root causes identified
Raw sensor data applied directly —
onSensorChangedfires every few milliseconds. Without smoothing, each raw accelerometer and magnetometer reading is applied directly to the arrow rotation, causing rapid visible oscillation.Arrow rotation takes the longest path — without shortest-path correction, rotating from 350° to 10° causes a 340° clockwise spin instead of a 20° counter-clockwise correction.
No animation interpolation — arrow snaps instantly between rotation values rather than animating smoothly between them.
GPS bearing unstable when stationary —
Location.bearingTo()becomes unreliable when the user is not moving. Small GPS coordinate jitter (sub-metre noise) causes large bearing swings when the calculated distance to the friend is short.Fixes required
1 — Low-pass filter on compass heading
Apply exponential smoothing in
onSensorChangedbefore updating the StateFlow:alpha = 0.15fis the recommended starting value. If still jitteryincrease to
0.20f, if too sluggish reduce to0.10f.2 — Shortest rotation path correction
Before updating arrow rotation, calculate the shortest angular path:
Use the corrected value as the target for animation.
3 — Animated arrow rotation in CompassScreen
Replace direct
Modifier.rotate(arrowRotation)with animated state:4 — Only update bearing on significant movement
GPS bearing is meaningless when stationary. Gate bearing updates behind a minimum movement threshold:
2 metres is the recommended threshold. Below this distance the GPS noise exceeds the real movement and the bearing becomes random.
Files to modify
presentation/compass/CompassViewModel.kt— low-pass filter,shortest rotation, movement threshold
presentation/compass/CompassScreen.kt— animateFloatAsStateTesting
Notes for report
Raw sensor fusion on Android requires smoothing — this is a known characteristic of the platform rather than a bug in the implementation.
The low-pass filter approach is documented in the Android Sensor documentation and is the standard solution.
Worth mentioning in the implementation chapter as a real-world calibration challenge.
Labels:
bugsensorsux