Skip to content

Commit 36abe0b

Browse files
authored
feat: add ClippingScrollView component (#1289)
## 📜 Description Added `ClippingScrollView` component (on Android only) that is supposed to act as a polyfill for `contentInset: {bottom}` property of `ScrollView`. ## 💡 Motivation and Context Those changes are based on my PR from facebook/react-native#49145 The big problem with original PR is that it wasn't working correctly, if we specify all insets/paddings simultaneously. But! It worked well for `bottom` inset property (keyboard-controller case, because keyboard appears from the bottom of the screen). I think it's risky to ship a code in this state into facebook codebase, so I decided to add those changes into `react-native-keyboard-controller` first. It's good because: - I have a full ownership of the code and I can fix bugs quickly (without waiting for a new RN release); - we don't ship buggy code in react-native repository; - we don't depend on react-native version and we don't need to write a conditional code, like "if prop is supported, then use new approach and if not, then fallback to old implementation". The approach that I've choose is based on "decorator" approach - this approach has been used in many other libs, such as `advanced-input-mask`, `live-markdown` etc. The idea is that we create our custom view that wraps our target view and then we access a target view as a children (and we can modify behavior/props of this view). I'm going to use this component in `KeyboardAwareScrollView` and in new `ChatKit` component. From lessons learned from the previous experience I can confidently say, that additional view near children will cause issues and this polyfill for `contentInset: {bottom}` should hopefully solve all the issues that we had. I'll continue experiments with this view in #797 ## 📢 Changelog <!-- High level overview of important changes --> <!-- For example: fixed status bar manipulation; added new types declarations; --> <!-- If your changes don't affect one of platform/language below - then remove this platform/language --> ### JS - added codegen component; - added new types props for new component; - added jsdoc for new component; ### Android - added `ClippingScrollView`; ## 🤔 How Has This Been Tested? Tested in example app in `KeyboardAwareScrollView` screen and in new component that is currently under active development 😊 Everywhere works stable on both architectures 🤞 ## 📸 Screenshots (if appropriate): |KeyboardAwareScollView|Non inverted chat list| |--------------------------|---------------------| |<video src="https://github.com/user-attachments/assets/0f7efef7-ec15-4fe6-96cb-40334f0c91ad">|<video src="https://github.com/user-attachments/assets/92093e87-434a-4c82-91d5-1c7e9706e5f0">| ## 📝 Checklist - [x] CI successfully passed - [x] I added new mocks and corresponding unit-tests if library API was changed
1 parent 5180338 commit 36abe0b

9 files changed

Lines changed: 142 additions & 0 deletions

File tree

android/src/base/java/com/reactnativekeyboardcontroller/KeyboardControllerPackage.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import com.facebook.react.module.model.ReactModuleInfoProvider
88
import com.facebook.react.uimanager.ViewManager
99
import com.reactnativekeyboardcontroller.modules.KeyboardControllerModuleImpl
1010
import com.reactnativekeyboardcontroller.modules.statusbar.StatusBarManagerCompatModuleImpl
11+
import java.com.reactnativekeyboardcontroller.ClippingScrollViewDecoratorViewManager
1112

1213
class KeyboardControllerPackage : BaseReactPackage() {
1314
override fun getModule(
@@ -58,5 +59,6 @@ class KeyboardControllerPackage : BaseReactPackage() {
5859
KeyboardGestureAreaViewManager(),
5960
OverKeyboardViewManager(),
6061
KeyboardBackgroundViewManager(),
62+
ClippingScrollViewDecoratorViewManager(),
6163
)
6264
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package java.com.reactnativekeyboardcontroller
2+
3+
import com.facebook.react.uimanager.ThemedReactContext
4+
import com.facebook.react.uimanager.ViewGroupManager
5+
import com.facebook.react.uimanager.ViewManagerDelegate
6+
import com.facebook.react.viewmanagers.ClippingScrollViewDecoratorViewManagerDelegate
7+
import com.facebook.react.viewmanagers.ClippingScrollViewDecoratorViewManagerInterface
8+
import com.reactnativekeyboardcontroller.managers.ClippingScrollViewDecoratorViewManagerImpl
9+
import com.reactnativekeyboardcontroller.views.ClippingScrollViewDecoratorView
10+
11+
class ClippingScrollViewDecoratorViewManager :
12+
ViewGroupManager<ClippingScrollViewDecoratorView>(),
13+
ClippingScrollViewDecoratorViewManagerInterface<ClippingScrollViewDecoratorView> {
14+
private val manager = ClippingScrollViewDecoratorViewManagerImpl()
15+
private val mDelegate = ClippingScrollViewDecoratorViewManagerDelegate(this)
16+
17+
override fun getDelegate(): ViewManagerDelegate<ClippingScrollViewDecoratorView> = mDelegate
18+
19+
override fun getName(): String = ClippingScrollViewDecoratorViewManagerImpl.NAME
20+
21+
override fun createViewInstance(context: ThemedReactContext): ClippingScrollViewDecoratorView =
22+
manager.createViewInstance(context)
23+
24+
override fun setContentInsetBottom(
25+
view: ClippingScrollViewDecoratorView?,
26+
value: Double,
27+
) {
28+
view?.setContentInsetBottom(value)
29+
}
30+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.reactnativekeyboardcontroller.managers
2+
3+
import com.facebook.react.uimanager.ThemedReactContext
4+
import com.reactnativekeyboardcontroller.views.ClippingScrollViewDecoratorView
5+
6+
class ClippingScrollViewDecoratorViewManagerImpl {
7+
fun createViewInstance(reactContext: ThemedReactContext): ClippingScrollViewDecoratorView =
8+
ClippingScrollViewDecoratorView(reactContext)
9+
10+
companion object {
11+
const val NAME = "ClippingScrollViewDecoratorView"
12+
}
13+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.reactnativekeyboardcontroller.views
2+
3+
import android.annotation.SuppressLint
4+
import android.widget.ScrollView
5+
import com.facebook.react.uimanager.ThemedReactContext
6+
import com.facebook.react.views.view.ReactViewGroup
7+
import com.reactnativekeyboardcontroller.extensions.px
8+
9+
@SuppressLint("ViewConstructor")
10+
class ClippingScrollViewDecoratorView(
11+
val reactContext: ThemedReactContext,
12+
) : ReactViewGroup(reactContext) {
13+
private var insetBottom = 0.0
14+
15+
override fun onAttachedToWindow() {
16+
super.onAttachedToWindow()
17+
18+
decorateScrollView()
19+
}
20+
21+
fun setContentInsetBottom(value: Double) {
22+
insetBottom = value
23+
decorateScrollView()
24+
}
25+
26+
private fun decorateScrollView() {
27+
val scrollView = getChildAt(0) as? ScrollView
28+
29+
scrollView?.clipToPadding = false
30+
scrollView?.setPadding(
31+
scrollView.paddingLeft,
32+
scrollView.paddingTop,
33+
scrollView.paddingRight,
34+
insetBottom.toFloat().px.toInt(),
35+
)
36+
}
37+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package java.com.reactnativekeyboardcontroller
2+
3+
import com.facebook.react.uimanager.ThemedReactContext
4+
import com.facebook.react.uimanager.ViewGroupManager
5+
import com.facebook.react.uimanager.annotations.ReactProp
6+
import com.reactnativekeyboardcontroller.managers.ClippingScrollViewDecoratorViewManagerImpl
7+
import com.reactnativekeyboardcontroller.views.ClippingScrollViewDecoratorView
8+
9+
class ClippingScrollViewDecoratorViewManager : ViewGroupManager<ClippingScrollViewDecoratorView>() {
10+
private val manager = ClippingScrollViewDecoratorViewManagerImpl()
11+
12+
override fun getName(): String = ClippingScrollViewDecoratorViewManagerImpl.NAME
13+
14+
override fun createViewInstance(reactContext: ThemedReactContext): ClippingScrollViewDecoratorView =
15+
manager.createViewInstance(reactContext)
16+
17+
@ReactProp(name = "contentInsetBottom")
18+
fun setContentInsetBottom(
19+
view: ClippingScrollViewDecoratorView,
20+
value: Double,
21+
) {
22+
view.setContentInsetBottom(value)
23+
}
24+
}

src/bindings.native.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { NativeEventEmitter, Platform } from "react-native";
22

33
import type {
4+
ClippingScrollViewProps,
45
FocusedInputEventsModule,
56
KeyboardBackgroundViewProps,
67
KeyboardControllerNativeModule,
@@ -71,3 +72,7 @@ export const RCTKeyboardExtender: React.FC<KeyboardExtenderProps> =
7172
Platform.OS === "ios"
7273
? require("./specs/KeyboardExtenderNativeComponent").default
7374
: ({ children }: KeyboardExtenderProps) => children;
75+
export const ClippingScrollView: React.FC<KeyboardBackgroundViewProps> =
76+
Platform.OS === "android"
77+
? require("./specs/ClippingScrollViewDecoratorViewNativeComponent").default
78+
: ({ children }: ClippingScrollViewProps) => children;

src/bindings.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { View } from "react-native";
22

33
import type {
4+
ClippingScrollViewProps,
45
FocusedInputEventsModule,
56
KeyboardBackgroundViewProps,
67
KeyboardControllerNativeModule,
@@ -82,3 +83,10 @@ export const KeyboardBackgroundView =
8283
*/
8384
export const RCTKeyboardExtender =
8485
View as unknown as React.FC<KeyboardExtenderProps>;
86+
/**
87+
* A decorator that will clip the content of the `ScrollView`. It helps to simulate `contentInset` behavior on Android.
88+
* Supports only `bottom` property (`paddingBottom` is not supported property of `ScrollView.style`).
89+
* Using this component we can modify bottom inset without having a fake view.
90+
*/
91+
export const ClippingScrollView =
92+
View as unknown as React.FC<ClippingScrollViewProps>;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { codegenNativeComponent } from "react-native";
2+
3+
import type { HostComponent } from "react-native";
4+
import type { ViewProps } from "react-native";
5+
import type { Double } from "react-native/Libraries/Types/CodegenTypes";
6+
7+
export interface NativeProps extends ViewProps {
8+
contentInsetBottom: Double;
9+
}
10+
11+
export default codegenNativeComponent<NativeProps>(
12+
"ClippingScrollViewDecoratorView",
13+
{
14+
interfaceOnly: true,
15+
excludedPlatforms: ["iOS"],
16+
},
17+
) as HostComponent<NativeProps>;

src/types/views.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,9 @@ export type KeyboardExtenderProps = PropsWithChildren<{
4949
/** Controls whether this `KeyboardExtender` instance should take an effect. Default is `true`. */
5050
enabled?: boolean;
5151
}>;
52+
export type ClippingScrollViewProps = PropsWithChildren<
53+
ViewProps & {
54+
/** An additional space that gets applied to the bottom of the `ScrollView` (inside a scrollable content). Default is `0`. */
55+
contentInsetBottom?: number;
56+
}
57+
>;

0 commit comments

Comments
 (0)