software-mansion / react-native-screens

Native navigation primitives for your React Native app.
MIT License
2.91k stars 498 forks source link

Search Bar looks broken when used along with a custom header title in Android #1858

Open ha3 opened 11 months ago

ha3 commented 11 months ago

Description

In Android, when search bar is enabled along with a custom header title, they are rendered on top of each other. The problem persists after search bar is focused. It works correctly if a string is provided for the headerTitle.

Steps to reproduce

  1. Add search bar and a custom title to a screen
  2. Navigate to that screen
  3. See search bar looks broken

Snack or a link to a repository

https://snack.expo.dev/@hozdemir/groaning-apples

Screens version

3.23.0

React Native version

0.72.3

Platforms

Android

JavaScript runtime

Hermes

Workflow

React Native (without Expo)

Architecture

Paper (Old Architecture)

Build type

Debug mode

Device

Real device

Device model

Redmi Note 7

Acknowledgements

Yes

ha3 commented 11 months ago

@kkafar hi. I created this patch, but not sure this is the correct approach–I'm quite inexperienced on the native side.

This solves the title and search bar being rendered on top of each other. But, in a real device, the search icon ends up aligned incorrectly to the left, if the search bar is blurred. On the emulator, everything works as expected.

diff --git a/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/CustomSearchView.kt b/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/CustomSearchView.kt
index 6984b10..17e23f0 100644
--- a/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/CustomSearchView.kt
+++ b/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/CustomSearchView.kt
@@ -4,6 +4,7 @@ import android.content.Context
 import androidx.activity.OnBackPressedCallback
 import androidx.appcompat.widget.SearchView
 import androidx.fragment.app.Fragment
