diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactNestedScrollView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactNestedScrollView.kt index b284e2437b9..e1f4ab55996 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactNestedScrollView.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactNestedScrollView.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<497c1f334f6b3325e032e09947f6fce4>> + * @generated SignedSource<> */ /** @@ -706,21 +706,6 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) : if (pagingEnabled) { flingAndSnap(correctedVelocityY) - } else if (scroller != null) { - val scrollWindowHeight = height - paddingBottom - paddingTop - scroller.fling( - scrollX, // startX - scrollY, // startY - 0, // velocityX - correctedVelocityY, // velocityY - 0, // minX - 0, // maxX - 0, // minY - Int.MAX_VALUE, // maxY - 0, // overX - scrollWindowHeight / 2, // overY - ) - postInvalidateOnAnimation() } else { super.fling(correctedVelocityY) } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/generate-nested-scroll-view.js b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/generate-nested-scroll-view.js index c16283509f5..119b03d6efc 100755 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/generate-nested-scroll-view.js +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/generate-nested-scroll-view.js @@ -100,6 +100,46 @@ function replaceCopyrightHeader(content, sourceFile) { return content.replace(COPYRIGHT_HEADER_PATTERN, generatedHeader(sourceFile)); } +/** + * Keep NestedScrollView in charge of the non-paging fling lifecycle. + * + * ReactScrollView intentionally drives its reflected OverScroller directly. For + * the generated AndroidX variant, doing the same bypasses NestedScrollView.fling() + * and therefore its TYPE_NON_TOUCH nested-scroll lifecycle. + */ +function replaceNestedScrollViewFling(content) { + const scrollerFlingBranch = ` } else if (scroller != null) { + val scrollWindowHeight = height - paddingBottom - paddingTop + scroller.fling( + scrollX, // startX + scrollY, // startY + 0, // velocityX + correctedVelocityY, // velocityY + 0, // minX + 0, // maxX + 0, // minY + Int.MAX_VALUE, // maxY + 0, // overX + scrollWindowHeight / 2, // overY + ) + postInvalidateOnAnimation() + } else { + super.fling(correctedVelocityY) + }`; + const nestedScrollFlingBranch = ` } else { + super.fling(correctedVelocityY) + }`; + const occurrenceCount = content.split(scrollerFlingBranch).length - 1; + + if (occurrenceCount !== 1) { + throw new Error( + `Expected exactly one ReactScrollView fling scroller branch; found ${occurrenceCount}.`, + ); + } + + return content.replace(scrollerFlingBranch, nestedScrollFlingBranch); +} + /** * Transform ReactScrollView.kt to ReactNestedScrollView.kt */ @@ -121,6 +161,10 @@ function transformScrollView(content) { // Replace ReactScrollView with ReactNestedScrollView content = replaceClassNames(content); + // Unlike android.widget.ScrollView, AndroidX NestedScrollView owns the nested-scroll + // lifecycle of a fling. Preserve that lifecycle in the generated variant. + content = replaceNestedScrollViewFling(content); + // Make the class internal to keep it out of the public API content = content.replace( 'public open class ReactNestedScrollView',