From 25538451009af2391b4988a281c40f1f152ae736 Mon Sep 17 00:00:00 2001 From: Love Billenius Date: Fri, 10 Apr 2026 10:05:57 +0200 Subject: [PATCH] fix Android swipe gesture edge cases --- PLAN.md | 1 + .../app/core/gestures/HermesGestures.kt | 35 +++++++++++++++++- .../gestures/HermesSwipeDownDetectorTest.kt | 37 +++++++++++++++++++ 3 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 mobile/android-app/app/src/test/java/com/hermes/app/core/gestures/HermesSwipeDownDetectorTest.kt diff --git a/PLAN.md b/PLAN.md index abd26b2..22889e9 100644 --- a/PLAN.md +++ b/PLAN.md @@ -24,6 +24,7 @@ This is the canonical working plan and progress log for the project. Use this fi - iOS project scaffolding now exists as `mobile/ios-app/HermesApp.xcodeproj` with a shared scheme and XCTest target. - iOS sources and tests build and pass in Xcode on an iPhone simulator, and a standalone localization/error-mapping harness passes. - Android local unit tests now cover localization, error mapping, and analytics batch conversion, and `./gradlew testDebugUnitTest` passes with the Android SDK installed. +- Android swipe-down gesture handling now uses cumulative drag distance, and edge-case tests cover upward drags, slow swipes, and per-gesture single-trigger behavior. ### Still Open diff --git a/mobile/android-app/app/src/main/java/com/hermes/app/core/gestures/HermesGestures.kt b/mobile/android-app/app/src/main/java/com/hermes/app/core/gestures/HermesGestures.kt index 6fa7784..23fcb7f 100644 --- a/mobile/android-app/app/src/main/java/com/hermes/app/core/gestures/HermesGestures.kt +++ b/mobile/android-app/app/src/main/java/com/hermes/app/core/gestures/HermesGestures.kt @@ -4,9 +4,40 @@ import androidx.compose.foundation.gestures.detectVerticalDragGestures import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.pointerInput +internal class HermesSwipeDownDetector( + private val threshold: Float = 32f, +) { + private var totalDrag = 0f + private var triggered = false + + fun onDrag(dragAmount: Float): Boolean { + if (triggered) { + return false + } + + totalDrag = (totalDrag + dragAmount).coerceAtLeast(0f) + if (totalDrag >= threshold) { + triggered = true + return true + } + + return false + } + + fun reset() { + totalDrag = 0f + triggered = false + } +} + fun Modifier.hermesSwipeDown(onSwipeDown: () -> Unit): Modifier = pointerInput(onSwipeDown) { - detectVerticalDragGestures { _, dragAmount -> - if (dragAmount > 32f) { + val detector = HermesSwipeDownDetector() + detectVerticalDragGestures( + onDragStart = { detector.reset() }, + onDragEnd = { detector.reset() }, + onDragCancel = { detector.reset() }, + ) { _, dragAmount -> + if (detector.onDrag(dragAmount)) { onSwipeDown() } } diff --git a/mobile/android-app/app/src/test/java/com/hermes/app/core/gestures/HermesSwipeDownDetectorTest.kt b/mobile/android-app/app/src/test/java/com/hermes/app/core/gestures/HermesSwipeDownDetectorTest.kt new file mode 100644 index 0000000..5affa51 --- /dev/null +++ b/mobile/android-app/app/src/test/java/com/hermes/app/core/gestures/HermesSwipeDownDetectorTest.kt @@ -0,0 +1,37 @@ +package com.hermes.app.core.gestures + +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class HermesSwipeDownDetectorTest { + @Test + fun triggersAfterCumulativeDownwardDrag() { + val detector = HermesSwipeDownDetector(threshold = 32f) + + assertFalse(detector.onDrag(10f)) + assertFalse(detector.onDrag(10f)) + assertTrue(detector.onDrag(12f)) + } + + @Test + fun doesNotTriggerForSmallOrUpwardDrags() { + val detector = HermesSwipeDownDetector(threshold = 32f) + + assertFalse(detector.onDrag(20f)) + assertFalse(detector.onDrag(-15f)) + assertFalse(detector.onDrag(20f)) + } + + @Test + fun onlyTriggersOncePerGestureUntilReset() { + val detector = HermesSwipeDownDetector(threshold = 32f) + + assertTrue(detector.onDrag(40f)) + assertFalse(detector.onDrag(40f)) + + detector.reset() + + assertTrue(detector.onDrag(40f)) + } +}