From d394e2c10abe6f61373d3d22f3ffba4f4e4d9ea3 Mon Sep 17 00:00:00 2001 From: Tymoteusz Boba Date: Thu, 12 Sep 2024 09:27:52 +0200 Subject: [PATCH] fix(Android): Request layout manually for CustomToolbar below Android 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
BEFORE ![CleanShot 2024-09-03 at 17 59 13](https://github.com/user-attachments/assets/3f7952a5-6430-4b25-b587-4690fac236d3)
AFTER ![CleanShot 2024-09-03 at 17 51 14](https://github.com/user-attachments/assets/f2551b98-5de1-4021-8c72-0e4718aaaf45)
## 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 --- .../com/swmansion/rnscreens/CustomToolbar.kt | 41 ++++++++- apps/src/tests/Test2332.tsx | 86 +++++++++++++++++++ apps/src/tests/index.ts | 2 +- 3 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 apps/src/tests/Test2332.tsx diff --git a/android/src/main/java/com/swmansion/rnscreens/CustomToolbar.kt b/android/src/main/java/com/swmansion/rnscreens/CustomToolbar.kt index 7168a4c6fd..0682ebc063 100644 --- a/android/src/main/java/com/swmansion/rnscreens/CustomToolbar.kt +++ b/android/src/main/java/com/swmansion/rnscreens/CustomToolbar.kt @@ -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, + ) + } + } + } +} diff --git a/apps/src/tests/Test2332.tsx b/apps/src/tests/Test2332.tsx new file mode 100644 index 0000000000..9728439194 --- /dev/null +++ b/apps/src/tests/Test2332.tsx @@ -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>(); + const onHandlePress = () => { + navigation.navigate('Details'); + }; + return ( + + HomeScreen + +