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..9992d906ac --- /dev/null +++ b/android/src/fabric/java/com/reactnativekeyboardcontroller/ClippingScrollViewDecoratorViewManager.kt @@ -0,0 +1,30 @@ +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..051a1e1da9 --- /dev/null +++ b/android/src/main/java/com/reactnativekeyboardcontroller/views/ClippingScrollViewDecoratorView.kt @@ -0,0 +1,37 @@ +package com.reactnativekeyboardcontroller.views + +import android.annotation.SuppressLint +import android.widget.ScrollView +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.views.view.ReactViewGroup +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; + } +>;