Skip to content

fix: non-scrollable ScrollView when blankSpace is big should make it scrollable#1487

Open
kirillzyusko wants to merge 4 commits into
mainfrom
fix/android-non-scrollable-view-when-blank-space-should-make-it-scrollable
Open

fix: non-scrollable ScrollView when blankSpace is big should make it scrollable#1487
kirillzyusko wants to merge 4 commits into
mainfrom
fix/android-non-scrollable-view-when-blank-space-should-make-it-scrollable

Conversation

@kirillzyusko

@kirillzyusko kirillzyusko commented Jun 6, 2026

Copy link
Copy Markdown
Owner

📜 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 ScrollView.onInterceptTouchEvent exits early when it thinks the view cannot scroll down:

if (getScrollY() == 0 && !canScrollVertically(1)) {
  return false;
}

canScrollVertically() is implemented on View and uses:

computeVerticalScrollRange() - computeVerticalScrollExtent()

For ScrollView, computeVerticalScrollRange() is based on the content child bounds:

int scrollRange = getChildAt(0).getBottom();

That does not include paddingBottom.

But the actual scroll/drag range uses getScrollRange():

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:

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:

screen-20260612-205154-1781290300148.mp4

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

📢 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
telegram-cloud-document-2-5301238181667056309.mp4
telegram-cloud-document-2-5301238181667056308.mp4

📝 Checklist

  • CI successfully passed
  • I added new mocks and corresponding unit-tests if library API was changed

@kirillzyusko kirillzyusko self-assigned this Jun 6, 2026
@kirillzyusko kirillzyusko added 🐛 bug Something isn't working 🤖 android Android specific KeyboardChatScrollView 💬 Anything about chat functionality labels Jun 6, 2026
@github-actions

github-actions Bot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

📊 Package size report

Current size Target Size Difference
320623 bytes 318700 bytes 1923 bytes 📈

@kirillzyusko kirillzyusko changed the title fix: non-scrollable ScrollView when blankSpace should make it scrollable fix: non-scrollable ScrollView when blankSpace is big should make it scrollable Jun 8, 2026
@kirillzyusko kirillzyusko marked this pull request as ready for review June 16, 2026 21:37
@github-actions

Copy link
Copy Markdown
Contributor
  1. Integer overflow risk in ClippingScrollViewDecoratorView.kt
    Why: The expression scrollView.height + scrollView.scrollY + MIN_SCROLL_RANGE_PX can exceed integer limits if scrollView.height and scrollY are large, causing incorrect values or negative numbers.
    Fix: Use a data type that prevents overflow or clamp the value within valid bounds.

  2. Undefined behavior in index.tsx
    Why: Using Math.min(layout.value.height, ...) without ensuring layout.value.height is defined could lead to runtime errors if layout.value.height is null/undefined.
    Fix: Ensure layout.value.height is always a valid number before using it in calculations.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🤖 android Android specific 🐛 bug Something isn't working KeyboardChatScrollView 💬 Anything about chat functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant