Skip to content

Commit 6b1de35

Browse files
kligarskikkafar
andauthored
fix(Android): fix formSheet sliding from top when it contains input with autofocus (#2909)
## Description On API 36, formSheet that contains input with autoFocus would sometimes slide from the top. Even though `react-native-screens` used `com.google.android.material:material:1.6.1`, `InsetsAnimationCallback` (which is not present in `1.6.1`) is called (it probably comes from `com.google.android.material:material:1.12.0` dependency in `react-native-edge-to-edge`). This callback interferes with our formSheet entering/closing animations: 1. Values used to calculate start and end position in `InsetsAnimationCallback`'s `onPrepare` and `onStart` are usually incorrect. This has become a significant issue since API 36: for some reason, animation created in `ScreenStackFragment`'s `onCreateAnimator` is sometimes incorrecly accounted for in calculating `startTranslationY`. The value becomes negative which means that the formSheet slides from the top. 2. Both `InsetsAnimationCallback` and formSheet animations use `setTranslationY` and they are overwriting each other's changes. `InsetsAnimationCallback`'s `onProgress` is called just after animation's `setTranslationY` so it takes precedence. We decided to replace the window insets animation callback which is set in `BottomSheetBehavior`'s `onLayoutChild` with our own implementation. Currently it is empty in order to bring back previous behavior. FormSheet that contains input with autoFocus still does not behave as intented - only rarely it gets expanded with the keyboard. This seems to be a timing issue and needs more investigation. It might be necessary to implement logic in new window insets animation callback. Fixes #2895. ## Changes - change `com.google.android.material:material` version to `1.12.0` - replace `BottomSheetBehavior`'s `InsetsAnimationCallback` with empty implementation - add test screen `Test2895` ## Test code and steps to reproduce Open `Test2895` screen in the example app and open the formSheet. ## Checklist - [x] Included code example that can be used to test this change - [x] Ensured that CI passes --------- Co-authored-by: Kacper Kafara <[email protected]>
1 parent 5ba6cea commit 6b1de35

File tree

5 files changed

+121
-1
lines changed

5 files changed

+121
-1
lines changed

android/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ dependencies {
231231
implementation 'androidx.fragment:fragment-ktx:1.6.1'
232232
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0'
233233
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
234-
implementation 'com.google.android.material:material:1.6.1'
234+
implementation 'com.google.android.material:material:1.12.0'
235235
implementation "androidx.core:core-ktx:1.8.0"
236236

237237
constraints {

android/src/main/java/com/swmansion/rnscreens/Screen.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ class Screen(
188188
if (!usesFormSheetPresentation() || !isNativeStackScreen) {
189189
return
190190
}
191+
191192
if (coordinatorLayoutDidChange) {
192193
dispatchShadowStateUpdate(width, height, top)
193194
}

android/src/main/java/com/swmansion/rnscreens/ScreenStackFragment.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ import android.view.animation.Animation
1717
import android.widget.LinearLayout
1818
import androidx.appcompat.widget.Toolbar
1919
import androidx.coordinatorlayout.widget.CoordinatorLayout
20+
import androidx.core.view.ViewCompat
21+
import androidx.core.view.WindowInsetsAnimationCompat
22+
import androidx.core.view.WindowInsetsCompat
2023
import com.facebook.react.uimanager.PixelUtil
2124
import com.facebook.react.uimanager.UIManagerHelper
2225
import com.google.android.material.appbar.AppBarLayout
@@ -237,6 +240,21 @@ class ScreenStackFragment :
237240
View.MeasureSpec.makeMeasureSpec(container.height, View.MeasureSpec.EXACTLY),
238241
)
239242
coordinatorLayout.layout(0, 0, container.width, container.height)
243+
244+
// Replace InsetsAnimationCallback created by BottomSheetBehavior with empty
245+
// implementation so it does not interfere with our custom formSheet entering animation
246+
// More details: https://github.com/software-mansion/react-native-screens/pull/2909
247+
ViewCompat.setWindowInsetsAnimationCallback(
248+
screen,
249+
object : WindowInsetsAnimationCompat.Callback(
250+
DISPATCH_MODE_STOP,
251+
) {
252+
override fun onProgress(
253+
insets: WindowInsetsCompat,
254+
runningAnimations: MutableList<WindowInsetsAnimationCompat>,
255+
): WindowInsetsCompat = insets
256+
},
257+
)
240258
}
241259

242260
return coordinatorLayout

apps/src/tests/Test2895.tsx

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import React, { Fragment } from 'react';
2+
import { NavigationContainer, ParamListBase } from '@react-navigation/native';
3+
import {
4+
NativeStackNavigationProp,
5+
createNativeStackNavigator,
6+
} from '@react-navigation/native-stack';
7+
import { Button, StyleSheet, View } from 'react-native';
8+
import { ThemedText, ThemedTextInput } from '../shared';
9+
10+
type StackRouteParamList = {
11+
Home: undefined;
12+
FormSheet: undefined;
13+
};
14+
15+
type NavigationProp<ParamList extends ParamListBase> = {
16+
navigation: NativeStackNavigationProp<ParamList>;
17+
};
18+
19+
type StackNavigationProp = NavigationProp<StackRouteParamList>;
20+
21+
const Stack = createNativeStackNavigator<StackRouteParamList>();
22+
23+
function Home({ navigation }: StackNavigationProp) {
24+
return (
25+
<View style={[{ backgroundColor: 'goldenrod', flex: 1 }]}>
26+
<Button
27+
title="Open FormSheet"
28+
onPress={() => navigation.navigate('FormSheet')}
29+
/>
30+
</View>
31+
);
32+
}
33+
34+
function FormSheet({}: StackNavigationProp) {
35+
const fields = [
36+
{ name: 'form-first-name', placeholder: 'First Name *', autoFocus: true },
37+
{ name: 'form-last-name', placeholder: 'Last Name *' },
38+
{ name: 'form-email', placeholder: 'Email *' },
39+
];
40+
41+
return (
42+
<View testID="form" style={styles.wrapper}>
43+
<ThemedText testID="form-header" style={styles.heading}>
44+
Example form
45+
</ThemedText>
46+
{fields.map(({ name, placeholder, autoFocus }) => (
47+
<Fragment key={name}>
48+
<ThemedText testID={`${name}-label`} style={styles.label}>
49+
{placeholder}
50+
</ThemedText>
51+
<ThemedTextInput
52+
autoFocus={autoFocus}
53+
testID={`${name}-input`}
54+
style={styles.input}
55+
/>
56+
</Fragment>
57+
))}
58+
</View>
59+
);
60+
}
61+
62+
export default function App() {
63+
return (
64+
<NavigationContainer>
65+
<Stack.Navigator>
66+
<Stack.Screen name="Home" component={Home} />
67+
<Stack.Screen
68+
name="FormSheet"
69+
component={FormSheet}
70+
options={{
71+
presentation: 'formSheet',
72+
sheetAllowedDetents: [0.5, 0.85],
73+
}}
74+
/>
75+
</Stack.Navigator>
76+
</NavigationContainer>
77+
);
78+
}
79+
80+
const styles = StyleSheet.create({
81+
wrapper: {
82+
margin: 15,
83+
},
84+
heading: {
85+
fontSize: 16,
86+
fontWeight: 'bold',
87+
marginBottom: 16,
88+
},
89+
label: {
90+
textTransform: 'capitalize',
91+
fontSize: 12,
92+
marginBottom: 8,
93+
},
94+
input: {
95+
borderWidth: 1,
96+
borderRadius: 5,
97+
marginBottom: 12,
98+
height: 40,
99+
},
100+
});

apps/src/tests/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ export { default as Test2811 } from './Test2811';
133133
export { default as Test2819 } from './Test2819';
134134
export { default as Test2855 } from './Test2855';
135135
export { default as Test2877 } from './Test2877'; // [E2E created](iOS): issue is related to formSheet on iOS
136+
export { default as Test2895 } from './Test2895';
136137
export { default as Test2899 } from './Test2899';
137138
export { default as TestScreenAnimation } from './TestScreenAnimation';
138139
export { default as TestScreenAnimationV5 } from './TestScreenAnimationV5';

0 commit comments

Comments
 (0)