Skip to content

fix(android): resolve enclosing scroll view when useNestedScrollViewAndroid is enabled - #1537

Open
debitan wants to merge 1 commit into
kirillzyusko:mainfrom
debitan:fix/android-nested-scroll-view-parent
Open

fix(android): resolve enclosing scroll view when useNestedScrollViewAndroid is enabled#1537
debitan wants to merge 1 commit into
kirillzyusko:mainfrom
debitan:fix/android-nested-scroll-view-parent

Conversation

@debitan

@debitan debitan commented Jul 9, 2026

Copy link
Copy Markdown

📜 Description

EditText.parentScrollViewTarget only matches ReactScrollView, but when the useNestedScrollViewAndroid feature flag is enabled RN renders the vertical scroll view as ReactNestedScrollView. The walk therefore never finds the enclosing scroll view and returns -1, so KeyboardAwareScrollView's maybeScroll bails on the parentScrollViewTarget !== scrollViewTarget guard and the focused input is never scrolled above the keyboard.

ReactNestedScrollView is package-private, so it cannot be referenced by type from outside com.facebook.react.views.scroll. This matches its public superclass androidx.core.widget.NestedScrollView instead (which ReactNestedScrollView extends).

💡 Motivation and Context

Fixes the Android auto-scroll failure discussed in #1411 for apps that enable useNestedScrollViewAndroid (commonly turned on for nested scrolling inside native bottom sheets). With the flag on, KeyboardAwareScrollView stopped scrolling focused inputs into view on Android.

📢 Changelog

Android

  • Also match NestedScrollView (the public superclass of ReactNestedScrollView) in parentScrollViewTarget, so auto-scroll works when the useNestedScrollViewAndroid feature flag is enabled.

🤔 How Has This Been Tested?

Tested in a React Native 0.85 (New Architecture) app on a physical Pixel and an emulator with useNestedScrollViewAndroid enabled. Before: inputs near the bottom of a KeyboardAwareScrollView were covered by the keyboard, and parentScrollViewTarget logged as -1. After: focused inputs scroll above the keyboard as expected. iOS is unaffected.

📝 Checklist

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

Comment on lines +109 to +111
if ((parentView is ReactScrollView && parentView.scrollEnabled) ||
parentView is NestedScrollView
) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I like these changes, we miss one important thing - if ScrollView will be with disabled scroll then this code still return it as valid scrollable container.

Do you think we can add extension that can handle both cases?

import android.view.View
import com.facebook.react.views.scroll.ReactScrollView
import java.lang.reflect.Field
import java.lang.reflect.Method

internal fun View.isEnabledReactScrollView(): Boolean {
  if (this is ReactScrollView) {
    return scrollEnabled
  }

  if (ReactNestedScrollViewCompat.isInstance(this)) {
    return ReactNestedScrollViewCompat.isScrollEnabled(this)
  }

  return false
}

private object ReactNestedScrollViewCompat {
  private const val CLASS_NAME =
    "com.facebook.react.views.scroll.ReactNestedScrollView"

  private val clazz: Class<*>? by lazy(LazyThreadSafetyMode.PUBLICATION) {
    runCatching {
      Class.forName(
        CLASS_NAME,
        false,
        ReactScrollView::class.java.classLoader,
      )
    }.getOrNull()
  }

  private val scrollEnabledGetter: Method? by lazy(LazyThreadSafetyMode.PUBLICATION) {
    val c = clazz ?: return@lazy null

    runCatching {
      c.getMethod("getScrollEnabled")
    }.getOrNull()
      ?: runCatching {
        c.getMethod("isScrollEnabled")
      }.getOrNull()
  }

  private val scrollEnabledField: Field? by lazy(LazyThreadSafetyMode.PUBLICATION) {
    val c = clazz ?: return@lazy null

    runCatching {
      c.getDeclaredField("mScrollEnabled").apply {
        isAccessible = true
      }
    }.getOrNull()
  }

  fun isInstance(view: View): Boolean {
    return clazz?.isInstance(view) == true
  }

  fun isScrollEnabled(view: View): Boolean {
    scrollEnabledGetter?.let { getter ->
      return runCatching {
        getter.invoke(view) as Boolean
      }.getOrDefault(false)
    }

    scrollEnabledField?.let { field ->
      return runCatching {
        field.getBoolean(view)
      }.getOrDefault(false)
    }

    return false
  }
}

And use it like:

if (parentView.isEnabledReactScrollView()) {

I'm fine if you can simpler/better code. My main concern is that we need to check scrollEnabled property too.

Ideally would be also to check that it returns false with disabled ScrollView + check any possible performance impact. The reflection is an expensive thing and calling it for each view might be expensive 🤞

Let me know what do you think about it!

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

📊 Package size report

Current size Target Size Difference
332780 bytes 332775 bytes 5 bytes 📈

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 focused input 📝 Anything about focused input functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants