Skip to content

Commit

Permalink
fix(Android): Request layout manually for CustomToolbar below Android…
Browse files Browse the repository at this point in the history
… API 29 (#2332)

## Description

On Android API 29, while using `windowSoftInputMode` with `adjustPan`
option, requestLayout is not being called while subviews are being
added. That's because while ScreenStackHeaderConfig adds view to the
toolbar, onMeasure is being called and even if we're calling
`requestLayout` on parent, Android is returning from requesting the
layout, as there's somehow ongoing layout. This is not the case for
Android API 30 and higher.

The solution is to request another layout via ReactChoreographer (same
as in ScreenContainer) to call our own layout callback on the next
frame.

## Changes

- Request layout via ReactChoreographer on `requestLayout` call in
CustomToolbar class

## Screenshots / GIFs

<details><summary>BEFORE</summary>

![CleanShot 2024-09-03 at 17 59
13](https://github.com/user-attachments/assets/3f7952a5-6430-4b25-b587-4690fac236d3)

</details>

<details><summary>AFTER</summary>

![CleanShot 2024-09-03 at 17 51
14](https://github.com/user-attachments/assets/f2551b98-5de1-4021-8c72-0e4718aaaf45)

</details>

## Test code and steps to reproduce

Use `Test2332.tsx` test case to check whether this PR works properly.

## Checklist

- [x] Included code example that can be used to test this change
- [ ] Ensured that CI passes

---------

Co-authored-by: Kacper Kafara <kacper.kafara@swmansion.com>
  • Loading branch information
tboba and kkafar authored Sep 12, 2024
1 parent ef47538 commit d394e2c
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 2 deletions.
41 changes: 40 additions & 1 deletion android/src/main/java/com/swmansion/rnscreens/CustomToolbar.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,50 @@ package com.swmansion.rnscreens

import android.annotation.SuppressLint
import android.content.Context
import android.os.Build
import androidx.appcompat.widget.Toolbar
import com.facebook.react.modules.core.ChoreographerCompat
import com.facebook.react.modules.core.ReactChoreographer

// This class is used to store config closer to search bar
@SuppressLint("ViewConstructor") // Only we construct this view, it is never inflated.
open class CustomToolbar(
context: Context,
val config: ScreenStackHeaderConfig,
) : Toolbar(context)
) : Toolbar(context) {
private var isLayoutEnqueued = false
private val layoutCallback: ChoreographerCompat.FrameCallback =
object : ChoreographerCompat.FrameCallback() {
override fun doFrame(frameTimeNanos: Long) {
isLayoutEnqueued = false
measure(
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY),
)
layout(left, top, right, bottom)
}
}

override fun requestLayout() {
super.requestLayout()
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
// Below Android API 29, layout is not being requested when subviews are being added to the layout,
// leading to having their subviews in position 0,0 of the toolbar (as Android don't calculate
// the position of each subview, even if Yoga has correctly set their width and height).
// This is mostly the issue, when windowSoftInputMode is set to adjustPan in AndroidManifest.
// Thus, we're manually calling the layout **after** the current layout.
@Suppress("SENSELESS_COMPARISON") // mLayoutCallback can be null here since this method can be called in init
if (!isLayoutEnqueued && layoutCallback != null) {
isLayoutEnqueued = true
// we use NATIVE_ANIMATED_MODULE choreographer queue because it allows us to catch the current
// looper loop instead of enqueueing the update in the next loop causing a one frame delay.
ReactChoreographer
.getInstance()
.postFrameCallback(
ReactChoreographer.CallbackType.NATIVE_ANIMATED_MODULE,
layoutCallback,
)
}
}
}
}
86 changes: 86 additions & 0 deletions apps/src/tests/Test2332.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/**
*
* IMPORTANT! READ BEFORE TESTING!
* Remember to switch windowSoftInputMode to `adjustPan` in AndroidManifest.xml file.
*
*/

import React, { useLayoutEffect } from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { useNavigation } from '@react-navigation/native';
import { Button, Text, TextInput, View } from 'react-native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { HeaderButton } from '@react-navigation/elements';
type RootStackNavigatorParamsList = {
Home: undefined;
Details: undefined;
};
const Stack = createNativeStackNavigator();
const HomeScreen = () => {
const navigation =
useNavigation<NativeStackNavigationProp<RootStackNavigatorParamsList>>();
const onHandlePress = () => {
navigation.navigate('Details');
};
return (
<View>
<Text>HomeScreen</Text>
<View>
<Button title="Go to Details" onPress={onHandlePress} />
</View>
</View>
);
};
const DetailsScreen = () => {
const [text, setText] = React.useState('');
const navigation =
useNavigation<NativeStackNavigationProp<RootStackNavigatorParamsList>>();
useLayoutEffect(() => {
navigation.setOptions({
headerTitle: 'Detail Screen',
headerRight: () => {
if (text.length === 0) {
return null;
}
return (
<HeaderButton>
<Text>X</Text>
</HeaderButton>
);
},
});
}, [navigation, text]);
const onHandlePress = () => {
navigation.goBack();
};

return (
<View>
<Text>RegisterScreen</Text>
<View>
<TextInput
style={{ backgroundColor: 'grey', height: 40, borderColor: 'black' }}
placeholder="Enter text"
value={text}
onChangeText={text => {
setText(text);
}}
/>
<Button title="Go to Home" onPress={onHandlePress} />
<Button title="Go to Details" onPress={onHandlePress} />
</View>
</View>
);
};
function App() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Details" component={DetailsScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}
export default App;
2 changes: 1 addition & 1 deletion apps/src/tests/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,6 @@ export { default as Test2235 } from './Test2235';
export { default as Test2252 } from './Test2252';
export { default as Test2271 } from './Test2271';
export { default as Test2282 } from './Test2282';
export { default as Test2232 } from './Test2332';
export { default as TestScreenAnimation } from './TestScreenAnimation';
export { default as TestHeader } from './TestHeader';

0 comments on commit d394e2c

Please sign in to comment.