From c2cbe62a7a7aa935425515ebabcdebc8e42d5b26 Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Tue, 27 Jan 2026 12:36:07 +0100 Subject: [PATCH 1/2] feat: add `ClippingScrollView` component --- .../KeyboardControllerPackage.kt | 2 + .../ClippingScrollViewDecoratorViewManager.kt | 27 +++++++++++++ ...ppingScrollViewDecoratorViewManagerImpl.kt | 13 +++++++ .../views/ClippingScrollViewDecoratorView.kt | 39 +++++++++++++++++++ .../ClippingScrollViewDecoratorViewManager.kt | 24 ++++++++++++ src/bindings.native.ts | 5 +++ src/bindings.ts | 8 ++++ ...gScrollViewDecoratorViewNativeComponent.ts | 17 ++++++++ src/types/views.ts | 6 +++ 9 files changed, 141 insertions(+) create mode 100644 android/src/fabric/java/com/reactnativekeyboardcontroller/ClippingScrollViewDecoratorViewManager.kt create mode 100644 android/src/main/java/com/reactnativekeyboardcontroller/managers/ClippingScrollViewDecoratorViewManagerImpl.kt create mode 100644 android/src/main/java/com/reactnativekeyboardcontroller/views/ClippingScrollViewDecoratorView.kt create mode 100644 android/src/paper/java/com/reactnativekeyboardcontroller/ClippingScrollViewDecoratorViewManager.kt create mode 100644 src/specs/ClippingScrollViewDecoratorViewNativeComponent.ts diff --git a/android/src/base/java/com/reactnativekeyboardcontroller/KeyboardControllerPackage.kt b/android/src/base/java/com/reactnativekeyboardcontroller/KeyboardControllerPackage.kt index b5e9f3811e..22fced3ac3 100644 --- a/android/src/base/java/com/reactnativekeyboardcontroller/KeyboardControllerPackage.kt +++ b/android/src/base/java/com/reactnativekeyboardcontroller/KeyboardControllerPackage.kt @@ -8,6 +8,7 @@ import com.facebook.react.module.model.ReactModuleInfoProvider import com.facebook.react.uimanager.ViewManager import com.reactnativekeyboardcontroller.modules.KeyboardControllerModuleImpl import com.reactnativekeyboardcontroller.modules.statusbar.StatusBarManagerCompatModuleImpl +import java.com.reactnativekeyboardcontroller.ClippingScrollViewDecoratorViewManager class KeyboardControllerPackage : BaseReactPackage() { override fun getModule( @@ -58,5 +59,6 @@ class KeyboardControllerPackage : BaseReactPackage() { KeyboardGestureAreaViewManager(), OverKeyboardViewManager(), KeyboardBackgroundViewManager(), + ClippingScrollViewDecoratorViewManager(), ) } diff --git a/android/src/fabric/java/com/reactnativekeyboardcontroller/ClippingScrollViewDecoratorViewManager.kt b/android/src/fabric/java/com/reactnativekeyboardcontroller/ClippingScrollViewDecoratorViewManager.kt new file mode 100644 index 0000000000..a580f4df02 --- /dev/null +++ b/android/src/fabric/java/com/reactnativekeyboardcontroller/ClippingScrollViewDecoratorViewManager.kt @@ -0,0 +1,27 @@ +package java.com.reactnativekeyboardcontroller + +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.ViewGroupManager +import com.facebook.react.uimanager.ViewManagerDelegate +import com.facebook.react.viewmanagers.ClippingScrollViewDecoratorViewManagerDelegate +import com.facebook.react.viewmanagers.ClippingScrollViewDecoratorViewManagerInterface +import com.reactnativekeyboardcontroller.managers.ClippingScrollViewDecoratorViewManagerImpl +import com.reactnativekeyboardcontroller.views.ClippingScrollViewDecoratorView + +class ClippingScrollViewDecoratorViewManager : + ViewGroupManager(), + ClippingScrollViewDecoratorViewManagerInterface { + private val manager = ClippingScrollViewDecoratorViewManagerImpl() + private val mDelegate = ClippingScrollViewDecoratorViewManagerDelegate(this) + + override fun getDelegate(): ViewManagerDelegate = mDelegate + + override fun getName(): String = ClippingScrollViewDecoratorViewManagerImpl.NAME + + override fun createViewInstance(context: ThemedReactContext): ClippingScrollViewDecoratorView = + manager.createViewInstance(context) + + override fun setContentInsetBottom(view: ClippingScrollViewDecoratorView?, value: Double) { + view?.setContentInsetBottom(value) + } +} diff --git a/android/src/main/java/com/reactnativekeyboardcontroller/managers/ClippingScrollViewDecoratorViewManagerImpl.kt b/android/src/main/java/com/reactnativekeyboardcontroller/managers/ClippingScrollViewDecoratorViewManagerImpl.kt new file mode 100644 index 0000000000..507b744351 --- /dev/null +++ b/android/src/main/java/com/reactnativekeyboardcontroller/managers/ClippingScrollViewDecoratorViewManagerImpl.kt @@ -0,0 +1,13 @@ +package com.reactnativekeyboardcontroller.managers + +import com.facebook.react.uimanager.ThemedReactContext +import com.reactnativekeyboardcontroller.views.ClippingScrollViewDecoratorView + +class ClippingScrollViewDecoratorViewManagerImpl { + fun createViewInstance(reactContext: ThemedReactContext): ClippingScrollViewDecoratorView = + ClippingScrollViewDecoratorView(reactContext) + + companion object { + const val NAME = "ClippingScrollViewDecoratorView" + } +} diff --git a/android/src/main/java/com/reactnativekeyboardcontroller/views/ClippingScrollViewDecoratorView.kt b/android/src/main/java/com/reactnativekeyboardcontroller/views/ClippingScrollViewDecoratorView.kt new file mode 100644 index 0000000000..83ee67d414 --- /dev/null +++ b/android/src/main/java/com/reactnativekeyboardcontroller/views/ClippingScrollViewDecoratorView.kt @@ -0,0 +1,39 @@ +package com.reactnativekeyboardcontroller.views; + +import android.annotation.SuppressLint +import android.widget.ScrollView +import com.facebook.react.uimanager.PixelUtil.dpToPx +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.views.view.ReactViewGroup +import com.reactnativekeyboardcontroller.extensions.dp +import com.reactnativekeyboardcontroller.extensions.px + +@SuppressLint("ViewConstructor") +class ClippingScrollViewDecoratorView( + val reactContext: ThemedReactContext, + ) : ReactViewGroup(reactContext) { + private var insetBottom = 0.0 + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + + decorateScrollView() + } + + fun setContentInsetBottom(value: Double) { + insetBottom = value + decorateScrollView() + } + + private fun decorateScrollView() { + val scrollView = getChildAt(0) as? ScrollView + + scrollView?.clipToPadding = false + scrollView?.setPadding( + scrollView.paddingLeft, + scrollView.paddingTop, + scrollView.paddingRight, + insetBottom.toFloat().px.toInt() + ) + } +} diff --git a/android/src/paper/java/com/reactnativekeyboardcontroller/ClippingScrollViewDecoratorViewManager.kt b/android/src/paper/java/com/reactnativekeyboardcontroller/ClippingScrollViewDecoratorViewManager.kt new file mode 100644 index 0000000000..a02cc22d1b --- /dev/null +++ b/android/src/paper/java/com/reactnativekeyboardcontroller/ClippingScrollViewDecoratorViewManager.kt @@ -0,0 +1,24 @@ +package java.com.reactnativekeyboardcontroller + +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.ViewGroupManager +import com.facebook.react.uimanager.annotations.ReactProp +import com.reactnativekeyboardcontroller.managers.ClippingScrollViewDecoratorViewManagerImpl +import com.reactnativekeyboardcontroller.views.ClippingScrollViewDecoratorView + +class ClippingScrollViewDecoratorViewManager : ViewGroupManager() { + private val manager = ClippingScrollViewDecoratorViewManagerImpl() + + override fun getName(): String = ClippingScrollViewDecoratorViewManagerImpl.NAME + + override fun createViewInstance(reactContext: ThemedReactContext): ClippingScrollViewDecoratorView = + manager.createViewInstance(reactContext) + + @ReactProp(name = "contentInsetBottom") + fun setContentInsetBottom( + view: ClippingScrollViewDecoratorView, + value: Double, + ) { + view.setContentInsetBottom(value) + } +} diff --git a/src/bindings.native.ts b/src/bindings.native.ts index 011478af1e..264bcf4a0c 100644 --- a/src/bindings.native.ts +++ b/src/bindings.native.ts @@ -1,6 +1,7 @@ import { NativeEventEmitter, Platform } from "react-native"; import type { + ClippingScrollViewProps, FocusedInputEventsModule, KeyboardBackgroundViewProps, KeyboardControllerNativeModule, @@ -71,3 +72,7 @@ export const RCTKeyboardExtender: React.FC = Platform.OS === "ios" ? require("./specs/KeyboardExtenderNativeComponent").default : ({ children }: KeyboardExtenderProps) => children; +export const ClippingScrollView: React.FC = + Platform.OS === "android" + ? require("./specs/ClippingScrollViewDecoratorViewNativeComponent").default + : ({ children }: ClippingScrollViewProps) => children; diff --git a/src/bindings.ts b/src/bindings.ts index 2b0c01ce85..535ab53d9c 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -1,6 +1,7 @@ import { View } from "react-native"; import type { + ClippingScrollViewProps, FocusedInputEventsModule, KeyboardBackgroundViewProps, KeyboardControllerNativeModule, @@ -82,3 +83,10 @@ export const KeyboardBackgroundView = */ export const RCTKeyboardExtender = View as unknown as React.FC; +/** + * A decorator that will clip the content of the `ScrollView`. It helps to simulate `contentInset` behavior on Android. + * Supports only `bottom` property (`paddingBottom` is not supported property of `ScrollView.style`). + * Using this component we can modify bottom inset without having a fake view. + */ +export const ClippingScrollView = + View as unknown as React.FC; diff --git a/src/specs/ClippingScrollViewDecoratorViewNativeComponent.ts b/src/specs/ClippingScrollViewDecoratorViewNativeComponent.ts new file mode 100644 index 0000000000..8c2a56fdec --- /dev/null +++ b/src/specs/ClippingScrollViewDecoratorViewNativeComponent.ts @@ -0,0 +1,17 @@ +import { codegenNativeComponent } from "react-native"; + +import type { HostComponent } from "react-native"; +import type { ViewProps } from "react-native"; +import type { Double } from "react-native/Libraries/Types/CodegenTypes"; + +export interface NativeProps extends ViewProps { + contentInsetBottom: Double; +} + +export default codegenNativeComponent( + "ClippingScrollViewDecoratorView", + { + interfaceOnly: true, + excludedPlatforms: ["iOS"], + }, +) as HostComponent; diff --git a/src/types/views.ts b/src/types/views.ts index 6691be0804..ebbca2a35a 100644 --- a/src/types/views.ts +++ b/src/types/views.ts @@ -49,3 +49,9 @@ export type KeyboardExtenderProps = PropsWithChildren<{ /** Controls whether this `KeyboardExtender` instance should take an effect. Default is `true`. */ enabled?: boolean; }>; +export type ClippingScrollViewProps = PropsWithChildren< + ViewProps & { + /** An additional space that gets applied to the bottom of the `ScrollView` (inside a scrollable content). Default is `0`. */ + contentInsetBottom?: number; + } +>; From 8d04f54f882e7f551ade730853907f18da06a6fc Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Wed, 28 Jan 2026 11:27:12 +0100 Subject: [PATCH 2/2] fix: ktlint --- .../ClippingScrollViewDecoratorViewManager.kt | 5 ++++- .../views/ClippingScrollViewDecoratorView.kt | 8 +++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/android/src/fabric/java/com/reactnativekeyboardcontroller/ClippingScrollViewDecoratorViewManager.kt b/android/src/fabric/java/com/reactnativekeyboardcontroller/ClippingScrollViewDecoratorViewManager.kt index a580f4df02..9992d906ac 100644 --- a/android/src/fabric/java/com/reactnativekeyboardcontroller/ClippingScrollViewDecoratorViewManager.kt +++ b/android/src/fabric/java/com/reactnativekeyboardcontroller/ClippingScrollViewDecoratorViewManager.kt @@ -21,7 +21,10 @@ class ClippingScrollViewDecoratorViewManager : override fun createViewInstance(context: ThemedReactContext): ClippingScrollViewDecoratorView = manager.createViewInstance(context) - override fun setContentInsetBottom(view: ClippingScrollViewDecoratorView?, value: Double) { + override fun setContentInsetBottom( + view: ClippingScrollViewDecoratorView?, + value: Double, + ) { view?.setContentInsetBottom(value) } } diff --git a/android/src/main/java/com/reactnativekeyboardcontroller/views/ClippingScrollViewDecoratorView.kt b/android/src/main/java/com/reactnativekeyboardcontroller/views/ClippingScrollViewDecoratorView.kt index 83ee67d414..051a1e1da9 100644 --- a/android/src/main/java/com/reactnativekeyboardcontroller/views/ClippingScrollViewDecoratorView.kt +++ b/android/src/main/java/com/reactnativekeyboardcontroller/views/ClippingScrollViewDecoratorView.kt @@ -1,17 +1,15 @@ -package com.reactnativekeyboardcontroller.views; +package com.reactnativekeyboardcontroller.views import android.annotation.SuppressLint import android.widget.ScrollView -import com.facebook.react.uimanager.PixelUtil.dpToPx import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.views.view.ReactViewGroup -import com.reactnativekeyboardcontroller.extensions.dp import com.reactnativekeyboardcontroller.extensions.px @SuppressLint("ViewConstructor") class ClippingScrollViewDecoratorView( val reactContext: ThemedReactContext, - ) : ReactViewGroup(reactContext) { +) : ReactViewGroup(reactContext) { private var insetBottom = 0.0 override fun onAttachedToWindow() { @@ -33,7 +31,7 @@ class ClippingScrollViewDecoratorView( scrollView.paddingLeft, scrollView.paddingTop, scrollView.paddingRight, - insetBottom.toFloat().px.toInt() + insetBottom.toFloat().px.toInt(), ) } }