Skip to content

Commit 1a2d326

Browse files
committed
experimental: enable polyfill for inverted mode (compensate with translateY + scrollTo)
1 parent feb3221 commit 1a2d326

10 files changed

Lines changed: 74 additions & 34 deletions

File tree

FabricExample/src/screens/Examples/KeyboardChatScrollView/config.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ function ConfigSheet() {
136136
testID="bottom_sheet_next_keyboard_lift_behavior"
137137
onPress={nextLiftBehavior}
138138
>
139-
<Text>{keyboardLiftBehavior}</Text>
139+
<Text style={styles.text}>{keyboardLiftBehavior}</Text>
140140
</TouchableOpacity>
141141
</View>
142142
</BottomSheetView>

android/src/fabric/java/com/reactnativekeyboardcontroller/ClippingScrollViewDecoratorViewManager.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,11 @@ class ClippingScrollViewDecoratorViewManager :
2727
) {
2828
view?.setContentInsetBottom(value)
2929
}
30+
31+
override fun setContentInsetTop(
32+
view: ClippingScrollViewDecoratorView?,
33+
value: Double,
34+
) {
35+
view?.setContentInsetTop(value)
36+
}
3037
}

android/src/main/java/com/reactnativekeyboardcontroller/views/ClippingScrollViewDecoratorView.kt

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.reactnativekeyboardcontroller.views
22

33
import android.annotation.SuppressLint
4+
import android.view.ViewGroup
45
import android.widget.ScrollView
56
import com.facebook.react.uimanager.ThemedReactContext
67
import com.facebook.react.views.view.ReactViewGroup
@@ -11,6 +12,8 @@ class ClippingScrollViewDecoratorView(
1112
val reactContext: ThemedReactContext,
1213
) : ReactViewGroup(reactContext) {
1314
private var insetBottom = 0.0
15+
private var insetTop = 0.0
16+
private var appliedTopInsetPx = 0
1417

1518
override fun onAttachedToWindow() {
1619
super.onAttachedToWindow()
@@ -23,15 +26,39 @@ class ClippingScrollViewDecoratorView(
2326
decorateScrollView()
2427
}
2528

29+
fun setContentInsetTop(value: Double) {
30+
insetTop = value
31+
decorateScrollView()
32+
}
33+
2634
private fun decorateScrollView() {
2735
val scrollView = getChildAt(0) as? ScrollView ?: return
2836

2937
scrollView.clipToPadding = false
38+
39+
val newTopInsetPx = insetTop.toFloat().px.toInt()
40+
41+
// Translate the content view as a whole — this keeps FlatList's
42+
// virtualizer calculations correct (it reads layout positions,
43+
// not translationY).
44+
val contentView = scrollView.getChildAt(0) as? ViewGroup ?: return
45+
(contentView.getChildAt(0) as? ViewGroup)?.translationY = newTopInsetPx.toFloat()
46+
3047
scrollView.setPadding(
3148
scrollView.paddingLeft,
3249
scrollView.paddingTop,
3350
scrollView.paddingRight,
34-
insetBottom.toFloat().px.toInt(),
51+
// pass accumulated value — visually both top and bottom insets
52+
// extend the scroll range via bottom padding
53+
(insetBottom + insetTop).toFloat().px.toInt(),
3554
)
55+
56+
// scroll by the delta to keep content visually in place
57+
val delta = newTopInsetPx - appliedTopInsetPx
58+
if (delta != 0) {
59+
scrollView.scrollBy(0, delta)
60+
}
61+
62+
appliedTopInsetPx = newTopInsetPx
3663
}
3764
}

android/src/paper/java/com/reactnativekeyboardcontroller/ClippingScrollViewDecoratorViewManager.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,12 @@ class ClippingScrollViewDecoratorViewManager : ViewGroupManager<ClippingScrollVi
2121
) {
2222
view.setContentInsetBottom(value)
2323
}
24+
25+
@ReactProp(name = "contentInsetTop")
26+
fun setContentInsetTop(
27+
view: ClippingScrollViewDecoratorView,
28+
value: Double,
29+
) {
30+
view.setContentInsetTop(value)
31+
}
2432
}

