Skip to content

Commit

Permalink
fix(Android): prevent sheet footer from flickering on sheet sliding (#…
Browse files Browse the repository at this point in the history
…2505)

## Description

Previous implementation of `sheetTopWhileDragging` relied on linear
interpolation.
Due to numeric errors in floating point division sometimes the footer
was +- 1 pixel up/down
from the accurate position, resulting in 1px flickering / "dancing".

This PR changes the way sheet top is computed to making use of the
parent screen `top` property,
whenever possible.

## Changes

- **Add test dedicated to formsheet**
- **"Stabilize" footer**

## Test code and steps to reproduce

Added dedicated test `TestFormSheet`.

## Checklist

- [ ] Included code example that can be used to test this change
- [ ] Updated TS types
- [ ] Updated documentation: <!-- For adding new props to native-stack
-->
- [ ]
https://github.com/software-mansion/react-native-screens/blob/main/guides/GUIDE_FOR_LIBRARY_AUTHORS.md
- [ ]
https://github.com/software-mansion/react-native-screens/blob/main/native-stack/README.md
- [ ]
https://github.com/software-mansion/react-native-screens/blob/main/src/types.tsx
- [ ]
https://github.com/software-mansion/react-native-screens/blob/main/src/native-stack/types.tsx
- [ ] Ensured that CI passes
  • Loading branch information
kkafar authored Nov 14, 2024
1 parent 1351473 commit 4d9eb76
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 8 deletions.
20 changes: 16 additions & 4 deletions android/src/main/java/com/swmansion/rnscreens/ScreenFooter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class ScreenFooter(
private val sheetBehavior
get() = requireScreenParent().sheetBehavior

private val hasReceivedInitialLayoutFromParent get() = lastContainerHeight > 0

// Due to Android restrictions on layout flow, particularly
// the fact that onMeasure must set `measuredHeight` & `measuredWidth` React calls `measure` on every
Expand Down Expand Up @@ -92,9 +93,10 @@ class ScreenFooter(
}

init {
val rootView = checkNotNull(reactContext.currentActivity) {
"[RNScreens] Context detached from activity while creating ScreenFooter"
}.window.decorView
val rootView =
checkNotNull(reactContext.currentActivity) {
"[RNScreens] Context detached from activity while creating ScreenFooter"
}.window.decorView

// Note that we do override insets animation on given view. I can see it interfering e.g.
// with reanimated keyboard or even other places in our code. Need to test this.
Expand All @@ -115,6 +117,9 @@ class ScreenFooter(
bottom: Int,
) {
super.onLayout(changed, left, top, right, bottom)
if (!hasReceivedInitialLayoutFromParent) {
return
}
layoutFooterOnYAxis(
lastContainerHeight,
bottom - top,
Expand Down Expand Up @@ -219,11 +224,18 @@ class ScreenFooter(
*
* This method should not be used for sheet in stable state.
*
* Currently the implementation assumes that the Screen's (sheet's) container starts at y: 0
* in global coordinates. Then we can use simply sheet's top. If that is for some reason
* unavailable, then we fallback to interpolation basing on values provided by sheet behaviour.
*
* We don't want to primarily rely on interpolation, because due to division rounding errors the footer
* will "flicker" (jump up / down a single pixel).
*
* @param slideOffset sheet offset as reported by [BottomSheetCallback.onSlide]
* @return position of sheet's top **relative to container**
*/
private fun sheetTopWhileDragging(slideOffset: Float): Int =
MathUtils
screenParent?.top ?: MathUtils
.lerp(
sheetTopInStableState(STATE_COLLAPSED).toFloat(),
sheetTopInStableState(
Expand Down
8 changes: 4 additions & 4 deletions apps/App.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React from 'react';
import { enableFreeze } from 'react-native-screens';
import Example from './Example';
// import * as Test from './src/tests';
// import Example from './Example';
import * as Test from './src/tests';

enableFreeze(true);

export default function App() {
return <Example />;
// return <Test.Test42 />;
// return <Example />;
return <Test.TestFormSheet />;
}
64 changes: 64 additions & 0 deletions apps/src/tests/TestFormSheet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { NavigationContainer, RouteProp } from "@react-navigation/native";
import { NativeStackNavigationProp, createNativeStackNavigator } from "@react-navigation/native-stack";
import React, { useLayoutEffect } from "react";
import { Button, Text, TextInput, View } from "react-native";

type RouteParamList = {
Home: undefined;
FormSheet: undefined;
}

type RouteProps<RouteName extends keyof RouteParamList> = {
navigation: NativeStackNavigationProp<RouteParamList, RouteName>;
route: RouteProp<RouteParamList, RouteName>;
}

const Stack = createNativeStackNavigator<RouteParamList>();

function Home({ navigation }: RouteProps<'Home'>) {
return (
<View style={{ flex: 1, backgroundColor: 'lightsalmon' }}>
<Button title="Open FormSheet" onPress={() => navigation.navigate('FormSheet')} />
</View>
);
}

function FormSheet({ navigation }: RouteProps<'FormSheet'>) {
return (
<View style={{ backgroundColor: 'lightgreen' }}>
<View style={{ paddingTop: 20 }}>
<Button title="Go back" onPress={() => navigation.goBack()} />
</View>
<View style={{ alignItems: 'center' }}>
<TextInput style={{ marginVertical: 12, paddingVertical: 8, backgroundColor: 'lavender', borderRadius: 24, width: '80%' }} placeholder="Trigger keyboard..."></TextInput>
</View>
</View>
)
}

export default function App() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Home" component={Home} />
<Stack.Screen name="FormSheet" component={FormSheet} options={{
presentation: 'formSheet',
sheetAllowedDetents: [0.5, 1.0],
sheetCornerRadius: 8,
sheetLargestUndimmedDetentIndex: 'last',
contentStyle: {
backgroundColor: 'lightblue',
},
unstable_sheetFooter: () => {
return (
<View style={{ height: 64, backgroundColor: 'red' }}>
<Text>Footer</Text>
<Button title="Just click me" onPress={() => console.log('Footer button clicked')} />
</View>
);
}
}}/>
</Stack.Navigator>
</NavigationContainer>
);
}
1 change: 1 addition & 0 deletions apps/src/tests/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,4 @@ export { default as TestActivityStateProgression } from './TestActivityStateProg
export { default as TestHeaderTitle } from './TestHeaderTitle';
export { default as TestModalNavigation } from './TestModalNavigation';
export { default as TestMemoryLeak } from './TestMemoryLeak';
export { default as TestFormSheet } from './TestFormSheet';

0 comments on commit 4d9eb76

Please sign in to comment.