Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(Android): Request layout manually for CustomToolbar below Android API 29 #2332

Merged
merged 2 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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';

Loading