Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
16 changes: 16 additions & 0 deletions android/src/main/java/com/swmansion/rnscreens/ScreenStack.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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

object ViewFinder {
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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ class TabScreen(
updateMenuItemAttributesIfNeeded(oldValue, newValue)
}

var shouldUseRepeatedTabSelectionScrollToTopSpecialEffect: Boolean = true
var shouldUseRepeatedTabSelectionPopToRootSpecialEffect: Boolean = true

private fun <T> updateMenuItemAttributesIfNeeded(
oldValue: T,
newValue: T,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,28 @@ class TabScreenViewManager :
value: Boolean,
) = Unit

@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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -92,7 +93,29 @@ class TabsHost(
}
}

private inner class SpecialEffectsHandler {
fun handleRepeatedTabSelection(): Boolean {
val contentView = [email protected]
val selectedTabFragment = [email protected]
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(
Expand Down Expand Up @@ -128,6 +151,9 @@ class TabsHost(

private val tabScreenFragments: MutableList<TabScreenFragment> = 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
Expand Down Expand Up @@ -226,8 +252,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
}
}
Expand Down Expand Up @@ -334,8 +362,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()
Expand Down
19 changes: 12 additions & 7 deletions apps/src/tests/TestBottomTabs/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -148,7 +148,7 @@ const TAB_CONFIGS: TabConfiguration[] = [
shared: {
type: 'imageSource',
imageSource: require('../../../assets/variableIcons/icon.png'),
}
},
},
selectedIcon: {
type: 'imageSource',
Expand All @@ -171,8 +171,8 @@ const TAB_CONFIGS: TabConfiguration[] = [
},
android: {
type: 'drawableResource',
name: 'custom_home_icon'
}
name: 'custom_home_icon',
},
},
selectedIcon: {
type: 'sfSymbol',
Expand All @@ -181,6 +181,11 @@ const TAB_CONFIGS: TabConfiguration[] = [
title: 'Tab4',
systemItem: 'search', // iOS specific
badgeValue: '123',
specialEffects: {
repeatedTabSelection: {
popToRoot: false,
},
},
},
component: Tab4,
},
Expand Down
2 changes: 1 addition & 1 deletion apps/src/tests/TestBottomTabs/tabs/Tab4.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export function Tab4() {
<Stack.Screen
name="Screen1"
component={Screen1}
options={{ headerTransparent: true }}
options={{ headerTransparent: false }}
/>
<Stack.Screen
name="Screen2"
Expand Down
8 changes: 6 additions & 2 deletions src/components/bottom-tabs/BottomTabsScreen.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,12 +467,16 @@ export interface BottomTabsScreenProps {
* `popToRoot` has priority over `scrollToTop`.
*
* @default All special effects are enabled by default.
*
* @platform ios
*/
specialEffects?: {
repeatedTabSelection?: {
/**
* @default true
*/
popToRoot?: boolean;
/**
* @default true
*/
scrollToTop?: boolean;
};
};
Expand Down
Loading