diff --git a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabsAppearanceApplicator.kt b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabsAppearanceApplicator.kt new file mode 100644 index 0000000000..8b6abe0d8c --- /dev/null +++ b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabsAppearanceApplicator.kt @@ -0,0 +1,321 @@ +package com.swmansion.rnscreens.gamma.tabs + +import android.annotation.SuppressLint +import android.content.res.ColorStateList +import android.util.TypedValue +import android.view.MenuItem +import android.view.ViewGroup +import android.widget.TextView +import androidx.appcompat.view.ContextThemeWrapper +import androidx.core.view.children +import androidx.core.view.isVisible +import com.facebook.react.common.assets.ReactFontManager +import com.facebook.react.uimanager.PixelUtil +import com.google.android.material.bottomnavigation.BottomNavigationView +import com.google.android.material.navigation.NavigationBarView + +@SuppressLint("PrivateResource") // We want to use variables from material design for default values +class TabsAppearanceApplicator( + private val context: ContextThemeWrapper, + private val bottomNavigationView: BottomNavigationView, +) { + private var lastBackgroundColor: Int? = null + private var lastFontColors: IntArray? = null + private var lastIconColors: IntArray? = null + private var lastLabelVisibilityMode: Int? = null + private var lastRippleColor: Int? = null + private var lastActiveIndicatorColor: Int? = null + private var lastIsActiveIndicatorEnabled: Boolean? = null + + private val lastBadgeValues = mutableMapOf() + private val lastBadgeTextColors = mutableMapOf() + private val lastBadgeBackgroundColors = mutableMapOf() + + private val states = + arrayOf( + intArrayOf(-android.R.attr.state_enabled), // disabled + intArrayOf(android.R.attr.state_selected), // selected + intArrayOf(android.R.attr.state_focused), // focused + intArrayOf(), // normal + ) + + private inline fun updatePropIfChanged( + oldValue: T, + newValue: T, + updateFn: (T) -> Unit, + ): T { + if (oldValue != newValue) { + updateFn(newValue) + } + return newValue + } + + private inline fun updatePropsIfArrayChanged( + oldValue: IntArray?, + newValue: IntArray, + updateFn: (IntArray) -> Unit, + ): IntArray { + if (oldValue == null || !oldValue.contentEquals(newValue)) { + updateFn(newValue) + } + return newValue + } + + private fun resolveColorAttr(attr: Int): Int { + val typedValue = TypedValue() + context.theme.resolveAttribute(attr, typedValue, true) + return typedValue.data + } + + fun updateSharedAppearance(tabsHost: TabsHost) { + val tabBarAppearance = tabsHost.currentFocusedTab.tabsScreen.appearance + + updatePropIfChanged(bottomNavigationView.isVisible, !tabsHost.tabBarHidden) { + bottomNavigationView.isVisible = !tabsHost.tabBarHidden + } + + val newBackgroundColor = + tabBarAppearance?.backgroundColor + ?: resolveColorAttr(com.google.android.material.R.attr.colorSurfaceContainer) + lastBackgroundColor = + updatePropIfChanged(lastBackgroundColor, newBackgroundColor) { + bottomNavigationView.setBackgroundColor(newBackgroundColor) + } + + // Font color + // Defaults from spec: https://m3.material.io/components/navigation-bar/specs + val fontDisabledColor = + tabBarAppearance?.itemColors?.disabled?.titleColor + ?: resolveColorAttr(com.google.android.material.R.attr.colorOnSurface) + + val fontFocusedColor = + tabBarAppearance?.itemColors?.focused?.titleColor + ?: resolveColorAttr(com.google.android.material.R.attr.colorOnSurfaceVariant) + + val fontSelectedColor = + tabBarAppearance?.itemColors?.selected?.titleColor + ?: resolveColorAttr(com.google.android.material.R.attr.colorSecondary) + + val fontNormalColor = + tabBarAppearance?.itemColors?.normal?.titleColor + ?: resolveColorAttr(com.google.android.material.R.attr.colorOnSurfaceVariant) + + val newFontColors = intArrayOf(fontDisabledColor, fontSelectedColor, fontFocusedColor, fontNormalColor) + lastFontColors = + updatePropsIfArrayChanged(lastFontColors, newFontColors) { + bottomNavigationView.itemTextColor = ColorStateList(states, newFontColors) + } + + // Icon color + // Defaults from spec: https://m3.material.io/components/navigation-bar/specs + val iconDisabledColor = + tabBarAppearance?.itemColors?.disabled?.iconColor + ?: resolveColorAttr(com.google.android.material.R.attr.colorOnSurface) + + val iconFocusedColor = + tabBarAppearance?.itemColors?.focused?.iconColor + ?: resolveColorAttr(com.google.android.material.R.attr.colorOnSurfaceVariant) + + val iconSelectedColor = + tabBarAppearance?.itemColors?.selected?.iconColor + ?: resolveColorAttr(com.google.android.material.R.attr.colorOnSecondaryContainer) + + val iconNormalColor = + tabBarAppearance?.itemColors?.normal?.iconColor + ?: resolveColorAttr(com.google.android.material.R.attr.colorOnSurfaceVariant) + + val newIconColors = intArrayOf(iconDisabledColor, iconSelectedColor, iconFocusedColor, iconNormalColor) + lastIconColors = + updatePropsIfArrayChanged(lastIconColors, newIconColors) { + bottomNavigationView.itemIconTintList = ColorStateList(states, newIconColors) + } + + // LabelVisibilityMode + // From docs: can be one of LABEL_VISIBILITY_AUTO, LABEL_VISIBILITY_SELECTED, LABEL_VISIBILITY_LABELED, or LABEL_VISIBILITY_UNLABELED + + val newVisibilityMode = + when (tabBarAppearance?.labelVisibilityMode) { + "selected" -> NavigationBarView.LABEL_VISIBILITY_SELECTED + "labeled" -> NavigationBarView.LABEL_VISIBILITY_LABELED + "unlabeled" -> NavigationBarView.LABEL_VISIBILITY_UNLABELED + else -> NavigationBarView.LABEL_VISIBILITY_AUTO + } + lastLabelVisibilityMode = + updatePropIfChanged(lastLabelVisibilityMode, newVisibilityMode) { + bottomNavigationView.labelVisibilityMode = newVisibilityMode + } + + // Ripple color + val newRippleColor = + tabBarAppearance?.itemRippleColor + ?: resolveColorAttr(com.google.android.material.R.attr.itemRippleColor) + lastRippleColor = + updatePropIfChanged(lastRippleColor, newRippleColor) { + bottomNavigationView.itemRippleColor = ColorStateList.valueOf(newRippleColor) + } + + // Active Indicator + val newActiveIndicatorColor = + tabBarAppearance?.activeIndicator?.color + ?: resolveColorAttr(com.google.android.material.R.attr.colorSecondaryContainer) + lastActiveIndicatorColor = + updatePropIfChanged(lastActiveIndicatorColor, newActiveIndicatorColor) { + bottomNavigationView.itemActiveIndicatorColor = ColorStateList.valueOf(newActiveIndicatorColor) + } + + val newIsActiveIndicatorEnabled = tabBarAppearance?.activeIndicator?.enabled ?: true + lastIsActiveIndicatorEnabled = + updatePropIfChanged(lastIsActiveIndicatorEnabled, newIsActiveIndicatorEnabled) { + bottomNavigationView.isItemActiveIndicatorEnabled = newIsActiveIndicatorEnabled + } + } + + fun updateFontStyles(tabsHost: TabsHost) { + val tabBarAppearance = tabsHost.currentFocusedTab.tabsScreen.appearance + + val bottomNavigationMenuView = bottomNavigationView.getChildAt(0) as ViewGroup + + val newIsFontStyleItalic = tabBarAppearance?.typography?.fontStyle == "italic" + + // Bold is 700, normal is 400 -> https://github.com/facebook/react-native/blob/e0efd3eb5b637bd00fb7528ab4d129f6b3e13d03/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/assets/ReactFontManager.kt#L150 + // It can be any other int -> https://reactnative.dev/docs/text-style-props#fontweight + // Default is 400 -> https://github.com/facebook/react-native/blob/e0efd3eb5b637bd00fb7528ab4d129f6b3e13d03/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/assets/ReactFontManager.kt#L117 + val newFontWeight = + if (tabBarAppearance?.typography?.fontWeight == + "bold" + ) { + 700 + } else { + tabBarAppearance?.typography?.fontWeight?.toIntOrNull() ?: 400 + } + + val newFontFamily = + ReactFontManager.getInstance().getTypeface( + tabBarAppearance?.typography?.fontFamily ?: "", + newFontWeight, + newIsFontStyleItalic, + context.assets, + ) + + /* + Short explanation about computations we're doing below. + R.dimen, has defined value in SP, getDimension converts it to pixels, and by default + TextView.setTextSize accepts SP, so the size is multiplied by density twice. Thus we need + to convert both values to pixels and make sure that setTextSizes is about that. + The Text tag in RN uses SP or DP based on `allowFontScaling` prop. For now we're going + with SP, if there will be a need for skipping scale, the we should introduce similar + `allowFontScaling` prop. + */ + val newSmallFontSize = + tabBarAppearance + ?.typography + ?.fontSizeSmall + ?.takeIf { it > 0 } + ?.let { PixelUtil.toPixelFromSP(it) } + ?: context.resources.getDimension(com.google.android.material.R.dimen.design_bottom_navigation_text_size) + val newLargeFontSize = + tabBarAppearance + ?.typography + ?.fontSizeLarge + ?.takeIf { it > 0 } + ?.let { PixelUtil.toPixelFromSP(it) } + ?: context.resources.getDimension(com.google.android.material.R.dimen.design_bottom_navigation_text_size) + + for (menuItem in bottomNavigationMenuView.children) { + val largeLabel = + menuItem.findViewById(com.google.android.material.R.id.navigation_bar_item_large_label_view) + val smallLabel = + menuItem.findViewById(com.google.android.material.R.id.navigation_bar_item_small_label_view) + + // Inactive + updatePropIfChanged(smallLabel.textSize, newSmallFontSize) { + smallLabel.setTextSize(TypedValue.COMPLEX_UNIT_PX, newSmallFontSize) + } + updatePropIfChanged(smallLabel.typeface, newFontFamily) { + smallLabel.typeface = newFontFamily + } + + // Active + updatePropIfChanged(largeLabel.textSize, newLargeFontSize) { + largeLabel.setTextSize(TypedValue.COMPLEX_UNIT_PX, newLargeFontSize) + } + updatePropIfChanged(largeLabel.typeface, newFontFamily) { + largeLabel.typeface = newFontFamily + } + } + } + + fun updateMenuItemAppearance( + menuItem: MenuItem, + tabsScreen: TabsScreen, + ) { + updatePropIfChanged(menuItem.title, tabsScreen.tabTitle) { + menuItem.title = tabsScreen.tabTitle + } + + updatePropIfChanged(menuItem.icon, tabsScreen.icon) { + menuItem.icon = tabsScreen.icon + } + } + + internal fun updateBadgeAppearance( + menuItem: MenuItem, + tabsScreen: TabsScreen, + badgeAppearance: BadgeAppearance?, + ) { + val menuItemIndex = bottomNavigationView.menu.children.indexOf(menuItem) + val badgeValue = tabsScreen.badgeValue + + if (badgeValue == null) { + if (lastBadgeValues[menuItemIndex] != null || !lastBadgeValues.containsKey(menuItemIndex)) { + lastBadgeValues[menuItemIndex] = null + } + + val badge = bottomNavigationView.getBadge(menuItemIndex) + badge?.isVisible = false + + return + } + + val badge = bottomNavigationView.getOrCreateBadge(menuItemIndex) + badge.isVisible = true + + lastBadgeValues[menuItemIndex] = + updatePropIfChanged(lastBadgeValues[menuItemIndex], badgeValue) { newValue -> + val badgeValueNumber = newValue?.toIntOrNull() + + badge.clearText() + badge.clearNumber() + + if (badgeValueNumber != null) { + badge.number = badgeValueNumber + } else if (newValue != "") { + badge.text = newValue + } + } + + // Styling + val oldBadgeTextColor: Int = + lastBadgeTextColors[menuItemIndex] + ?: resolveColorAttr(com.google.android.material.R.attr.colorOnError) + val newBadgeTextColor = + badgeAppearance?.textColor + ?: resolveColorAttr(com.google.android.material.R.attr.colorOnError) + lastBadgeTextColors[menuItemIndex] = + updatePropIfChanged(oldBadgeTextColor, newBadgeTextColor) { + badge.badgeTextColor = newBadgeTextColor + } + + // https://github.com/material-components/material-components-android/blob/master/docs/getting-started.md#non-transitive-r-classes-referencing-library-resources-programmatically + val oldBadgeBackgroundColor: Int = + lastBadgeBackgroundColors[menuItemIndex] + ?: resolveColorAttr(androidx.appcompat.R.attr.colorError) + val newBadgeBackgroundColor = + badgeAppearance?.backgroundColor + ?: resolveColorAttr(androidx.appcompat.R.attr.colorError) + lastBadgeBackgroundColors[menuItemIndex] = + updatePropIfChanged(oldBadgeBackgroundColor, newBadgeBackgroundColor) { + badge.backgroundColor = newBadgeBackgroundColor + } + } +} diff --git a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabsHostAppearanceCoordinator.kt b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabsAppearanceCoordinator.kt similarity index 79% rename from android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabsHostAppearanceCoordinator.kt rename to android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabsAppearanceCoordinator.kt index 6ab0610b6c..aad457683d 100644 --- a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabsHostAppearanceCoordinator.kt +++ b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabsAppearanceCoordinator.kt @@ -6,37 +6,39 @@ import androidx.appcompat.view.ContextThemeWrapper import androidx.core.view.size import com.google.android.material.bottomnavigation.BottomNavigationView -class TabsHostAppearanceCoordinator( +class TabsAppearanceCoordinator( context: ContextThemeWrapper, private val bottomNavigationView: BottomNavigationView, private val tabsScreenFragments: MutableList, ) { - private val appearanceApplicator = TabsHostAppearanceApplicator(context, bottomNavigationView) + private val appearanceApplicator = TabsAppearanceApplicator(context, bottomNavigationView) fun updateTabAppearance(tabsHost: TabsHost) { appearanceApplicator.updateSharedAppearance(tabsHost) - updateMenuItems() + updateMenuItems(tabsHost) appearanceApplicator.updateFontStyles(tabsHost) // It needs to be updated after updateMenuItems } - private fun updateMenuItems() { + private fun updateMenuItems(tabsHost: TabsHost) { if (bottomNavigationView.menu.size != tabsScreenFragments.size) { // Most likely first render or some tab has been removed. Let's nuke the menu (easiest option). bottomNavigationView.menu.clear() } tabsScreenFragments.forEachIndexed { index, fragment -> + val appearance = tabsHost.currentFocusedTab.tabsScreen.appearance val menuItem = bottomNavigationView.menu.getOrCreateMenuItem(index, fragment.tabsScreen) check(menuItem.itemId == index) { "[RNScreens] Illegal state: menu items are shuffled" } - updateMenuItemAppearance(menuItem, fragment.tabsScreen) + updateMenuItemAppearance(menuItem, fragment.tabsScreen, appearance) } } - fun updateMenuItemAppearance( + internal fun updateMenuItemAppearance( menuItem: MenuItem, tabsScreen: TabsScreen, + appearance: AndroidTabsAppearance?, ) { appearanceApplicator.updateMenuItemAppearance(menuItem, tabsScreen) - appearanceApplicator.updateBadgeAppearance(menuItem, tabsScreen) + appearanceApplicator.updateBadgeAppearance(menuItem, tabsScreen, appearance?.badge) } } diff --git a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabsAppearanceModel.kt b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabsAppearanceModel.kt new file mode 100644 index 0000000000..0ec1456e36 --- /dev/null +++ b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabsAppearanceModel.kt @@ -0,0 +1,41 @@ +package com.swmansion.rnscreens.gamma.tabs + +internal data class AndroidTabsAppearance( + val backgroundColor: Int? = null, + val itemColors: BottomNavItemColors? = null, + val activeIndicator: ActiveIndicatorAppearance? = null, + val itemRippleColor: Int? = null, + val labelVisibilityMode: String? = null, + val typography: TypographyAppearance? = null, + val badge: BadgeAppearance? = null, +) + +internal data class BottomNavItemColors( + val normal: ItemStateColors? = null, + val selected: ItemStateColors? = null, + val disabled: ItemStateColors? = null, + val focused: ItemStateColors? = null, +) + +internal data class ItemStateColors( + val iconColor: Int? = null, + val titleColor: Int? = null, +) + +internal data class ActiveIndicatorAppearance( + val enabled: Boolean? = null, + val color: Int? = null, +) + +internal data class TypographyAppearance( + val fontFamily: String? = null, + val fontSizeSmall: Float? = null, + val fontSizeLarge: Float? = null, + val fontWeight: String? = null, + val fontStyle: String? = null, +) + +internal data class BadgeAppearance( + val textColor: Int? = null, + val backgroundColor: Int? = null, +) 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 3ab574b435..5a96fe160e 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 @@ -153,7 +153,7 @@ class TabsHost( private val tabsScreenFragments: MutableList = arrayListOf() - private val currentFocusedTab: TabsScreenFragment + internal val currentFocusedTab: TabsScreenFragment get() = checkNotNull(tabsScreenFragments.find { it.tabsScreen.isFocusedTab }) { "[RNScreens] No focused tab present" } private var lastAppliedUiMode: Int? = null @@ -163,66 +163,10 @@ class TabsHost( private var interfaceInsetsChangeListener: SafeAreaView? = null private val appearanceCoordinator = - TabsHostAppearanceCoordinator(wrappedContext, bottomNavigationView, tabsScreenFragments) + TabsAppearanceCoordinator(wrappedContext, bottomNavigationView, tabsScreenFragments) private val a11yCoordinator = TabsHostA11yCoordinator(bottomNavigationView, tabsScreenFragments) - var tabBarBackgroundColor: Int? by Delegates.observable(null) { _, oldValue, newValue -> - updateNavigationMenuIfNeeded(oldValue, newValue) - } - - var tabBarItemActiveIndicatorColor: Int? by Delegates.observable(null) { _, oldValue, newValue -> - updateNavigationMenuIfNeeded(oldValue, newValue) - } - - var isTabBarItemActiveIndicatorEnabled: Boolean by Delegates.observable(true) { _, oldValue, newValue -> - updateNavigationMenuIfNeeded(oldValue, newValue) - } - - var tabBarItemIconColor: Int? by Delegates.observable(null) { _, oldValue, newValue -> - updateNavigationMenuIfNeeded(oldValue, newValue) - } - - var tabBarItemTitleFontFamily: String? by Delegates.observable(null) { _, oldValue, newValue -> - updateNavigationMenuIfNeeded(oldValue, newValue) - } - - var tabBarItemIconColorActive: Int? by Delegates.observable(null) { _, oldValue, newValue -> - updateNavigationMenuIfNeeded(oldValue, newValue) - } - - var tabBarItemTitleFontColor: Int? by Delegates.observable(null) { _, oldValue, newValue -> - updateNavigationMenuIfNeeded(oldValue, newValue) - } - - var tabBarItemTitleFontColorActive: Int? by Delegates.observable(null) { _, oldValue, newValue -> - updateNavigationMenuIfNeeded(oldValue, newValue) - } - - var tabBarItemTitleFontSize: Float? by Delegates.observable(null) { _, oldValue, newValue -> - updateNavigationMenuIfNeeded(oldValue, newValue) - } - - var tabBarItemTitleFontSizeActive: Float? by Delegates.observable(null) { _, oldValue, newValue -> - updateNavigationMenuIfNeeded(oldValue, newValue) - } - - var tabBarItemTitleFontWeight: String? by Delegates.observable(null) { _, oldValue, newValue -> - updateNavigationMenuIfNeeded(oldValue, newValue) - } - - var tabBarItemTitleFontStyle: String? by Delegates.observable(null) { _, oldValue, newValue -> - updateNavigationMenuIfNeeded(oldValue, newValue) - } - - var tabBarItemRippleColor: Int? by Delegates.observable(null) { _, oldValue, newValue -> - updateNavigationMenuIfNeeded(oldValue, newValue) - } - - var tabBarItemLabelVisibilityMode: String? by Delegates.observable(null) { _, oldValue, newValue -> - updateNavigationMenuIfNeeded(oldValue, newValue) - } - var tabBarHidden: Boolean by Delegates.observable(false) { _, oldValue, newValue -> if (newValue != oldValue) { updateInterfaceInsets() @@ -336,6 +280,15 @@ class TabsHost( } } + override fun onAppearanceChanged(tabScreen: TabsScreen) { + if (tabScreen.isFocusedTab) { + containerUpdateCoordinator.let { + it.invalidateNavigationMenu() + it.postContainerUpdateIfNeeded() + } + } + } + override fun onTabFocusChangedFromJS( tabsScreen: TabsScreen, isFocused: Boolean, @@ -348,7 +301,8 @@ class TabsHost( override fun onMenuItemAttributesChange(tabsScreen: TabsScreen) { getMenuItemForTabsScreen(tabsScreen)?.let { menuItem -> - appearanceCoordinator.updateMenuItemAppearance(menuItem, tabsScreen) + val appearance = currentFocusedTab.tabsScreen.appearance + appearanceCoordinator.updateMenuItemAppearance(menuItem, tabsScreen, appearance) a11yCoordinator.setA11yPropertiesToTabItem(menuItem, tabsScreen) } } diff --git a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabsHostAppearanceApplicator.kt b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabsHostAppearanceApplicator.kt deleted file mode 100644 index 8020c2b022..0000000000 --- a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabsHostAppearanceApplicator.kt +++ /dev/null @@ -1,204 +0,0 @@ -package com.swmansion.rnscreens.gamma.tabs - -import android.annotation.SuppressLint -import android.content.res.ColorStateList -import android.util.TypedValue -import android.view.MenuItem -import android.view.ViewGroup -import android.widget.TextView -import androidx.appcompat.view.ContextThemeWrapper -import androidx.core.view.children -import androidx.core.view.isVisible -import com.facebook.react.common.assets.ReactFontManager -import com.facebook.react.uimanager.PixelUtil -import com.google.android.material.bottomnavigation.BottomNavigationView -import com.google.android.material.navigation.NavigationBarView - -@SuppressLint("PrivateResource") // We want to use variables from material design for default values -class TabsHostAppearanceApplicator( - private val context: ContextThemeWrapper, - private val bottomNavigationView: BottomNavigationView, -) { - private fun resolveColorAttr(attr: Int): Int { - val typedValue = TypedValue() - context.theme.resolveAttribute(attr, typedValue, true) - return typedValue.data - } - - fun updateSharedAppearance(tabsHost: TabsHost) { - bottomNavigationView.isVisible = !tabsHost.tabBarHidden - bottomNavigationView.setBackgroundColor( - tabsHost.tabBarBackgroundColor - ?: resolveColorAttr(com.google.android.material.R.attr.colorSurfaceContainer), - ) - - val states = - arrayOf( - intArrayOf(-android.R.attr.state_checked), - intArrayOf(android.R.attr.state_checked), - ) - - // Font color - val fontInactiveColor = - tabsHost.tabBarItemTitleFontColor - ?: resolveColorAttr(com.google.android.material.R.attr.colorOnSurfaceVariant) - - val fontActiveColor = - tabsHost.tabBarItemTitleFontColorActive - ?: tabsHost.tabBarItemTitleFontColor - ?: resolveColorAttr(com.google.android.material.R.attr.colorSecondary) - - val fontColors = intArrayOf(fontInactiveColor, fontActiveColor) - bottomNavigationView.itemTextColor = ColorStateList(states, fontColors) - - // Icon color - val iconInactiveColor = - tabsHost.tabBarItemIconColor - ?: resolveColorAttr(com.google.android.material.R.attr.colorOnSurfaceVariant) - - val iconActiveColor = - tabsHost.tabBarItemIconColorActive - ?: tabsHost.tabBarItemIconColor - ?: resolveColorAttr(com.google.android.material.R.attr.colorOnSecondaryContainer) - - val iconColors = intArrayOf(iconInactiveColor, iconActiveColor) - bottomNavigationView.itemIconTintList = ColorStateList(states, iconColors) - - // LabelVisibilityMode - // From docs: can be one of LABEL_VISIBILITY_AUTO, LABEL_VISIBILITY_SELECTED, LABEL_VISIBILITY_LABELED, or LABEL_VISIBILITY_UNLABELED - - val visibilityMode = - when (tabsHost.tabBarItemLabelVisibilityMode) { - "selected" -> NavigationBarView.LABEL_VISIBILITY_SELECTED - "labeled" -> NavigationBarView.LABEL_VISIBILITY_LABELED - "unlabeled" -> NavigationBarView.LABEL_VISIBILITY_UNLABELED - else -> NavigationBarView.LABEL_VISIBILITY_AUTO - } - - bottomNavigationView.labelVisibilityMode = visibilityMode - - // Ripple color - val rippleColor = - tabsHost.tabBarItemRippleColor - ?: resolveColorAttr(com.google.android.material.R.attr.itemRippleColor) - bottomNavigationView.itemRippleColor = ColorStateList.valueOf(rippleColor) - - // Active Indicator - val activeIndicatorColor = - tabsHost.tabBarItemActiveIndicatorColor - ?: resolveColorAttr(com.google.android.material.R.attr.colorSecondaryContainer) - - bottomNavigationView.isItemActiveIndicatorEnabled = - tabsHost.isTabBarItemActiveIndicatorEnabled - bottomNavigationView.itemActiveIndicatorColor = ColorStateList.valueOf(activeIndicatorColor) - } - - fun updateFontStyles(tabsHost: TabsHost) { - val bottomNavigationMenuView = bottomNavigationView.getChildAt(0) as ViewGroup - - for (menuItem in bottomNavigationMenuView.children) { - val largeLabel = - menuItem.findViewById(com.google.android.material.R.id.navigation_bar_item_large_label_view) - val smallLabel = - menuItem.findViewById(com.google.android.material.R.id.navigation_bar_item_small_label_view) - - val isFontStyleItalic = tabsHost.tabBarItemTitleFontStyle == "italic" - - // Bold is 700, normal is 400 -> https://github.com/facebook/react-native/blob/e0efd3eb5b637bd00fb7528ab4d129f6b3e13d03/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/assets/ReactFontManager.kt#L150 - // It can be any other int -> https://reactnative.dev/docs/text-style-props#fontweight - // Default is 400 -> https://github.com/facebook/react-native/blob/e0efd3eb5b637bd00fb7528ab4d129f6b3e13d03/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/assets/ReactFontManager.kt#L117 - val fontWeight = - if (tabsHost.tabBarItemTitleFontWeight == - "bold" - ) { - 700 - } else { - tabsHost.tabBarItemTitleFontWeight?.toIntOrNull() ?: 400 - } - - val fontFamily = - ReactFontManager.getInstance().getTypeface( - tabsHost.tabBarItemTitleFontFamily ?: "", - fontWeight, - isFontStyleItalic, - context.assets, - ) - - /* - Short explanation about computations we're doing below. - R.dimen, has defined value in SP, getDimension converts it to pixels, and by default - TextView.setTextSize accepts SP, so the size is multiplied by density twice. Thus we need - to convert both values to pixels and make sure that setTextSizes is about that. - The Text tag in RN uses SP or DP based on `allowFontScaling` prop. For now we're going - with SP, if there will be a need for skipping scale, the we should introduce similar - `allowFontScaling` prop. - */ - val smallFontSize = - tabsHost.tabBarItemTitleFontSize?.takeIf { it > 0 }?.let { PixelUtil.toPixelFromSP(it) } - ?: context.resources.getDimension(com.google.android.material.R.dimen.design_bottom_navigation_text_size) - val largeFontSize = - tabsHost.tabBarItemTitleFontSizeActive?.takeIf { it > 0 }?.let { PixelUtil.toPixelFromSP(it) } - ?: context.resources.getDimension(com.google.android.material.R.dimen.design_bottom_navigation_text_size) - - // Inactive - smallLabel.setTextSize(TypedValue.COMPLEX_UNIT_PX, smallFontSize) - smallLabel.typeface = fontFamily - - // Active - largeLabel.setTextSize(TypedValue.COMPLEX_UNIT_PX, largeFontSize) - largeLabel.typeface = fontFamily - } - } - - fun updateMenuItemAppearance( - menuItem: MenuItem, - tabsScreen: TabsScreen, - ) { - if (menuItem.title != tabsScreen.tabTitle) { - menuItem.title = tabsScreen.tabTitle - } - - if (menuItem.icon != tabsScreen.icon) { - menuItem.icon = tabsScreen.icon - } - } - - fun updateBadgeAppearance( - menuItem: MenuItem, - tabsScreen: TabsScreen, - ) { - val menuItemIndex = bottomNavigationView.menu.children.indexOf(menuItem) - val badgeValue = tabsScreen.badgeValue - - if (badgeValue == null) { - val badge = bottomNavigationView.getBadge(menuItemIndex) - badge?.isVisible = false - - return - } - - val badgeValueNumber = badgeValue.toIntOrNull() - - val badge = bottomNavigationView.getOrCreateBadge(menuItemIndex) - badge.isVisible = true - - badge.clearText() - badge.clearNumber() - - if (badgeValueNumber != null) { - badge.number = badgeValueNumber - } else if (badgeValue != "") { - badge.text = badgeValue - } - - // Styling - badge.badgeTextColor = - tabsScreen.tabBarItemBadgeTextColor - ?: resolveColorAttr(com.google.android.material.R.attr.colorOnError) - - // https://github.com/material-components/material-components-android/blob/master/docs/getting-started.md#non-transitive-r-classes-referencing-library-resources-programmatically - badge.backgroundColor = - tabsScreen.tabBarItemBadgeBackgroundColor - ?: resolveColorAttr(androidx.appcompat.R.attr.colorError) - } -} diff --git a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabsHostViewManager.kt b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabsHostViewManager.kt index 3f56f76ee6..d69508e875 100644 --- a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabsHostViewManager.kt +++ b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabsHostViewManager.kt @@ -66,72 +66,16 @@ class TabsHostViewManager : // These should be ignored or another component, dedicated for Android should be used - @ReactProp(name = "tabBarBackgroundColor", customType = "Color") - override fun setTabBarBackgroundColor( - view: TabsHost, - value: Int?, - ) { - view.tabBarBackgroundColor = value - } - override fun setTabBarTintColor( view: TabsHost, value: Int?, ) = Unit - @ReactProp(name = "tabBarItemTitleFontSize") - override fun setTabBarItemTitleFontSize( - view: TabsHost?, - value: Float, - ) { - view?.tabBarItemTitleFontSize = value - } - override fun setControlNavigationStateInJS( view: TabsHost?, value: Boolean, ) = Unit - @ReactProp(name = "tabBarItemTitleFontFamily") - override fun setTabBarItemTitleFontFamily( - view: TabsHost, - value: String?, - ) { - view.tabBarItemTitleFontFamily = value - } - - @ReactProp(name = "tabBarItemTitleFontWeight") - override fun setTabBarItemTitleFontWeight( - view: TabsHost, - value: String?, - ) { - view.tabBarItemTitleFontWeight = value - } - - @ReactProp(name = "tabBarItemTitleFontStyle") - override fun setTabBarItemTitleFontStyle( - view: TabsHost, - value: String?, - ) { - view.tabBarItemTitleFontStyle = value - } - - @ReactProp(name = "tabBarItemTitleFontColor", customType = "Color") - override fun setTabBarItemTitleFontColor( - view: TabsHost, - value: Int?, - ) { - view.tabBarItemTitleFontColor = value - } - - @ReactProp(name = "tabBarItemIconColor", customType = "Color") - override fun setTabBarItemIconColor( - view: TabsHost, - value: Int?, - ) { - view.tabBarItemIconColor = value - } - override fun setTabBarMinimizeBehavior( view: TabsHost, value: String?, @@ -158,64 +102,6 @@ class TabsHostViewManager : view.nativeContainerBackgroundColor = value } - // Android additional - - @ReactProp(name = "tabBarItemTitleFontColorActive", customType = "Color") - override fun setTabBarItemTitleFontColorActive( - view: TabsHost, - value: Int?, - ) { - view.tabBarItemTitleFontColorActive = value - } - - @ReactProp(name = "tabBarItemActiveIndicatorColor", customType = "Color") - override fun setTabBarItemActiveIndicatorColor( - view: TabsHost, - value: Int?, - ) { - view.tabBarItemActiveIndicatorColor = value - } - - @ReactProp(name = "tabBarItemActiveIndicatorEnabled") - override fun setTabBarItemActiveIndicatorEnabled( - view: TabsHost, - value: Boolean, - ) { - view.isTabBarItemActiveIndicatorEnabled = value - } - - @ReactProp(name = "tabBarItemIconColorActive", customType = "Color") - override fun setTabBarItemIconColorActive( - view: TabsHost, - value: Int?, - ) { - view.tabBarItemIconColorActive = value - } - - @ReactProp(name = "tabBarItemTitleFontSizeActive") - override fun setTabBarItemTitleFontSizeActive( - view: TabsHost?, - value: Float, - ) { - view?.tabBarItemTitleFontSizeActive = value - } - - @ReactProp(name = "tabBarItemRippleColor", customType = "Color") - override fun setTabBarItemRippleColor( - view: TabsHost, - value: Int?, - ) { - view.tabBarItemRippleColor = value - } - - @ReactProp(name = "tabBarItemLabelVisibilityMode") - override fun setTabBarItemLabelVisibilityMode( - view: TabsHost, - value: String?, - ) { - view.tabBarItemLabelVisibilityMode = value - } - companion object { const val REACT_CLASS = "RNSTabsHost" } diff --git a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabsScreen.kt b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabsScreen.kt index 8ebadff702..9a4d3dd9aa 100644 --- a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabsScreen.kt +++ b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabsScreen.kt @@ -30,6 +30,12 @@ class TabsScreen( internal lateinit var eventEmitter: TabsScreenEventEmitter + internal var appearance: AndroidTabsAppearance? by Delegates.observable(null) { _, oldValue, newValue -> + if (oldValue != newValue) { + tabsScreenDelegate.get()?.onAppearanceChanged(this) + } + } + var tabKey: String? = null set(value) { field = @@ -49,14 +55,6 @@ class TabsScreen( updateMenuItemAttributesIfNeeded(oldValue, newValue) } - var tabBarItemBadgeTextColor: Int? by Delegates.observable(null) { _, oldValue, newValue -> - updateMenuItemAttributesIfNeeded(oldValue, newValue) - } - - var tabBarItemBadgeBackgroundColor: Int? by Delegates.observable(null) { _, oldValue, newValue -> - updateMenuItemAttributesIfNeeded(oldValue, newValue) - } - // Accessibility var tabBarItemTestID: String? by Delegates.observable(null) { _, oldValue, newValue -> updateMenuItemAttributesIfNeeded(oldValue, newValue) diff --git a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabsScreenDelegate.kt b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabsScreenDelegate.kt index 7107a3f17f..daf1d58848 100644 --- a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabsScreenDelegate.kt +++ b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabsScreenDelegate.kt @@ -4,6 +4,8 @@ import android.content.res.Configuration import androidx.fragment.app.Fragment internal interface TabsScreenDelegate { + fun onAppearanceChanged(tabScreen: TabsScreen) + fun onTabFocusChangedFromJS( tabsScreen: TabsScreen, isFocused: Boolean, diff --git a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabsScreenViewManager.kt b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabsScreenViewManager.kt index 6fb4aa8274..637f5c76b7 100644 --- a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabsScreenViewManager.kt +++ b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabsScreenViewManager.kt @@ -1,7 +1,10 @@ package com.swmansion.rnscreens.gamma.tabs +import android.util.Log +import androidx.core.graphics.toColorInt import com.facebook.react.bridge.Dynamic import com.facebook.react.bridge.ReadableMap +import com.facebook.react.bridge.ReadableType import com.facebook.react.module.annotations.ReactModule import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.ViewGroupManager @@ -61,14 +64,6 @@ class TabsScreenViewManager : value: Dynamic, ) = Unit - @ReactProp(name = "tabBarItemBadgeBackgroundColor", customType = "Color") - override fun setTabBarItemBadgeBackgroundColor( - view: TabsScreen, - value: Int?, - ) { - view.tabBarItemBadgeBackgroundColor = value - } - override fun setIconType( view: TabsScreen?, value: String?, @@ -198,13 +193,6 @@ class TabsScreenViewManager : } // Android specific - @ReactProp(name = "tabBarItemBadgeTextColor", customType = "Color") - override fun setTabBarItemBadgeTextColor( - view: TabsScreen, - value: Int?, - ) { - view.tabBarItemBadgeTextColor = value - } @ReactProp(name = "drawableIconResourceName") override fun setDrawableIconResourceName( @@ -240,6 +228,118 @@ class TabsScreenViewManager : } } + @ReactProp(name = "standardAppearanceAndroid") + override fun setStandardAppearanceAndroid( + view: TabsScreen, + value: Dynamic?, + ) { + if (value == null || value.isNull) { + view.appearance = null + return + } + val appearanceMap = if (value.type == ReadableType.Map) value.asMap() else null + + if (appearanceMap == null) { + view.appearance = null + return + } + + view.appearance = parseAndroidTabsAppearance(appearanceMap) + } + + private fun parseAndroidTabsAppearance(appearance: ReadableMap): AndroidTabsAppearance = + AndroidTabsAppearance( + backgroundColor = appearance.getOptionalColor("tabBarBackgroundColor"), + itemRippleColor = appearance.getOptionalColor("tabBarItemRippleColor"), + labelVisibilityMode = appearance.getOptionalString("tabBarItemLabelVisibilityMode"), + itemColors = if (appearance.hasKey("itemColors")) parseBottomNavItemAppearanceStates(appearance.getMap("itemColors")) else null, + activeIndicator = + if (appearance.hasKey( + "activeIndicator", + ) + ) { + parseActiveIndicator(appearance.getMap("activeIndicator")) + } else { + null + }, + typography = if (appearance.hasKey("typography")) parseTypography(appearance.getMap("typography")) else null, + badge = if (appearance.hasKey("badge")) parseBadge(appearance.getMap("badge")) else null, + ) + + private fun parseBottomNavItemAppearanceStates(appearanceStates: ReadableMap?): BottomNavItemColors? { + if (appearanceStates == null) return null + return BottomNavItemColors( + normal = if (appearanceStates.hasKey("normal")) parseItemStateColors(appearanceStates.getMap("normal")) else null, + selected = if (appearanceStates.hasKey("selected")) parseItemStateColors(appearanceStates.getMap("selected")) else null, + focused = if (appearanceStates.hasKey("focused")) parseItemStateColors(appearanceStates.getMap("focused")) else null, + disabled = if (appearanceStates.hasKey("disabled")) parseItemStateColors(appearanceStates.getMap("disabled")) else null, + ) + } + + private fun parseItemStateColors(stateColors: ReadableMap?): ItemStateColors? { + if (stateColors == null) return null + return ItemStateColors( + titleColor = stateColors.getOptionalColor("titleColor"), + iconColor = stateColors.getOptionalColor("iconColor"), + ) + } + + private fun parseActiveIndicator(activeIndicatorConfig: ReadableMap?): ActiveIndicatorAppearance? { + if (activeIndicatorConfig == null) return null + return ActiveIndicatorAppearance( + color = activeIndicatorConfig.getOptionalColor("color"), + enabled = activeIndicatorConfig.getOptionalBoolean("enabled"), + ) + } + + private fun parseTypography(typographyConfig: ReadableMap?): TypographyAppearance? { + if (typographyConfig == null) return null + return TypographyAppearance( + fontFamily = typographyConfig.getOptionalString("fontFamily"), + fontSizeSmall = typographyConfig.getOptionalFloat("fontSizeSmall"), + fontSizeLarge = typographyConfig.getOptionalFloat("fontSizeLarge"), + fontWeight = typographyConfig.getOptionalString("fontWeight"), + fontStyle = typographyConfig.getOptionalString("fontStyle"), + ) + } + + private fun parseBadge(badgeConfig: ReadableMap?): BadgeAppearance? { + if (badgeConfig == null) return null + return BadgeAppearance( + backgroundColor = badgeConfig.getOptionalColor("backgroundColor"), + textColor = badgeConfig.getOptionalColor("textColor"), + ) + } + + private fun ReadableMap.getOptionalBoolean(key: String): Boolean? { + if (!hasKey(key) || isNull(key)) return null + return if (getType(key) == ReadableType.Boolean) getBoolean(key) else null + } + + private fun ReadableMap.getOptionalString(key: String): String? { + if (!hasKey(key) || isNull(key)) return null + return if (getType(key) == ReadableType.String) getString(key) else null + } + + private fun ReadableMap.getOptionalFloat(key: String): Float? { + if (!hasKey(key) || isNull(key)) return null + return if (getType(key) == ReadableType.Number) getDouble(key).toFloat() else null + } + + private fun ReadableMap.getOptionalColor(key: String): Int? { + if (!hasKey(key) || isNull(key)) return null + return try { + when (getType(key)) { + ReadableType.Number -> getInt(key) + ReadableType.String -> getString(key)?.toColorInt() + else -> null + } + } catch (e: Exception) { + Log.w("RNScreens", "[RNScreens] Could not parse color for key '$key': ${e.message}") + null + } + } + companion object { const val REACT_CLASS = "RNSTabsScreen" } diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/RNSTabsHostManagerDelegate.java b/android/src/paper/java/com/facebook/react/viewmanagers/RNSTabsHostManagerDelegate.java index 72fb8b2ec6..fbdd6869c7 100644 --- a/android/src/paper/java/com/facebook/react/viewmanagers/RNSTabsHostManagerDelegate.java +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNSTabsHostManagerDelegate.java @@ -30,48 +30,6 @@ public void setProperty(T view, String propName, @Nullable Object value) { case "nativeContainerBackgroundColor": mViewManager.setNativeContainerBackgroundColor(view, ColorPropConverter.getColor(value, view.getContext())); break; - case "tabBarBackgroundColor": - mViewManager.setTabBarBackgroundColor(view, ColorPropConverter.getColor(value, view.getContext())); - break; - case "tabBarItemTitleFontFamily": - mViewManager.setTabBarItemTitleFontFamily(view, value == null ? null : (String) value); - break; - case "tabBarItemTitleFontSize": - mViewManager.setTabBarItemTitleFontSize(view, value == null ? 0f : ((Double) value).floatValue()); - break; - case "tabBarItemTitleFontSizeActive": - mViewManager.setTabBarItemTitleFontSizeActive(view, value == null ? 0f : ((Double) value).floatValue()); - break; - case "tabBarItemTitleFontWeight": - mViewManager.setTabBarItemTitleFontWeight(view, value == null ? null : (String) value); - break; - case "tabBarItemTitleFontStyle": - mViewManager.setTabBarItemTitleFontStyle(view, value == null ? null : (String) value); - break; - case "tabBarItemTitleFontColor": - mViewManager.setTabBarItemTitleFontColor(view, ColorPropConverter.getColor(value, view.getContext())); - break; - case "tabBarItemTitleFontColorActive": - mViewManager.setTabBarItemTitleFontColorActive(view, ColorPropConverter.getColor(value, view.getContext())); - break; - case "tabBarItemIconColor": - mViewManager.setTabBarItemIconColor(view, ColorPropConverter.getColor(value, view.getContext())); - break; - case "tabBarItemIconColorActive": - mViewManager.setTabBarItemIconColorActive(view, ColorPropConverter.getColor(value, view.getContext())); - break; - case "tabBarItemActiveIndicatorColor": - mViewManager.setTabBarItemActiveIndicatorColor(view, ColorPropConverter.getColor(value, view.getContext())); - break; - case "tabBarItemActiveIndicatorEnabled": - mViewManager.setTabBarItemActiveIndicatorEnabled(view, value == null ? true : (boolean) value); - break; - case "tabBarItemRippleColor": - mViewManager.setTabBarItemRippleColor(view, ColorPropConverter.getColor(value, view.getContext())); - break; - case "tabBarItemLabelVisibilityMode": - mViewManager.setTabBarItemLabelVisibilityMode(view, (String) value); - break; case "tabBarTintColor": mViewManager.setTabBarTintColor(view, ColorPropConverter.getColor(value, view.getContext())); break; diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/RNSTabsHostManagerInterface.java b/android/src/paper/java/com/facebook/react/viewmanagers/RNSTabsHostManagerInterface.java index 47d6146bb9..5585d74fcf 100644 --- a/android/src/paper/java/com/facebook/react/viewmanagers/RNSTabsHostManagerInterface.java +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNSTabsHostManagerInterface.java @@ -16,20 +16,6 @@ public interface RNSTabsHostManagerInterface extends ViewManagerWithGeneratedInterface { void setTabBarHidden(T view, boolean value); void setNativeContainerBackgroundColor(T view, @Nullable Integer value); - void setTabBarBackgroundColor(T view, @Nullable Integer value); - void setTabBarItemTitleFontFamily(T view, @Nullable String value); - void setTabBarItemTitleFontSize(T view, float value); - void setTabBarItemTitleFontSizeActive(T view, float value); - void setTabBarItemTitleFontWeight(T view, @Nullable String value); - void setTabBarItemTitleFontStyle(T view, @Nullable String value); - void setTabBarItemTitleFontColor(T view, @Nullable Integer value); - void setTabBarItemTitleFontColorActive(T view, @Nullable Integer value); - void setTabBarItemIconColor(T view, @Nullable Integer value); - void setTabBarItemIconColorActive(T view, @Nullable Integer value); - void setTabBarItemActiveIndicatorColor(T view, @Nullable Integer value); - void setTabBarItemActiveIndicatorEnabled(T view, boolean value); - void setTabBarItemRippleColor(T view, @Nullable Integer value); - void setTabBarItemLabelVisibilityMode(T view, @Nullable String value); void setTabBarTintColor(T view, @Nullable Integer value); void setTabBarMinimizeBehavior(T view, @Nullable String value); void setTabBarControllerMode(T view, @Nullable String value); diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/RNSTabsScreenManagerDelegate.java b/android/src/paper/java/com/facebook/react/viewmanagers/RNSTabsScreenManagerDelegate.java index 841e261106..da361605b2 100644 --- a/android/src/paper/java/com/facebook/react/viewmanagers/RNSTabsScreenManagerDelegate.java +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNSTabsScreenManagerDelegate.java @@ -11,7 +11,6 @@ import android.view.View; import androidx.annotation.Nullable; -import com.facebook.react.bridge.ColorPropConverter; import com.facebook.react.bridge.DynamicFromObject; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.uimanager.BaseViewManager; @@ -56,11 +55,8 @@ public void setProperty(T view, String propName, @Nullable Object value) { case "imageIconResource": mViewManager.setImageIconResource(view, (ReadableMap) value); break; - case "tabBarItemBadgeTextColor": - mViewManager.setTabBarItemBadgeTextColor(view, ColorPropConverter.getColor(value, view.getContext())); - break; - case "tabBarItemBadgeBackgroundColor": - mViewManager.setTabBarItemBadgeBackgroundColor(view, ColorPropConverter.getColor(value, view.getContext())); + case "standardAppearanceAndroid": + mViewManager.setStandardAppearanceAndroid(view, new DynamicFromObject(value)); break; case "standardAppearance": mViewManager.setStandardAppearance(view, new DynamicFromObject(value)); diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/RNSTabsScreenManagerInterface.java b/android/src/paper/java/com/facebook/react/viewmanagers/RNSTabsScreenManagerInterface.java index 923f7f0835..40e76a1c3d 100644 --- a/android/src/paper/java/com/facebook/react/viewmanagers/RNSTabsScreenManagerInterface.java +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNSTabsScreenManagerInterface.java @@ -26,8 +26,7 @@ public interface RNSTabsScreenManagerInterface extends ViewManag void setOrientation(T view, @Nullable String value); void setDrawableIconResourceName(T view, @Nullable String value); void setImageIconResource(T view, @Nullable ReadableMap value); - void setTabBarItemBadgeTextColor(T view, @Nullable Integer value); - void setTabBarItemBadgeBackgroundColor(T view, @Nullable Integer value); + void setStandardAppearanceAndroid(T view, Dynamic value); void setStandardAppearance(T view, Dynamic value); void setScrollEdgeAppearance(T view, Dynamic value); void setIconType(T view, @Nullable String value); diff --git a/apps/src/tests/issue-tests/TestBottomTabs/index.tsx b/apps/src/tests/issue-tests/TestBottomTabs/index.tsx index 31b096f684..f1e0102cd8 100644 --- a/apps/src/tests/issue-tests/TestBottomTabs/index.tsx +++ b/apps/src/tests/issue-tests/TestBottomTabs/index.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { enableFreeze } from 'react-native-screens'; +import { AndroidTabsAppearance, enableFreeze } from 'react-native-screens'; import ConfigWrapperContext, { type Configuration, DEFAULT_GLOBAL_CONFIGURATION, @@ -16,9 +16,45 @@ import { internalEnableDetailedBottomTabsLogging } from 'react-native-screens/pr enableFreeze(true); internalEnableDetailedBottomTabsLogging(); +const DEFAULT_APPEARANCE_ANDROID: AndroidTabsAppearance = { + backgroundColor: Colors.NavyLight100, + itemRippleColor: Colors.WhiteTransparentDark, + labelVisibilityMode: 'auto', + itemColors: { + normal: { + iconColor: Colors.BlueLight100, + titleColor: Colors.BlueLight40, + }, + selected: { + iconColor: Colors.GreenLight100, + titleColor: Colors.GreenLight40, + }, + focused: { + iconColor: Colors.YellowDark100, + titleColor: Colors.YellowDark40, + } + }, + activeIndicator: { + enabled: true, + color: Colors.GreenLight40, + }, + typography: { + fontSizeSmall: 10, + fontSizeLarge: 16, + fontFamily: 'monospace', + fontStyle: 'italic', + fontWeight: 700, + }, + badge: { + textColor: Colors.RedDark120, + backgroundColor: Colors.RedDark40, + } +} + const TAB_CONFIGS: TabConfiguration[] = [ { tabScreenProps: { + standardAppearanceAndroid: DEFAULT_APPEARANCE_ANDROID, scrollEdgeAppearance: { tabBarBackgroundColor: Colors.NavyLight100, stacked: { @@ -63,6 +99,28 @@ const TAB_CONFIGS: TabConfiguration[] = [ accessibilityLabel: 'Second Tab Screen', tabBarItemTestID: 'tab-item-2-id', tabBarItemAccessibilityLabel: 'Second Tab Item', + standardAppearanceAndroid: { + ...DEFAULT_APPEARANCE_ANDROID, + backgroundColor: Colors.PurpleDark100, + itemRippleColor: Colors.PurpleDark40, + itemColors: { + normal: { + iconColor: Colors.YellowDark100, + titleColor: Colors.YellowDark40, + }, + selected: { + iconColor: Colors.RedDark100, + titleColor: Colors.RedDark40, + }, + focused: { + iconColor: Colors.RedLight100, + titleColor: Colors.RedLight40, + } + }, + activeIndicator: { + color: Colors.PurpleDark120, + } + }, scrollEdgeAppearance: { tabBarBackgroundColor: Colors.NavyDark140, stacked: { @@ -108,7 +166,6 @@ const TAB_CONFIGS: TabConfiguration[] = [ }, }, }, - tabBarItemBadgeBackgroundColor: Colors.GreenDark100, icon: { ios: { type: 'templateSource', @@ -142,8 +199,13 @@ const TAB_CONFIGS: TabConfiguration[] = [ tabBarItemTestID: 'tab-item-3-id', tabBarItemAccessibilityLabel: 'Third Tab Item', scrollEdgeEffects: { bottom: 'hard' }, - tabBarItemBadgeBackgroundColor: Colors.RedDark40, - tabBarItemBadgeTextColor: Colors.RedDark120, + standardAppearanceAndroid: { + ...DEFAULT_APPEARANCE_ANDROID, + badge: { + textColor: Colors.GreenDark120, + backgroundColor: Colors.GreenDark40, + } + }, standardAppearance: { stacked: { normal: { @@ -180,6 +242,9 @@ const TAB_CONFIGS: TabConfiguration[] = [ accessibilityLabel: 'Fourth Tab Screen', tabBarItemTestID: 'tab-item-4-id', tabBarItemAccessibilityLabel: 'Fourth Tab Item', + standardAppearanceAndroid: { + ...DEFAULT_APPEARANCE_ANDROID, + }, icon: { ios: { type: 'sfSymbol', @@ -220,21 +285,7 @@ function App() { }}> diff --git a/src/components/tabs/TabsHost.types.ts b/src/components/tabs/TabsHost.types.ts index b4acf60a41..15e54f8caf 100644 --- a/src/components/tabs/TabsHost.types.ts +++ b/src/components/tabs/TabsHost.types.ts @@ -1,10 +1,5 @@ import { ReactNode } from 'react'; -import type { - ColorValue, - TextStyle, - NativeSyntheticEvent, - ViewProps, -} from 'react-native'; +import type { ColorValue, NativeSyntheticEvent, ViewProps } from 'react-native'; import type { TabsAccessoryEnvironment } from './TabsAccessory.types'; export type TabAccessoryComponentFactory = ( @@ -16,13 +11,6 @@ export type NativeFocusChangeEvent = { repeatedSelectionHandledBySpecialEffect: boolean; }; -// Android-specific -export type TabBarItemLabelVisibilityMode = - | 'auto' - | 'selected' - | 'labeled' - | 'unlabeled'; - // iOS-specific export type TabBarMinimizeBehavior = | 'automatic' @@ -76,116 +64,6 @@ export interface TabsHostProps { nativeContainerStyle?: TabsHostNativeContainerStyleProps; // #endregion General - // #region Android-only - /** - * @summary Specifies the background color for the entire tab bar. - * - * @platform android - */ - tabBarBackgroundColor?: ColorValue; - /** - * @summary Specifies the font family used for the title of each tab bar item. - * - * @platform android - */ - tabBarItemTitleFontFamily?: TextStyle['fontFamily']; - /** - * @summary Specifies the font size used for the title of each tab bar item. - * - * The size is represented in scale-independent pixels (sp). - * - * @platform android - */ - tabBarItemTitleFontSize?: TextStyle['fontSize']; - /** - * @summary Specifies the font size used for the title of each tab bar item in active state. - * - * The size is represented in scale-independent pixels (sp). - * - * @platform android - */ - tabBarItemTitleFontSizeActive?: TextStyle['fontSize']; - /** - * @summary Specifies the font weight used for the title of each tab bar item. - * - * @platform android - */ - tabBarItemTitleFontWeight?: TextStyle['fontWeight']; - /** - * @summary Specifies the font style used for the title of each tab bar item. - * - * @platform android - */ - tabBarItemTitleFontStyle?: TextStyle['fontStyle']; - /** - * @summary Specifies the font color used for the title of each tab bar item. - * - * @platform android - */ - tabBarItemTitleFontColor?: TextStyle['color']; - /** - * @summary Specifies the font color used for the title of each tab bar item in active state. - * - * If not provided, `tabBarItemTitleFontColor` is used. - * - * @platform android - */ - tabBarItemTitleFontColorActive?: TextStyle['color']; - /** - * @summary Specifies the icon color for each tab bar item. - * - * @platform android - */ - tabBarItemIconColor?: ColorValue; - /** - * @summary Specifies the icon color for each tab bar item in active state. - * - * If not provided, `tabBarItemIconColor` is used. - * - * @platform android - */ - tabBarItemIconColorActive?: ColorValue; - /** - * @summary Specifies the background color of the active indicator. - * - * @platform android - */ - tabBarItemActiveIndicatorColor?: ColorValue; - /** - * @summary Specifies if the active indicator should be used. - * - * @default true - * - * @platform android - */ - tabBarItemActiveIndicatorEnabled?: boolean; - /** - * @summary Specifies the color of each tab bar item's ripple effect. - * - * @platform android - */ - tabBarItemRippleColor?: ColorValue; - /** - * @summary Specifies the label visibility mode. - * - * The label visibility mode defines when the labels of each item bar should be displayed. - * - * The following values are available: - * - `auto` - the label behaves as in “labeled” mode when there are 3 items or less, or as in “selected” mode when there are 4 items or more - * - `selected` - the label is only shown on the selected navigation item - * - `labeled` - the label is shown on all navigation items - * - `unlabeled` - the label is hidden for all navigation items - * - * The supported values correspond to the official Material Components documentation: - * @see {@link https://github.com/material-components/material-components-android/blob/master/docs/components/BottomNavigation.md#making-navigation-bar-accessible|Material Components documentation} - * - * @default auto - * - * @platform android - */ - tabBarItemLabelVisibilityMode?: TabBarItemLabelVisibilityMode; - // #endregion Android-only - // #region iOS-only /** * @summary Specifies the color used for selected tab's text and icon color. diff --git a/src/components/tabs/TabsScreen.tsx b/src/components/tabs/TabsScreen.tsx index a147810f7a..843f21bae5 100644 --- a/src/components/tabs/TabsScreen.tsx +++ b/src/components/tabs/TabsScreen.tsx @@ -17,6 +17,7 @@ import TabsScreenNativeComponent, { type IconType, type NativeProps, type Appearance, + type AppearanceAndroid, type ItemAppearance, type ItemStateAppearance, } from '../../fabric/tabs/TabsScreenNativeComponent'; @@ -27,6 +28,12 @@ import type { TabsScreenItemStateAppearance, TabsScreenProps, EmptyObject, + AndroidTabsAppearance, + BottomNavItemColorsAndroid, + ItemStateColorsAndroid, + ActiveIndicatorAppearanceAndroid, + TypographyAppearanceAndroid, + BadgeAppearanceAndroid, } from './TabsScreen.types'; import { bottomTabsDebugLog } from '../../private/logging'; import type { @@ -63,6 +70,7 @@ function TabsScreen(props: TabsScreenProps) { icon, selectedIcon, standardAppearance, + standardAppearanceAndroid, scrollEdgeAppearance, scrollEdgeEffects, // eslint-disable-next-line camelcase -- we use sneak case experimental prefix @@ -138,6 +146,9 @@ function TabsScreen(props: TabsScreenProps) { isFocused={isFocused} {...iconProps} standardAppearance={mapAppearanceToNativeProp(standardAppearance)} + standardAppearanceAndroid={mapAndroidAppearanceToNativeProp( + standardAppearanceAndroid, + )} scrollEdgeAppearance={mapAppearanceToNativeProp(scrollEdgeAppearance)} // @ts-ignore - This is debug only anyway ref={componentNodeRef} @@ -179,6 +190,85 @@ function mapAppearanceToNativeProp( }; } +export function mapAndroidAppearanceToNativeProp( + appearance?: AndroidTabsAppearance, +): AppearanceAndroid | undefined { + if (!appearance) return undefined; + + const { + backgroundColor, + itemRippleColor, + labelVisibilityMode, + itemColors, + activeIndicator, + typography, + badge, + } = appearance; + + return { + tabBarBackgroundColor: processColor(backgroundColor), + tabBarItemRippleColor: processColor(itemRippleColor), + tabBarItemLabelVisibilityMode: labelVisibilityMode, + itemColors: mapBottomNavItemColorsAndroid(itemColors), + activeIndicator: mapActiveIndicatorAndroid(activeIndicator), + typography: mapTypographyAndroid(typography), + badge: mapBadgeAndroid(badge), + }; +} + +function mapBottomNavItemColorsAndroid(colors?: BottomNavItemColorsAndroid) { + if (!colors) return undefined; + + return { + normal: mapItemStateColorsAndroid(colors.normal), + selected: mapItemStateColorsAndroid(colors.selected), + focused: mapItemStateColorsAndroid(colors.focused), + disabled: mapItemStateColorsAndroid(colors.disabled), + }; +} + +function mapItemStateColorsAndroid(stateColors?: ItemStateColorsAndroid) { + if (!stateColors) return undefined; + + return { + titleColor: processColor(stateColors.titleColor), + iconColor: processColor(stateColors.iconColor), + }; +} + +function mapActiveIndicatorAndroid( + indicator?: ActiveIndicatorAppearanceAndroid, +) { + if (!indicator) return undefined; + + return { + ...indicator, + color: processColor(indicator.color), + }; +} + +function mapTypographyAndroid(typography?: TypographyAppearanceAndroid) { + if (!typography) return undefined; + + return { + ...typography, + fontWeight: + typography.fontWeight !== undefined + ? String(typography.fontWeight) + : undefined, + }; +} + +function mapBadgeAndroid(badge?: BadgeAppearanceAndroid) { + if (!badge) return undefined; + + return { + ...badge, + backgroundColor: processColor(badge.backgroundColor), + textColor: processColor(badge.textColor), + }; +} + function mapItemAppearanceToNativeProp( itemAppearance?: TabsScreenItemAppearance, ): ItemAppearance | undefined { diff --git a/src/components/tabs/TabsScreen.types.ts b/src/components/tabs/TabsScreen.types.ts index 3fba9df140..dc465547bd 100644 --- a/src/components/tabs/TabsScreen.types.ts +++ b/src/components/tabs/TabsScreen.types.ts @@ -63,6 +63,13 @@ export type TabsSystemItem = | 'search' | 'topRated'; +// Android-specific +export type TabBarItemLabelVisibilityMode = + | 'auto' + | 'selected' + | 'labeled' + | 'unlabeled'; + // Currently iOS-only export type TabsScreenOrientation = | 'inherit' @@ -75,6 +82,180 @@ export type TabsScreenOrientation = | 'landscapeLeft' | 'landscapeRight'; +// Android-specific +export interface ItemStateColorsAndroid { + /** + * @summary Specifies the font color used for the title of each tab bar item. + * + * @platform android + */ + titleColor?: TextStyle['color']; + /** + * @summary Specifies the icon color for each tab bar item. + * + * @platform android + */ + iconColor?: ColorValue; +} + +export interface BottomNavItemColorsAndroid { + /** + * Specifies the tab bar item colors when it's enabled and unselected. + * + * @platform android + */ + normal?: ItemStateColorsAndroid; + /** + * Specifies the tab bar item colors when it's selected. + * Maps to Android `state_selected=true`. + * + * @platform android + */ + selected?: ItemStateColorsAndroid; + /** + * Specifies the tab bar item colors when it's focused. + * Maps to Android `state_focused=true` (Used mostly for Keyboard navigation). + * + * @platform android + */ + focused?: ItemStateColorsAndroid; + /** + * Specifies the tab bar item colors when it's disabled. + * Maps to Android `state_enabled=false`. + * + * @platform android + */ + disabled?: ItemStateColorsAndroid; +} + +export interface ActiveIndicatorAppearanceAndroid { + /** + * @summary Specifies the background color of the active indicator. + * + * @platform android + */ + color?: ColorValue; + /** + * @summary Specifies if the active indicator should be used. + * + * @default true + * + * @platform android + */ + enabled?: boolean; +} + +export interface TypographyAppearanceAndroid { + /** + * @summary Specifies the font family used for the title of each tab bar item. + * + * @platform android + */ + fontFamily?: TextStyle['fontFamily']; + /** + * @summary Specifies the font size used for the title unselected tab bar items. + * + * The size is represented in scale-independent pixels (sp). + * + * @platform android + */ + fontSizeSmall?: TextStyle['fontSize']; + /** + * @summary Specifies the font size used for the title of selected tab bar item. + * + * The size is represented in scale-independent pixels (sp). + * + * @platform android + */ + fontSizeLarge?: TextStyle['fontSize']; + /** + * @summary Specifies the font weight used for the title of each tab bar item. + * + * @platform android + */ + fontWeight?: TextStyle['fontWeight']; + /** + * @summary Specifies the font style used for the title of each tab bar item. + * + * @platform android + */ + fontStyle?: TextStyle['fontStyle']; +} + +export interface BadgeAppearanceAndroid { + /** + * @summary Specifies the background color of the badge. + * + * @platform android + */ + backgroundColor?: ColorValue; + /** + * @summary Specifies the text color of the badge. + * + * @platform android + */ + textColor?: ColorValue; +} + +export interface AndroidTabsAppearance { + /** + * @summary Specifies the background color for the entire tab bar. + * + * @platform android + */ + backgroundColor?: ColorValue; + /** + * @summary Specifies the color of each tab bar item's ripple effect. + * + * @platform android + */ + itemRippleColor?: ColorValue; + /** + * @summary Specifies the label visibility mode. + * + * The label visibility mode defines when the labels of each item bar should be displayed. + * + * The following values are available: + * - `auto` - the label behaves as in “labeled” mode when there are 3 items or less, or as in “selected” mode when there are 4 items or more + * - `selected` - the label is only shown on the selected navigation item + * - `labeled` - the label is shown on all navigation items + * - `unlabeled` - the label is hidden for all navigation items + * + * The supported values correspond to the official Material Components documentation: + * @see {@link https://github.com/material-components/material-components-android/blob/master/docs/components/BottomNavigation.md#making-navigation-bar-accessible|Material Components documentation} + * + * @default auto + * + * @platform android + */ + labelVisibilityMode?: TabBarItemLabelVisibilityMode; + + /** + * @summary Specifies the colors of the icon and title for different item states. + * + * @platform android + */ + itemColors?: BottomNavItemColorsAndroid; + /** + * @summary Specifies the appearance of the active indicator (pill shape behind the active icon). + * + * @platform android + */ + activeIndicator?: ActiveIndicatorAppearanceAndroid; + /** + * @summary Specifies the typography (font, size, weight, style) used for the title of each tab bar item. + * + * @platform android + */ + typography?: TypographyAppearanceAndroid; + /** + * @summary Specifies the appearance of the badges on the tab bar items. + * + * @platform android + */ + badge?: BadgeAppearanceAndroid; +} + // iOS-specific export interface TabsScreenAppearance { /** @@ -417,17 +598,14 @@ export interface TabsScreenProps { // #region Android-only /** - * @summary Specifies the color of the text in the badge. + * @summary Specifies the standard tab bar appearance. * - * @platform android - */ - tabBarItemBadgeTextColor?: ColorValue; - /** - * @summary Specifies the background color of the badge. + * Allows to customize the appearance depending on the tab bar item state + * (normal, selected, focused, disabled). Configuration for the Bottom Navigation View. * * @platform android */ - tabBarItemBadgeBackgroundColor?: ColorValue; + standardAppearanceAndroid?: AndroidTabsAppearance; // #endregion Android-only // #region iOS-only diff --git a/src/fabric/tabs/TabsHostNativeComponent.ts b/src/fabric/tabs/TabsHostNativeComponent.ts index 3864c414bc..d0aa74a292 100644 --- a/src/fabric/tabs/TabsHostNativeComponent.ts +++ b/src/fabric/tabs/TabsHostNativeComponent.ts @@ -15,12 +15,6 @@ type NativeFocusChangeEvent = { repeatedSelectionHandledBySpecialEffect: boolean; }; -type TabBarItemLabelVisibilityMode = - | 'auto' - | 'selected' - | 'labeled' - | 'unlabeled'; - type TabBarMinimizeBehavior = | 'automatic' | 'never' @@ -40,25 +34,6 @@ export interface NativeProps extends ViewProps { // Appearance // tabBarAppearance?: TabBarAppearance; // Does not work due to codegen issue. - // Android-specific - tabBarBackgroundColor?: ColorValue; - tabBarItemTitleFontFamily?: string; - tabBarItemTitleFontSize?: CT.Float; - tabBarItemTitleFontSizeActive?: CT.Float; - tabBarItemTitleFontWeight?: string; - tabBarItemTitleFontStyle?: string; - tabBarItemTitleFontColor?: ColorValue; - tabBarItemTitleFontColorActive?: ColorValue; - tabBarItemIconColor?: ColorValue; - tabBarItemIconColorActive?: ColorValue; - tabBarItemActiveIndicatorColor?: ColorValue; - tabBarItemActiveIndicatorEnabled?: CT.WithDefault; - tabBarItemRippleColor?: ColorValue; - tabBarItemLabelVisibilityMode?: CT.WithDefault< - TabBarItemLabelVisibilityMode, - 'auto' - >; - // iOS-specific tabBarTintColor?: ColorValue; tabBarMinimizeBehavior?: CT.WithDefault; diff --git a/src/fabric/tabs/TabsScreenNativeComponent.ts b/src/fabric/tabs/TabsScreenNativeComponent.ts index f6ce662399..ae75269cc3 100644 --- a/src/fabric/tabs/TabsScreenNativeComponent.ts +++ b/src/fabric/tabs/TabsScreenNativeComponent.ts @@ -3,7 +3,6 @@ import { codegenNativeComponent } from 'react-native'; import type { CodegenTypes as CT, - ColorValue, ImageSource, ProcessedColorValue, ViewProps, @@ -53,6 +52,56 @@ export type Appearance = { tabBarBlurEffect?: CT.WithDefault; }; +type TabBarItemLabelVisibilityMode = + | 'auto' + | 'selected' + | 'labeled' + | 'unlabeled'; + +export type ItemStateColorsAndroid = { + iconColor?: ProcessedColorValue | null; + titleColor?: ProcessedColorValue | null; +}; + +export type BottomNavItemColorsAndroid = { + normal?: ItemStateColorsAndroid; + selected?: ItemStateColorsAndroid; + focused?: ItemStateColorsAndroid; + disabled?: ItemStateColorsAndroid; +}; + +export type ActiveIndicatorAppearanceAndroid = { + color?: ProcessedColorValue | null; + enabled?: CT.WithDefault; +}; + +export type TypographyAppearanceAndroid = { + fontFamily?: string; + fontSizeSmall?: CT.Float; + fontSizeLarge?: CT.Float; + fontWeight?: string; + fontStyle?: string; +}; + +export type BadgeAppearanceAndroid = { + textColor?: ProcessedColorValue | null; + backgroundColor?: ProcessedColorValue | null; +}; + +export type AppearanceAndroid = { + tabBarBackgroundColor?: ProcessedColorValue | null; + tabBarItemRippleColor?: ProcessedColorValue | null; + tabBarItemLabelVisibilityMode?: CT.WithDefault< + TabBarItemLabelVisibilityMode, + 'auto' + >; + + itemColors?: BottomNavItemColorsAndroid; + activeIndicator?: ActiveIndicatorAppearanceAndroid; + typography?: TypographyAppearanceAndroid; + badge?: BadgeAppearanceAndroid; +}; + type BlurEffect = | 'none' | 'systemDefault' @@ -134,10 +183,11 @@ export interface NativeProps extends ViewProps { // Android-specific image handling drawableIconResourceName?: string; imageIconResource?: ImageSource; - tabBarItemBadgeTextColor?: ColorValue; - tabBarItemBadgeBackgroundColor?: ColorValue; - // iOS-specific + // Android-specific appearance + standardAppearanceAndroid?: UnsafeMixed; + + // iOS-specific appearance standardAppearance?: UnsafeMixed; scrollEdgeAppearance?: UnsafeMixed; diff --git a/src/flags.ts b/src/flags.ts index 82fae19203..332ab52f17 100644 --- a/src/flags.ts +++ b/src/flags.ts @@ -43,6 +43,13 @@ export const compatibilityFlags = { * is in use or not. */ usesNewAndroidHeaderHeightImplementation: true, + + /** + * In https://github.com/software-mansion/react-native-screens/pull/3672, we refactored + * appearance props for Tabs on Android. To allow backward compatibility, + * we expose a way to check whether the new implementation is in use or not. + */ + usesRefactoredTabsAppearanceApiAndroid: true, } as const; const _featureFlags = {