+import kotlin.collections.ArrayList

 class CustomSearchView(context: Context, fragment: Fragment) : SearchView(context) {
     /*
@@ -13,8 +14,9 @@ class CustomSearchView(context: Context, fragment: Fragment) : SearchView(contex
         setOnSearchClickListener - https://developer.android.com/reference/android/widget/SearchView#setOnSearchClickListener(android.view.View.OnClickListener)
         setOnCloseListener - https://developer.android.com/reference/android/widget/SearchView#setOnCloseListener(android.widget.SearchView.OnCloseListener)
     */
-    private var mCustomOnCloseListener: OnCloseListener? = null
-    private var mCustomOnSearchClickedListener: OnClickListener? = null
+
+    private var mCustomOnCloseListeners: MutableList<OnCloseListener> = ArrayList()
+    private var mCustomOnSearchClickedListeners: MutableList<OnClickListener> = ArrayList()

     private var mOnBackPressedCallback: OnBackPressedCallback =
         object : OnBackPressedCallback(true) {
@@ -40,12 +42,12 @@ class CustomSearchView(context: Context, fragment: Fragment) : SearchView(contex

     fun setText(text: String) = setQuery(text, false)

-    override fun setOnCloseListener(listener: OnCloseListener?) {
-        mCustomOnCloseListener = listener
+    override fun setOnCloseListener(listener: OnCloseListener) {
+        mCustomOnCloseListeners.add(listener)
     }

-    override fun setOnSearchClickListener(listener: OnClickListener?) {
-        mCustomOnSearchClickedListener = listener
+    override fun setOnSearchClickListener(listener: OnClickListener) {
+        mCustomOnSearchClickedListeners.add(listener)
     }

     override fun onAttachedToWindow() {
@@ -62,14 +64,14 @@ class CustomSearchView(context: Context, fragment: Fragment) : SearchView(contex

     init {
         super.setOnSearchClickListener { v ->
-            mCustomOnSearchClickedListener?.onClick(v)
+            mCustomOnSearchClickedListeners.forEach { it.onClick(v) }
             backPressOverrider.maybeAddBackCallback()
         }

         super.setOnCloseListener {
-            val result = mCustomOnCloseListener?.onClose() ?: false
+            mCustomOnCloseListeners.forEach { it.onClose() }
             backPressOverrider.removeBackCallbackIfAdded()
-            result
+            false
         }

         maxWidth = Integer.MAX_VALUE
diff --git a/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/ScreenStackFragment.kt b/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/ScreenStackFragment.kt
index 86a7654..391ac4b 100644
--- a/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/ScreenStackFragment.kt
+++ b/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/ScreenStackFragment.kt
@@ -147,6 +147,25 @@ class ScreenStackFragment : ScreenFragment {
         return false
     }

+    private val subViewsToHideDuringSearch: MutableList<ScreenStackHeaderSubview>
+        get() {
+            val arr: MutableList<ScreenStackHeaderSubview> = ArrayList()
+
+            val config = screen.headerConfig
+            val numberOfSubViews = config?.configSubviewsCount ?: 0
+
+            if (config != null && numberOfSubViews > 0) {
+                for (i in 0 until numberOfSubViews) {
+                    val subView = config.getConfigSubview(i)
+                    if (subView.type != ScreenStackHeaderSubview.Type.SEARCH_BAR) {
+                        arr.add(subView)
+                    }
+                }
+            }
+
+            return arr
+        }
+
     private fun updateToolbarMenu(menu: Menu) {
         menu.clear()
         if (shouldShowSearchBar()) {
@@ -154,6 +173,13 @@ class ScreenStackFragment : ScreenFragment {
             if (searchView == null && currentContext != null) {
                 val newSearchView = CustomSearchView(currentContext, this)
                 searchView = newSearchView
+                newSearchView.setOnSearchClickListener {
+                    subViewsToHideDuringSearch.forEach { it.visibility = View.GONE }
+                }
+                newSearchView.setOnCloseListener {
+                    subViewsToHideDuringSearch.forEach { it.visibility = View.VISIBLE }
+                    false
+                }
                 onSearchViewCreate?.invoke(newSearchView)
             }
             menu.add("").apply {
Acetyld commented 11 months ago

Exact problem i am facing, same with the headerRight icons, i cant seem to find a solid way, atm i got:

  const navigation = useNavigation<TasksScreenNavigationProp<'index'>>();
  const [search, setSearch] = React.useState('');
  const [isSearching, setIsSearching] = React.useState(false);
  const taskCount = 8381;
  const searchBarRef = useRef<SearchBarCommands>(null);
  useLayoutEffect(() => {
    navigation.setOptions({
      headerSearchBarOptions: {
        autoCapitalize: 'none',
        cancelButtonText: 'Annuleer',
        placeholder: 'Zoeken',
        shouldShowHintSearchIcon: false,
        textColor: colors.white,
        hintTextColor: colors.primary['300'],
        tintColor: colors.white,
        headerIconColor: colors.white,
        onChangeText: event => setSearch(event.nativeEvent.text),
        ref: searchBarRef,
        onOpen: async () => {
          setIsSearching(true);
        },
        onClose: async () => {
          console.log('close');
          setIsSearching(false);
        },
      },
      headerTitle: props =>
        !isSearching ? (
          <HeaderTitle
            style={{
              marginLeft: Platform.OS === 'android' ? -20 : 0,
            }}
            {...props}>{`${taskCount} openstaande taken`}</HeaderTitle>
        ) : null,
      headerRight: () =>
        !isSearching ? (
          <View
            className={'h-full flex-row'}
            style={{
              gap: 10,
            }}>
            <Pressable hitSlop={7} className={'active:opacity-50'}>
              <FontAwesomeIcon
                color={'#fff'}
                icon={['far', 'ellipsis-vertical']}
                size={20}
              />
            </Pressable>
            <Pressable hitSlop={7} className={'active:opacity-50'}>
              <FontAwesomeIcon
                color={'#fff'}
                icon={['far', 'filter']}
                size={20}
              />
            </Pressable>
          </View>
        ) : null,
    });
  }, [isSearching, navigation]);

But this is causing a small flicker on open

ha3 commented 8 months ago

@tboba thanks for the https://github.com/software-mansion/react-native-screens/pull/1903 which also addresses this issue. Changing the visibility of header elements, Instead of pushing them to the side, was my approach to the problem too. Unfortunately, with 3.27.0 I encountered the same problem that I had–the search icon is misaligned after the blur on certain Android devices, i.e. Xiaomi and Huawei.

On these devices, the search icon is pushed significantly to the left after the blur whether a custom header element is used or not. Here are some screenshots of the problem. It is tested on Xiaomi Redmi:

No custom element - Before focus: image

No custom element - After blur: image

Custom header left - Before focus: Screenshot_2023-10-29-16-17-34-764_com meel meel

Custom header left - After blur: Screenshot_2023-10-29-16-17-03-775_com meel meel

alduzy commented 1 week ago

Hi, I've tested it with the latest version and could not reproduce the issue. I'll try again using Xiaomi/Huawei device in upcoming days. In the meantime, can anyone confirm that the problem still persists?