Commit a2f7e7e
authored
## 📜 Description
This fixes an Android-only `KeyboardChatScrollView` edge case where
short content can become scrollable only because of `blankSpace`, but a
scroll gesture that starts on the content is not intercepted by the
`ScrollView`.
## 💡 Motivation and Context
The root cause is not Fabric and not React Native “overriding touch
events” in the usual sense. The issue comes from Android `ScrollView`
having two different scrollability calculations when padding is used as
synthetic scrollable space.
[AOSP
ScrollView.java](https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/core/java/android/widget/ScrollView.java#581)
`ScrollView.onInterceptTouchEvent` exits early when it thinks the view
cannot scroll down:
```java
if (getScrollY() == 0 && !canScrollVertically(1)) {
return false;
}
```
`canScrollVertically()` is implemented on
[View](https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/core/java/android/view/View.java#22167)
and uses:
```java
computeVerticalScrollRange() - computeVerticalScrollExtent()
```
For
[ScrollView](https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/core/java/android/widget/ScrollView.java#1338),
`computeVerticalScrollRange()` is based on the content child bounds:
```java
int scrollRange = getChildAt(0).getBottom();
```
That does not include `paddingBottom`.
But the actual scroll/drag range uses
[`getScrollRange()`](https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/core/java/android/widget/ScrollView.java#1036):
```java
child.getHeight() - (getHeight() - mPaddingBottom - mPaddingTop)
```
That does include padding, because padding reduces the viewport height.
So with short content plus padding-backed `blankSpace`, Android can get
into this contradictory state:
```text
canScrollVertically(1) -> false
actual getScrollRange() -> positive
```
React Native is not the primary source of the bug. `ReactScrollView`
adds RN event/state handling, but still delegates interception and touch
handling to AOSP `ScrollView`. The package triggers the Android edge
case because `KeyboardChatScrollView` uses native padding to model an
iOS-like `contentInset`/`blankSpace`.
There are a few possible ways to fix this:
1. Patch Android or React Native `ScrollView` so `canScrollVertically()`
and the actual scroll range agree when padding is used as scrollable
inset. This would be the most fundamental fix, but it is outside this
package.
2. Own a custom Android `ReactScrollView` subclass and override the
relevant scrollability/range methods. This is more correct in isolation,
but it is a large maintenance burden because we would need to preserve
React Native ScrollView props, events, commands, refs, Paper/Fabric
behavior, and Reanimated compatibility.
3. Represent `blankSpace` as real content layout, for example with a
spacer/footer or `contentContainerStyle.paddingBottom`. This makes
Android’s range calculations agree naturally, but it changes content
layout semantics and can interfere with virtualized lists (it changes
layout, JS update may overwrite it etc., not really "safe" way of doing
this)
4. Keep the current `ClippingScrollView` approach and avoid pathological
blank ranges.
This PR chooses option 4. The native workaround fixes the short-content
gesture issue without forking React Native’s ScrollView or changing user
content layout. On top of that, we clamp `blankSpace` in JS so it cannot
exceed one ScrollView viewport. Otherwise we have this bug:
https://github.com/user-attachments/assets/5311381b-76ad-4f0e-95b5-f235741ab754
The JS clamp is needed because values larger than the viewport create a
scroll range made mostly or entirely of empty space. That oversized
empty range is not useful for the chat use case and can expose Android
momentum/range desynchronization during fast gestures. One viewport of
`blankSpace` is enough to make short content scrollable and position it
correctly; larger values only allow scrolling to a fully blank screen:
#1497
Closes
#1497
#1453
#1455
## 📢 Changelog
<!-- High level overview of important changes -->
<!-- For example: fixed status bar manipulation; added new types
declarations; -->
<!-- If your changes don't affect one of platform/language below - then
remove this platform/language -->
## 📢 Changelog
### JS
- clamp `KeyboardChatScrollView` `blankSpace` to the ScrollView viewport
height;
- prevent oversized blank scroll ranges that can cause Android momentum
flicker;
### Android
- adjust `ClippingScrollViewDecoratorView` touch handling to allow short
content with padding-backed `blankSpace` to start scrolling from the
content area;
- keep the temporary content range expansion scoped to the touch
dispatch path and restore it immediately after dispatch;
## 🤔 How Has This Been Tested?
Tested manually on Pixel 7 Pro (API 36).
## 📸 Screenshots (if appropriate):
|Before|After|
|-------|-----|
|<video
src="https://github.com/user-attachments/assets/2c9aabdd-7252-4d23-95a2-9de944ecdafa">|<video
src="https://github.com/user-attachments/assets/8e64eefd-55a8-49ce-8f53-ac32d6b36598">|
## 📝 Checklist
- [x] CI successfully passed
- [x] I added new mocks and corresponding unit-tests if library API was
changed
1 parent cac5e2d commit a2f7e7e
2 files changed
Lines changed: 106 additions & 1 deletion
File tree
- android/src/main/java/com/reactnativekeyboardcontroller/views
- src/components/KeyboardChatScrollView
Lines changed: 95 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
| 4 | + | |
4 | 5 | | |
5 | 6 | | |
6 | 7 | | |
7 | 8 | | |
8 | 9 | | |
9 | 10 | | |
| 11 | + | |
10 | 12 | | |
11 | 13 | | |
12 | 14 | | |
| |||
15 | 17 | | |
16 | 18 | | |
17 | 19 | | |
| 20 | + | |
18 | 21 | | |
19 | 22 | | |
20 | 23 | | |
21 | 24 | | |
22 | 25 | | |
23 | 26 | | |
24 | 27 | | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
25 | 56 | | |
26 | 57 | | |
27 | 58 | | |
| |||
68 | 99 | | |
69 | 100 | | |
70 | 101 | | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
71 | 161 | | |
72 | 162 | | |
73 | 163 | | |
| |||
83 | 173 | | |
84 | 174 | | |
85 | 175 | | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
86 | 181 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
88 | 88 | | |
89 | 89 | | |
90 | 90 | | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
91 | 98 | | |
92 | | - | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
93 | 103 | | |
94 | 104 | | |
95 | 105 | | |
| |||
0 commit comments