example/src/screens/Examples/KeyboardChatScrollView/config.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ function ConfigSheet() {
136136
testID="bottom_sheet_next_keyboard_lift_behavior"
137137
onPress={nextLiftBehavior}
138138
>
139-
<Text>{keyboardLiftBehavior}</Text>
139+
<Text style={styles.text}>{keyboardLiftBehavior}</Text>
140140
</TouchableOpacity>
141141
</View>
142142
</BottomSheetView>

src/components/KeyboardChatScrollView/index.tsx

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React, { forwardRef } from "react";
2-
import { useAnimatedRef, useAnimatedStyle } from "react-native-reanimated";
2+
import { View } from "react-native";
3+
import { useAnimatedRef } from "react-native-reanimated";
34
import Reanimated from "react-native-reanimated";
45

56
import useCombinedRef from "../hooks/useCombinedRef";
@@ -28,29 +29,33 @@ const KeyboardChatScrollView = forwardRef<
2829
const scrollViewRef = useAnimatedRef<Reanimated.ScrollView>();
2930
const onRef = useCombinedRef(ref, scrollViewRef);
3031

31-
const { padding, contentOffsetY, containerTranslateY } = useChatKeyboard(
32-
scrollViewRef,
33-
{ inverted, keyboardLiftBehavior, freeze, offset },
34-
);
35-
36-
const containerStyle = useAnimatedStyle(
37-
() => ({
38-
transform: [{ translateY: containerTranslateY.value }],
39-
}),
40-
[],
41-
);
32+
const { padding, contentOffsetY } = useChatKeyboard(scrollViewRef, {
33+
inverted,
34+
keyboardLiftBehavior,
35+
freeze,
36+
offset,
37+
});
4238

4339
return (
4440
<ScrollViewWithBottomPadding
4541
ref={onRef}
4642
{...rest}
4743
bottomPadding={padding}
48-
containerStyle={containerStyle}
4944
contentOffsetY={contentOffsetY}
5045
inverted={inverted}
5146
ScrollViewComponent={ScrollViewComponent}
5247
>
53-
{children}
48+
{inverted ? (
49+
// The only thing it can break is `StickyHeader`, but it's already broken in FlatList and other lists
50+
// don't support this functionality, so we can add additional view here
51+
// The correct fix would be to add a new prop in ScrollView that allows
52+
// to customize children extraction logic and skip custom view
53+
<View collapsable={false} nativeID="container">
54+
{children}
55+
</View>
56+
) : (
57+
children
58+
)}
5459
</ScrollViewWithBottomPadding>
5560
);
5661
},

src/components/KeyboardChatScrollView/useChatKeyboard/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ type UseChatKeyboardOptions = {
2626
};
2727

2828
type UseChatKeyboardReturn = {
29-
/** Extra scrollable space (= keyboard height). Used as contentInset on iOS, contentInsetBottom on Android. */
29+
/** Extra scrollable space (= keyboard height). Used as contentInset on iOS, contentInsetBottom/contentInsetTop on Android. */
3030
padding: SharedValue<number>;
3131
/** Absolute Y content offset for iOS (set once in onStart). `undefined` on Android. */
3232
contentOffsetY: SharedValue<number> | undefined;

src/components/ScrollViewWithBottomPadding/index.tsx

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { ClippingScrollView } from "../../bindings";
66

77
import styles from "./styles";
88

9-
import type { ScrollViewProps, StyleProp, ViewStyle } from "react-native";
9+
import type { ScrollViewProps } from "react-native";
1010
import type { SharedValue } from "react-native-reanimated";
1111

1212
const OS = Platform.OS;
@@ -30,8 +30,6 @@ type ScrollViewWithBottomPaddingProps = {
3030
bottomPadding: SharedValue<number>;
3131
/** Absolute Y content offset (iOS only, for KeyboardChatScrollView). */
3232
contentOffsetY?: SharedValue<number>;
33-
/** Style applied to the container wrapper (Android only, for KeyboardChatScrollView translateY). */
34-
containerStyle?: StyleProp<ViewStyle>;
3533
} & ScrollViewProps;
3634

3735
const ScrollViewWithBottomPadding = forwardRef<
@@ -46,9 +44,7 @@ const ScrollViewWithBottomPadding = forwardRef<
4644
scrollIndicatorInsets,
4745
inverted,
4846
contentOffsetY,
49-
containerStyle,
5047
children,
51-
style,
5248
...rest
5349
},
5450
ref,
@@ -75,8 +71,10 @@ const ScrollViewWithBottomPadding = forwardRef<
7571
right: scrollIndicatorInsets?.right,
7672
left: scrollIndicatorInsets?.left,
7773
},
74+
// TODO: now it duplicates the logic with content insets
7875
// Android prop
79-
contentInsetBottom: bottomPadding.value,
76+
contentInsetBottom: !inverted ? bottomPadding.value : 0,
77+
contentInsetTop: inverted ? bottomPadding.value : 0,
8078
};
8179

8280
if (contentOffsetY) {
@@ -100,17 +98,9 @@ const ScrollViewWithBottomPadding = forwardRef<
10098
return (
10199
<ReanimatedClippingScrollView
102100
animatedProps={animatedProps}
103-
style={[
104-
styles.container,
105-
OS === "android" ? containerStyle : undefined,
106-
]}
101+
style={styles.container}
107102
>
108-
<ScrollViewComponent
109-
ref={ref}
110-
animatedProps={animatedProps}
111-
style={style}
112-
{...rest}
113-
>
103+
<ScrollViewComponent ref={ref} animatedProps={animatedProps} {...rest}>
114104
{children}
115105
</ScrollViewComponent>
116106
</ReanimatedClippingScrollView>

src/specs/ClippingScrollViewDecoratorViewNativeComponent.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { Double } from "react-native/Libraries/Types/CodegenTypes";
66

77
export interface NativeProps extends ViewProps {
88
contentInsetBottom: Double;
9+
contentInsetTop: Double;
910
}
1011

1112
export default codegenNativeComponent<NativeProps>(

src/types/views.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,5 +53,7 @@ export type ClippingScrollViewProps = PropsWithChildren<
5353
ViewProps & {
5454
/** An additional space that gets applied to the bottom of the `ScrollView` (inside a scrollable content). Default is `0`. */
5555
contentInsetBottom?: number;
56+
/** An additional space that gets applied to the top of the `ScrollView` (inside a scrollable content). Default is `0`. */
57+
contentInsetTop?: number;
5658
}
5759
>;

0 commit comments

Comments
 (0)