Skip to content

fix: compass arrow jitter and instability during outdoor use #27

@fionan313

Description

@fionan313

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

  1. Raw sensor data applied directlyonSensorChanged fires every few milliseconds. Without smoothing, each raw accelerometer and magnetometer reading is applied directly to the arrow rotation, causing rapid visible oscillation.

  2. 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.

  3. No animation interpolation — arrow snaps instantly between rotation values rather than animating smoothly between them.

  4. GPS bearing unstable when stationaryLocation.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

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingenhancementNew feature or request

Projects

Status

Ready

Relationships

None yet

Development

No branches or pull requests

Issue actions