diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenContainer.kt b/android/src/main/java/com/swmansion/rnscreens/ScreenContainer.kt index 422b21bc60..b04a3578a3 100644 --- a/android/src/main/java/com/swmansion/rnscreens/ScreenContainer.kt +++ b/android/src/main/java/com/swmansion/rnscreens/ScreenContainer.kt @@ -243,6 +243,15 @@ open class ScreenContainer( transaction.commitNowAllowingStateLoss() } + fun notifyScreenDetached(screen: Screen) { + if (context is ReactContext) { + val surfaceId = UIManagerHelper.getSurfaceId(context) + UIManagerHelper + .getEventDispatcherForReactTag(context as ReactContext, screen.id) + ?.dispatchEvent(ScreenDismissedEvent(surfaceId, screen.id)) + } + } + fun notifyTopDetached() { val top = topScreen as Screen if (context is ReactContext) { diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenStack.kt b/android/src/main/java/com/swmansion/rnscreens/ScreenStack.kt index 9894cd4be0..2bf18bca7e 100644 --- a/android/src/main/java/com/swmansion/rnscreens/ScreenStack.kt +++ b/android/src/main/java/com/swmansion/rnscreens/ScreenStack.kt @@ -109,6 +109,22 @@ class ScreenStack( super.removeScreenAt(index) } + // When there is more then one active screen on stack, + // pops the screen, so that only one remains + // Returns true when any screen was popped + // When there was only one screen on stack returns false + fun popToRoot(): Boolean { + val rootIndex = screenWrappers.indexOfFirst { it.screen.activityState != Screen.ActivityState.INACTIVE } + val lastActiveIndex = screenWrappers.indexOfLast { it.screen.activityState != Screen.ActivityState.INACTIVE } + if (rootIndex >= 0 && lastActiveIndex > rootIndex) { + for (screenIndex in (rootIndex + 1)..lastActiveIndex) { + notifyScreenDetached(screenWrappers[screenIndex].screen) + } + return true + } + return false + } + override fun removeAllScreens() { dismissedWrappers.clear() super.removeAllScreens() diff --git a/android/src/main/java/com/swmansion/rnscreens/gamma/helpers/ViewFinder.kt b/android/src/main/java/com/swmansion/rnscreens/gamma/helpers/ViewFinder.kt new file mode 100644 index 0000000000..7a6ac9f617 --- /dev/null +++ b/android/src/main/java/com/swmansion/rnscreens/gamma/helpers/ViewFinder.kt @@ -0,0 +1,43 @@ +package com.swmansion.rnscreens.gamma.helpers + +import android.view.View +import android.view.ViewGroup +import android.widget.ScrollView +import androidx.core.view.isNotEmpty +import com.swmansion.rnscreens.ScreenStack + +class ViewFinder { + companion object { + fun findScrollViewInFirstDescendantChain(view: View): ScrollView? { + var currentView: View? = view + + while (currentView != null) { + if (currentView is ScrollView) { + return currentView + } else if (currentView is ViewGroup && currentView.isNotEmpty()) { + currentView = currentView.getChildAt(0) + } else { + break + } + } + + return null + } + + fun findScreenStackInFirstDescendantChain(view: View): ScreenStack? { + var currentView: View? = view + + while (currentView != null) { + if (currentView is ScreenStack) { + return currentView + } else if (currentView is ViewGroup && currentView.isNotEmpty()) { + currentView = currentView.getChildAt(0) + } else { + break + } + } + + return null + } + } +} diff --git a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabScreen.kt b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabScreen.kt index 12c7beaea1..ea70037f97 100644 --- a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabScreen.kt +++ b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabScreen.kt @@ -68,6 +68,9 @@ class TabScreen( updateMenuItemAttributesIfNeeded(oldValue, newValue) } + var shouldUseRepeatedTabSelectionScrollToTopSpecialEffect: Boolean = true + var shouldUseRepeatedTabSelectionPopToRootSpecialEffect: Boolean = true + private fun updateMenuItemAttributesIfNeeded( oldValue: T, newValue: T, diff --git a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabScreenViewManager.kt b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabScreenViewManager.kt index e5b00283ff..6cfe8752dc 100644 --- a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabScreenViewManager.kt +++ b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabScreenViewManager.kt @@ -128,10 +128,28 @@ class TabScreenViewManager : view.tabTitle = value } + @ReactProp(name = "specialEffects") override fun setSpecialEffects( view: TabScreen, value: ReadableMap?, - ) = Unit + ) { + var scrollToTop = true + var popToRoot = true + if (value?.hasKey("repeatedTabSelection") ?: false) { + value.getMap("repeatedTabSelection")?.let { repeatedTabSelectionConfig -> + if (repeatedTabSelectionConfig.hasKey("scrollToTop")) { + scrollToTop = + repeatedTabSelectionConfig.getBoolean("scrollToTop") + } + if (repeatedTabSelectionConfig.hasKey("popToRoot")) { + popToRoot = + repeatedTabSelectionConfig.getBoolean("popToRoot") + } + } + } + view.shouldUseRepeatedTabSelectionPopToRootSpecialEffect = popToRoot + view.shouldUseRepeatedTabSelectionScrollToTopSpecialEffect = scrollToTop + } override fun setOverrideScrollViewContentInsetAdjustmentBehavior( view: TabScreen, diff --git a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabsHost.kt b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabsHost.kt index ca1dfe7a54..f73da61a51 100644 --- a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabsHost.kt +++ b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabsHost.kt @@ -16,6 +16,7 @@ import com.facebook.react.uimanager.ThemedReactContext import com.google.android.material.bottomnavigation.BottomNavigationView import com.swmansion.rnscreens.BuildConfig import com.swmansion.rnscreens.gamma.helpers.FragmentManagerHelper +import com.swmansion.rnscreens.gamma.helpers.ViewFinder import com.swmansion.rnscreens.gamma.helpers.ViewIdGenerator import com.swmansion.rnscreens.safearea.EdgeInsets import com.swmansion.rnscreens.safearea.SafeAreaProvider @@ -92,7 +93,31 @@ class TabsHost( } } + private inner class SpecialEffectsHandler { + // Handles the repeated tab selection special effects such as popToRoot and scrollToTop + // Returns true if any special effect was handled + fun handleRepeatedTabSelection(): Boolean { + val contentView = this@TabsHost.contentView + val selectedTabFragment = this@TabsHost.currentFocusedTab + if (selectedTabFragment.tabScreen.shouldUseRepeatedTabSelectionPopToRootSpecialEffect) { + val screenStack = ViewFinder.findScreenStackInFirstDescendantChain(contentView) + if (screenStack != null && screenStack.popToRoot()) { + return true + } + } + if (selectedTabFragment.tabScreen.shouldUseRepeatedTabSelectionScrollToTopSpecialEffect) { + val scrollView = ViewFinder.findScrollViewInFirstDescendantChain(contentView) + if (scrollView != null && scrollView.scrollY > 0) { + scrollView.smoothScrollTo(scrollView.scrollX, 0) + return true + } + } + return false + } + } + private val containerUpdateCoordinator = ContainerUpdateCoordinator() + private val specialEffectsHandler = SpecialEffectsHandler() private val wrappedContext = ContextThemeWrapper( @@ -128,6 +153,9 @@ class TabsHost( private val tabScreenFragments: MutableList = arrayListOf() + private val currentFocusedTab: TabScreenFragment + get() = checkNotNull(tabScreenFragments.find { it.tabScreen.isFocusedTab }) { "[RNScreens] No focused tab present" } + private var lastAppliedUiMode: Int? = null private var isLayoutEnqueued: Boolean = false @@ -219,8 +247,10 @@ class TabsHost( bottomNavigationView.setOnItemSelectedListener { item -> RNSLog.d(TAG, "Item selected $item") val fragment = getFragmentForMenuItemId(item.itemId) - val tabKey = fragment?.tabScreen?.tabKey ?: "undefined" - eventEmitter.emitOnNativeFocusChange(tabKey) + if (fragment != currentFocusedTab || !specialEffectsHandler.handleRepeatedTabSelection()) { + val tabKey = fragment?.tabScreen?.tabKey ?: "undefined" + eventEmitter.emitOnNativeFocusChange(tabKey) + } true } } @@ -327,8 +357,7 @@ class TabsHost( } private fun updateSelectedTab() { - val newFocusedTab = - checkNotNull(tabScreenFragments.find { it.tabScreen.isFocusedTab }) { "[RNScreens] No focused tab present" } + val newFocusedTab = currentFocusedTab check(requireFragmentManager.fragments.size <= 1) { "[RNScreens] There can be only a single focused tab" } val oldFocusedTab = requireFragmentManager.fragments.firstOrNull() diff --git a/apps/src/tests/TestBottomTabs/index.tsx b/apps/src/tests/TestBottomTabs/index.tsx index 34c91ce52f..e368ed4d08 100644 --- a/apps/src/tests/TestBottomTabs/index.tsx +++ b/apps/src/tests/TestBottomTabs/index.tsx @@ -38,11 +38,11 @@ const TAB_CONFIGS: TabConfiguration[] = [ ios: { type: 'sfSymbol', name: 'house.fill', - }, + }, android: { type: 'imageSource', imageSource: require('../../../assets/variableIcons/icon_fill.png'), - } + }, }, selectedIcon: { type: 'sfSymbol', @@ -105,11 +105,11 @@ const TAB_CONFIGS: TabConfiguration[] = [ ios: { type: 'templateSource', templateSource: require('../../../assets/variableIcons/icon.png'), - }, + }, android: { type: 'drawableResource', name: 'sym_call_missed', - } + }, }, selectedIcon: { type: 'templateSource', @@ -148,7 +148,7 @@ const TAB_CONFIGS: TabConfiguration[] = [ shared: { type: 'imageSource', imageSource: require('../../../assets/variableIcons/icon.png'), - } + }, }, selectedIcon: { type: 'imageSource', @@ -171,8 +171,8 @@ const TAB_CONFIGS: TabConfiguration[] = [ }, android: { type: 'drawableResource', - name: 'custom_home_icon' - } + name: 'custom_home_icon', + }, }, selectedIcon: { type: 'sfSymbol', @@ -181,6 +181,11 @@ const TAB_CONFIGS: TabConfiguration[] = [ title: 'Tab4', systemItem: 'search', // iOS specific badgeValue: '123', + specialEffects: { + repeatedTabSelection: { + popToRoot: false, + }, + }, }, component: Tab4, }, diff --git a/apps/src/tests/TestBottomTabs/tabs/Tab4.tsx b/apps/src/tests/TestBottomTabs/tabs/Tab4.tsx index c97c64a6e6..18470057b4 100644 --- a/apps/src/tests/TestBottomTabs/tabs/Tab4.tsx +++ b/apps/src/tests/TestBottomTabs/tabs/Tab4.tsx @@ -22,7 +22,7 @@ const Stack = createNativeStackNavigator(); export function LongText() { return ( - + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sed egestas felis. Proin laoreet eros a tellus elementum, quis euismod enim gravida. Morbi at arcu commodo, condimentum purus a, congue sapien. Nunc luctus @@ -113,7 +113,7 @@ export function Tab4() {