Repository: eventfahrplan/eventfahrplan
Issue: Android 12+: Stretch overscroll effect breaks schedule screen interaction

# Environment
- Android version: 12 (API 31) or newer
- Branch: `master`

# How to reproduce
1. Visit the schedule screen at the first column
2. Try to scroll before the first column (the column items stretch a little and the spacing on the right is lost)
3. Try to tap a session in the first column to open it

# Observed behavior
- The session won't open anymore.

  https://user-images.githubusercontent.com/144518/178827021-d241026c-cdb8-4488-8b16-1aa9a5dcb584.mp4

# Expected behavior
- The session details are shown.

# Workaround
- Rotate the device to re-render the schedule screen.

# Related
- [Android 12: Behavior changes / Stretch overscroll effect](https://developer.android.com/about/versions/12/behavior-changes-all#overscroll)

Changed files:
app/src/main/java/nerd/tuxmobil/fahrplan/congress/schedule/HorizontalSnapScrollView.kt
app/src/main/java/nerd/tuxmobil/fahrplan/congress/schedule/HorizontalSnapScrollState.kt
app/src/test/java/nerd/tuxmobil/fahrplan/congress/schedule/HorizontalSnapScrollStateTest.kt

diff --git a/app/src/main/java/nerd/tuxmobil/fahrplan/congress/schedule/HorizontalSnapScrollState.kt b/app/src/main/java/nerd/tuxmobil/fahrplan/congress/schedule/HorizontalSnapScrollState.kt
index 9aa81a39..f4a478db 100644
--- a/app/src/main/java/nerd/tuxmobil/fahrplan/congress/schedule/HorizontalSnapScrollState.kt
+++ b/app/src/main/java/nerd/tuxmobil/fahrplan/congress/schedule/HorizontalSnapScrollState.kt
@@ -54,6 +54,7 @@ data class HorizontalSnapScrollState @JvmOverloads constructor(
     /**
      * Returns the column index calculated based on the given [scrollX] value
      * and the current state of [HorizontalSnapScrollState].
+     * @return Returns the calculated index if it is valid, in any other case -1
      */
     fun calculateOnTouchColumnIndex(scrollX: Int): Int {
         val relativeDistance = scrollX - xStart
@@ -74,8 +75,9 @@ data class HorizontalSnapScrollState @JvmOverloads constructor(
 
         columnIndex -= columnIndexDelta
 
-        columnIndex = max(columnIndex, 0)
-        columnIndex = min(columnIndex, roomsCount - displayColumnCount)
+        if (columnIndex < 0 || maxColumnIndex < columnIndex) {
+            return -1
+        }
 
         return columnIndex
     }
diff --git a/app/src/main/java/nerd/tuxmobil/fahrplan/congress/schedule/HorizontalSnapScrollView.kt b/app/src/main/java/nerd/tuxmobil/fahrplan/congress/schedule/HorizontalSnapScrollView.kt
index 60e45c88..683e4dc9 100644
--- a/app/src/main/java/nerd/tuxmobil/fahrplan/congress/schedule/HorizontalSnapScrollView.kt
+++ b/app/src/main/java/nerd/tuxmobil/fahrplan/congress/schedule/HorizontalSnapScrollView.kt
@@ -173,9 +173,13 @@ class HorizontalSnapScrollView(context: Context?, attrs: AttributeSet?) : Horizo
                 return false
             } else if (event.action == ACTION_UP || event.action == ACTION_CANCEL) {
                 val scrollX = event.x.toInt()
-                val columnIndex = horizontalSnapScrollState.calculateOnTouchColumnIndex(scrollX)
-                scrollToColumn(columnIndex, fast = false)
-                return true
+
+                val calculatedColumnIndex = horizontalSnapScrollState.calculateOnTouchColumnIndex(scrollX)
+
+                if (calculatedColumnIndex > -1) {
+                    scrollToColumn(calculatedColumnIndex, fast = false)
+                    return true
+                }
             }
             return false
         }
diff --git a/app/src/test/java/nerd/tuxmobil/fahrplan/congress/schedule/HorizontalSnapScrollStateTest.kt b/app/src/test/java/nerd/tuxmobil/fahrplan/congress/schedule/HorizontalSnapScrollStateTest.kt
index 8d5057ab..8e706f8b 100644
--- a/app/src/test/java/nerd/tuxmobil/fahrplan/congress/schedule/HorizontalSnapScrollStateTest.kt
+++ b/app/src/test/java/nerd/tuxmobil/fahrplan/congress/schedule/HorizontalSnapScrollStateTest.kt
@@ -184,7 +184,7 @@ class HorizontalSnapScrollStateTest {
     }
 
     @Test
-    fun `calculateOnTouchColumnIndex returns 0 as the minimum index in portrait mode to prevent scrolling off the screen to the left`() {
+    fun `calculateOnTouchColumnIndex returns -1 if the target index is lower than 0 in portrait mode to prevent scrolling off the screen to the left`() {
         with(
             createState().copy(
                 columnWidth = 993,
@@ -194,12 +194,12 @@ class HorizontalSnapScrollStateTest {
             )
         ) {
             val scrollThresholdWidth = (columnWidth * SCROLL_THRESHOLD_FACTOR).toInt()
-            assertThat(calculateOnTouchColumnIndex(scrollX = scrollThresholdWidth + 1)).isEqualTo(0)
+            assertThat(calculateOnTouchColumnIndex(scrollX = scrollThresholdWidth + 1)).isEqualTo(-1)
         }
     }
 
     @Test
-    fun `calculateOnTouchColumnIndex returns 0 as the minimum index in landscape mode to prevent scrolling off the screen to the left`() {
+    fun `calculateOnTouchColumnIndex returns -1 if the target index is lower than 0 in landscape mode to prevent scrolling off the screen to the left`() {
         with(
             createState().copy(
                 columnWidth = 993,
@@ -208,27 +208,27 @@ class HorizontalSnapScrollStateTest {
                 roomsCount = 5
             )
         ) {
-            assertThat(calculateOnTouchColumnIndex(scrollX = columnWidth * 2)).isEqualTo(0)
+            assertThat(calculateOnTouchColumnIndex(scrollX = columnWidth * 2)).isEqualTo(-1)
         }
     }
 
     @Test
-    fun `calculateOnTouchColumnIndex returns the last possible index as the maximum value in portrait mode to prevent scrolling off the screen to the right`() {
+    fun `calculateOnTouchColumnIndex returns -1 if the target index is higher than the max allowed in portrait mode to prevent scrolling off the screen to the right`() {
         with(
             createState().copy(
                 columnWidth = 993,
                 displayColumnCount = 1,
-                activeColumnIndex = 4,
+                activeColumnIndex = 9,
                 roomsCount = 10
             )
         ) {
             val scrollThresholdWidth = (columnWidth * SCROLL_THRESHOLD_FACTOR).toInt()
-            assertThat(calculateOnTouchColumnIndex(scrollX = -scrollThresholdWidth + 1)).isEqualTo(4)
+            assertThat(calculateOnTouchColumnIndex(scrollX = -scrollThresholdWidth - 1)).isEqualTo(-1)
         }
     }
 
     @Test
-    fun `calculateOnTouchColumnIndex returns the last possible index as the maximum value in landscape mode to prevent scrolling off the screen to the right`() {
+    fun `calculateOnTouchColumnIndex returns -1 if the target index is higher than the max allowed in landscape mode to prevent scrolling off the screen to the right`() {
         with(
             createState().copy(
                 columnWidth = 993,
@@ -237,7 +237,7 @@ class HorizontalSnapScrollStateTest {
                 roomsCount = 10
             )
         ) {
-            assertThat(calculateOnTouchColumnIndex(scrollX = -columnWidth * 2)).isEqualTo(6)
+            assertThat(calculateOnTouchColumnIndex(scrollX = -columnWidth * 2)).isEqualTo(-1)
         }
     }