diff --git a/FabricExample/android/app/src/main/res/drawable/person_walking.xml b/FabricExample/android/app/src/main/res/drawable/person_walking.xml
new file mode 100644
index 0000000000..7cf9490d16
--- /dev/null
+++ b/FabricExample/android/app/src/main/res/drawable/person_walking.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FabricExample/android/app/src/main/res/drawable/swm_logo.xml b/FabricExample/android/app/src/main/res/drawable/swm_logo.xml
new file mode 100644
index 0000000000..de43a23b56
--- /dev/null
+++ b/FabricExample/android/app/src/main/res/drawable/swm_logo.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/FabricExample/ios/FabricExample/Images.xcassets/nano.swm.symbolset/Contents.json b/FabricExample/ios/FabricExample/Images.xcassets/nano.swm.symbolset/Contents.json
new file mode 100644
index 0000000000..6e08e083e5
--- /dev/null
+++ b/FabricExample/ios/FabricExample/Images.xcassets/nano.swm.symbolset/Contents.json
@@ -0,0 +1,15 @@
+{
+ "info": {
+ "author": "xcode",
+ "version": 1
+ },
+ "properties": {
+ "symbol-rendering-intent": "template"
+ },
+ "symbols": [
+ {
+ "filename": "nano.swm.svg",
+ "idiom": "universal"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/FabricExample/ios/FabricExample/Images.xcassets/nano.swm.symbolset/nano.swm.svg b/FabricExample/ios/FabricExample/Images.xcassets/nano.swm.symbolset/nano.swm.svg
new file mode 100644
index 0000000000..614671e8b0
--- /dev/null
+++ b/FabricExample/ios/FabricExample/Images.xcassets/nano.swm.symbolset/nano.swm.svg
@@ -0,0 +1,45 @@
+
+
diff --git a/FabricExample/ios/FabricExample/Images.xcassets/nanomc.walker.imageset/Contents.json b/FabricExample/ios/FabricExample/Images.xcassets/nanomc.walker.imageset/Contents.json
new file mode 100644
index 0000000000..405b6983dd
--- /dev/null
+++ b/FabricExample/ios/FabricExample/Images.xcassets/nanomc.walker.imageset/Contents.json
@@ -0,0 +1,16 @@
+{
+ "images": [
+ {
+ "filename": "nanomc.walker.svg",
+ "idiom": "universal"
+ }
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1
+ },
+ "properties": {
+ "preserves-vector-representation": true,
+ "template-rendering-intent": "original"
+ }
+}
\ No newline at end of file
diff --git a/FabricExample/ios/FabricExample/Images.xcassets/nanomc.walker.imageset/nanomc.walker.svg b/FabricExample/ios/FabricExample/Images.xcassets/nanomc.walker.imageset/nanomc.walker.svg
new file mode 100644
index 0000000000..883d10e8bd
--- /dev/null
+++ b/FabricExample/ios/FabricExample/Images.xcassets/nanomc.walker.imageset/nanomc.walker.svg
@@ -0,0 +1,26 @@
+
diff --git a/android/src/main/java/com/swmansion/rnscreens/helpers/NoTintDrawable.kt b/android/src/main/java/com/swmansion/rnscreens/helpers/NoTintDrawable.kt
new file mode 100644
index 0000000000..2c2e8e7a66
--- /dev/null
+++ b/android/src/main/java/com/swmansion/rnscreens/helpers/NoTintDrawable.kt
@@ -0,0 +1,18 @@
+package com.swmansion.rnscreens.helpers
+
+import android.content.res.ColorStateList
+import android.graphics.PorterDuff
+import android.graphics.drawable.Drawable
+import androidx.appcompat.graphics.drawable.DrawableWrapperCompat
+
+// Ignores tinting so the wrapped icon keeps its own colors even when a host view
+// (e.g. BottomNavigationView) applies an itemIconTintList.
+internal class NoTintDrawable(
+ drawable: Drawable,
+) : DrawableWrapperCompat(drawable) {
+ override fun setTintList(tint: ColorStateList?) = Unit
+
+ override fun setTint(tintColor: Int) = Unit
+
+ override fun setTintMode(tintMode: PorterDuff.Mode?) = Unit
+}
diff --git a/android/src/main/java/com/swmansion/rnscreens/tabs/appearance/TabsAppearanceApplicator.kt b/android/src/main/java/com/swmansion/rnscreens/tabs/appearance/TabsAppearanceApplicator.kt
index 790428e886..10cc9835f4 100644
--- a/android/src/main/java/com/swmansion/rnscreens/tabs/appearance/TabsAppearanceApplicator.kt
+++ b/android/src/main/java/com/swmansion/rnscreens/tabs/appearance/TabsAppearanceApplicator.kt
@@ -3,6 +3,8 @@ package com.swmansion.rnscreens.tabs.appearance
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.ColorStateList
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.InsetDrawable
import android.graphics.drawable.StateListDrawable
import android.util.TypedValue
import android.view.MenuItem
@@ -16,12 +18,83 @@ import com.google.android.material.R
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.navigation.NavigationBarView
import com.swmansion.rnscreens.tabs.screen.TabsScreen
+import com.swmansion.rnscreens.utils.dpToPx
+import com.swmansion.rnscreens.utils.pxToDp
import com.swmansion.rnscreens.utils.resolveColorAttr
@SuppressLint("PrivateResource") // We want to use variables from material design for default values
internal class TabsAppearanceApplicator(
private val bottomNavigationView: BottomNavigationView,
) {
+ // Resolved on each access: tracks the material library version and the display density.
+ internal val defaultIconSizeDp: Float
+ get() =
+ bottomNavigationView.pxToDp(
+ bottomNavigationView.resources.getDimension(R.dimen.mtrl_navigation_bar_item_default_icon_size),
+ )
+
+ internal fun effectiveIconSizeDp(tabsScreen: TabsScreen): Float =
+ if (tabsScreen.drawableIconSize > 0f) tabsScreen.drawableIconSize else defaultIconSizeDp
+
+ // Material allows only one icon size for all items; iconBoxDp is the largest effective per-tab size.
+ fun applyIconBox(iconBoxDp: Float) {
+ bottomNavigationView.itemIconSize = bottomNavigationView.dpToPx(iconBoxDp).toInt()
+ }
+
+ // Resolved on each access: tracks the material library version and the display density.
+ private val defaultIndicatorWidthPx: Int
+ get() = bottomNavigationView.resources.getDimensionPixelSize(R.dimen.m3_bottom_nav_item_active_indicator_width)
+
+ private val defaultIndicatorHeightPx: Int
+ get() = bottomNavigationView.resources.getDimensionPixelSize(R.dimen.m3_bottom_nav_item_active_indicator_height)
+
+ // Auto-scale preserves the themed default padding around the icon: default indicator minus default icon size.
+ private val autoIndicatorHorizontalPaddingDp: Float
+ get() = bottomNavigationView.pxToDp(defaultIndicatorWidthPx.toFloat()) - defaultIconSizeDp
+
+ private val autoIndicatorVerticalPaddingDp: Float
+ get() = bottomNavigationView.pxToDp(defaultIndicatorHeightPx.toFloat()) - defaultIconSizeDp
+
+ private fun applyActiveIndicatorSize(
+ widthDp: Float?,
+ heightDp: Float?,
+ iconBoxDp: Float,
+ ) {
+ val autoScale = iconBoxDp > defaultIconSizeDp
+ bottomNavigationView.itemActiveIndicatorWidth =
+ resolveIndicatorDimensionPx(widthDp, iconBoxDp + autoIndicatorHorizontalPaddingDp, defaultIndicatorWidthPx, autoScale)
+ bottomNavigationView.itemActiveIndicatorHeight =
+ resolveIndicatorDimensionPx(heightDp, iconBoxDp + autoIndicatorVerticalPaddingDp, defaultIndicatorHeightPx, autoScale)
+ }
+
+ // Explicit dp wins; else auto-scale to the enlarged icon box; else themed Material default.
+ private fun resolveIndicatorDimensionPx(
+ explicitDp: Float?,
+ autoScaledDp: Float,
+ defaultPx: Int,
+ autoScale: Boolean,
+ ): Int =
+ when {
+ explicitDp != null && explicitDp > 0f -> bottomNavigationView.dpToPx(explicitDp).toInt()
+ autoScale -> bottomNavigationView.dpToPx(autoScaledDp).toInt()
+ else -> defaultPx
+ }
+
+ // Inset the icon so it renders at effectiveDp, centered within iconBoxDp.
+ // Intrinsic-relative on purpose: Material FIT_CENTER-scales the drawable to the icon box,
+ // so the intrinsic factor cancels and the glyph lands at effectiveDp/iconBoxDp of the box.
+ private fun sizeIcon(
+ icon: Drawable?,
+ effectiveDp: Float,
+ iconBoxDp: Float,
+ ): Drawable? {
+ if (icon == null || effectiveDp >= iconBoxDp) return icon
+ val larger = maxOf(icon.intrinsicWidth, icon.intrinsicHeight)
+ if (larger <= 0) return icon
+ val insetPx = (larger * (iconBoxDp - effectiveDp) / (2f * effectiveDp)).toInt()
+ return if (insetPx > 0) InsetDrawable(icon, insetPx) else icon
+ }
+
private val states =
arrayOf(
intArrayOf(-android.R.attr.state_enabled), // disabled
@@ -34,6 +107,7 @@ internal class TabsAppearanceApplicator(
context: Context,
tabBarAppearance: TabsAppearance?,
isTabBarHidden: Boolean,
+ iconBoxDp: Float,
) {
bottomNavigationView.isVisible = !isTabBarHidden
bottomNavigationView.setBackgroundColor(
@@ -109,6 +183,12 @@ internal class TabsAppearanceApplicator(
bottomNavigationView.isItemActiveIndicatorEnabled =
tabBarAppearance?.tabBarItemActiveIndicatorEnabled ?: true
bottomNavigationView.itemActiveIndicatorColor = ColorStateList.valueOf(activeIndicatorColor)
+
+ applyActiveIndicatorSize(
+ tabBarAppearance?.tabBarItemActiveIndicatorWidth,
+ tabBarAppearance?.tabBarItemActiveIndicatorHeight,
+ iconBoxDp,
+ )
}
fun updateFontStyles(
@@ -174,24 +254,26 @@ internal class TabsAppearanceApplicator(
fun updateMenuItemAppearance(
menuItem: MenuItem,
tabsScreen: TabsScreen,
+ iconBoxDp: Float,
) {
if (menuItem.title != tabsScreen.tabTitle) {
menuItem.title = tabsScreen.tabTitle
}
- val targetIcon =
- if (tabsScreen.selectedIcon != null && tabsScreen.icon != null) {
+ // Sized per slot: a StateListDrawable's intrinsic size follows its current state, so a
+ // single inset computed from one child would mis-size the other when their resolutions differ.
+ val effectiveDp = effectiveIconSizeDp(tabsScreen)
+ val iconDrawable = sizeIcon(tabsScreen.icon.drawable, effectiveDp, iconBoxDp)
+ val selectedIconDrawable = sizeIcon(tabsScreen.selectedIcon.drawable, effectiveDp, iconBoxDp)
+ menuItem.icon =
+ if (selectedIconDrawable != null && iconDrawable != null) {
StateListDrawable().apply {
- addState(intArrayOf(android.R.attr.state_checked), tabsScreen.selectedIcon?.mutate())
- addState(intArrayOf(), tabsScreen.icon?.mutate())
+ addState(intArrayOf(android.R.attr.state_checked), selectedIconDrawable.mutate())
+ addState(intArrayOf(), iconDrawable.mutate())
}
} else {
- tabsScreen.icon
+ iconDrawable
}
-
- if (menuItem.icon != targetIcon) {
- menuItem.icon = targetIcon
- }
}
internal fun updateBadgeAppearance(
diff --git a/android/src/main/java/com/swmansion/rnscreens/tabs/appearance/TabsAppearanceCoordinator.kt b/android/src/main/java/com/swmansion/rnscreens/tabs/appearance/TabsAppearanceCoordinator.kt
index 9b46e00169..dbf39b851e 100644
--- a/android/src/main/java/com/swmansion/rnscreens/tabs/appearance/TabsAppearanceCoordinator.kt
+++ b/android/src/main/java/com/swmansion/rnscreens/tabs/appearance/TabsAppearanceCoordinator.kt
@@ -14,19 +14,27 @@ internal class TabsAppearanceCoordinator(
) {
private val appearanceApplicator = TabsAppearanceApplicator(bottomNavigationView)
+ // Icon box is bar-wide: the largest effective size across tabs.
+ private fun resolveIconBoxDp(): Float =
+ tabsScreenFragments.maxOfOrNull { appearanceApplicator.effectiveIconSizeDp(it.tabsScreen) }
+ ?: appearanceApplicator.defaultIconSizeDp
+
fun updateTabAppearance(
context: Context,
tabsContainer: TabsContainer,
) {
val selectedTabAppearance = tabsContainer.selectedTab.tabsScreen.appearance
- appearanceApplicator.updateSharedAppearance(context, selectedTabAppearance, tabsContainer.tabBarHidden)
- updateMenuItems(context, selectedTabAppearance)
+ val iconBoxDp = resolveIconBoxDp()
+ appearanceApplicator.applyIconBox(iconBoxDp)
+ appearanceApplicator.updateSharedAppearance(context, selectedTabAppearance, tabsContainer.tabBarHidden, iconBoxDp)
+ updateMenuItems(context, selectedTabAppearance, iconBoxDp)
appearanceApplicator.updateFontStyles(context, selectedTabAppearance) // It needs to be updated after updateMenuItems
}
private fun updateMenuItems(
context: Context,
tabsAppearance: TabsAppearance?,
+ iconBoxDp: Float,
) {
tabsScreenFragments.forEachIndexed { index, fragment ->
val menuItemId = menuItemIdForFragmentAtIndex(index)
@@ -35,7 +43,7 @@ internal class TabsAppearanceCoordinator(
"[RNScreens] Missing MenuItem for id: $menuItemId"
}
check(menuItem.itemId == menuItemId) { "[RNScreens] Illegal state: menu items are shuffled" }
- updateMenuItemAppearance(context, menuItem, fragment.tabsScreen, tabsAppearance)
+ updateMenuItemAppearance(context, menuItem, fragment.tabsScreen, tabsAppearance, iconBoxDp)
}
}
@@ -44,8 +52,9 @@ internal class TabsAppearanceCoordinator(
menuItem: MenuItem,
tabsScreen: TabsScreen,
appearance: TabsAppearance?,
+ iconBoxDp: Float = resolveIconBoxDp(),
) {
- appearanceApplicator.updateMenuItemAppearance(menuItem, tabsScreen)
+ appearanceApplicator.updateMenuItemAppearance(menuItem, tabsScreen, iconBoxDp)
appearanceApplicator.updateBadgeAppearance(context, menuItem, tabsScreen, appearance)
}
}
diff --git a/android/src/main/java/com/swmansion/rnscreens/tabs/appearance/TabsAppearanceModel.kt b/android/src/main/java/com/swmansion/rnscreens/tabs/appearance/TabsAppearanceModel.kt
index 7fd509f60b..a87f61f5fd 100644
--- a/android/src/main/java/com/swmansion/rnscreens/tabs/appearance/TabsAppearanceModel.kt
+++ b/android/src/main/java/com/swmansion/rnscreens/tabs/appearance/TabsAppearanceModel.kt
@@ -10,6 +10,8 @@ internal data class TabsAppearance(
val disabled: ItemStateAppearance? = null,
val tabBarItemActiveIndicatorColor: Int? = null,
val tabBarItemActiveIndicatorEnabled: Boolean? = null,
+ val tabBarItemActiveIndicatorWidth: Float? = null,
+ val tabBarItemActiveIndicatorHeight: Float? = null,
val tabBarItemTitleFontFamily: String? = null,
val tabBarItemTitleSmallLabelFontSize: Float? = null,
val tabBarItemTitleLargeLabelFontSize: Float? = null,
diff --git a/android/src/main/java/com/swmansion/rnscreens/tabs/container/TabsContainer.kt b/android/src/main/java/com/swmansion/rnscreens/tabs/container/TabsContainer.kt
index a425e63c71..9fa4ec5308 100644
--- a/android/src/main/java/com/swmansion/rnscreens/tabs/container/TabsContainer.kt
+++ b/android/src/main/java/com/swmansion/rnscreens/tabs/container/TabsContainer.kt
@@ -420,6 +420,17 @@ class TabsContainer internal constructor(
}
}
+ override fun onIconSizeChange(tabsScreen: TabsScreen) {
+ // Icon box is bar-wide, so this must run even when the changed tab is not selected.
+ // Skip when detached; reattachment reapplies the full appearance anyway.
+ if (isAttachedToWindow) {
+ invalidationFlags.isNavigationMenuAppearanceInvalidated = true
+ post {
+ this.flushPendingUpdates()
+ }
+ }
+ }
+
override fun getFragmentForTabsScreen(tabsScreen: TabsScreen): TabsScreenFragment? =
tabsModel.find {
it.tabsScreen ===
diff --git a/android/src/main/java/com/swmansion/rnscreens/tabs/screen/TabsScreen.kt b/android/src/main/java/com/swmansion/rnscreens/tabs/screen/TabsScreen.kt
index b8bec2bfc2..49f7386b0b 100644
--- a/android/src/main/java/com/swmansion/rnscreens/tabs/screen/TabsScreen.kt
+++ b/android/src/main/java/com/swmansion/rnscreens/tabs/screen/TabsScreen.kt
@@ -1,7 +1,6 @@
package com.swmansion.rnscreens.tabs.screen
import android.content.res.Configuration
-import android.graphics.drawable.Drawable
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.facebook.react.uimanager.ThemedReactContext
@@ -9,7 +8,6 @@ import com.swmansion.rnscreens.common.FragmentProviding
import com.swmansion.rnscreens.common.container.Container
import com.swmansion.rnscreens.common.container.ContainerItem
import com.swmansion.rnscreens.common.container.ContainerItemSupport
-import com.swmansion.rnscreens.helpers.getSystemDrawableResource
import com.swmansion.rnscreens.scrollviewmarker.ScrollViewMarker
import com.swmansion.rnscreens.scrollviewmarker.ScrollViewSeeking
import com.swmansion.rnscreens.tabs.appearance.TabsAppearance
@@ -78,24 +76,21 @@ class TabsScreen(
// endregion
// region Icon
- var drawableIconResourceName: String? by Delegates.observable(null) { _, oldValue, newValue ->
- if (newValue != oldValue) {
- icon = getSystemDrawableResource(reactContext, newValue)
- }
- }
- var selectedDrawableIconResourceName: String? by Delegates.observable(null) { _, oldValue, newValue ->
- if (newValue != oldValue) {
- selectedIcon = getSystemDrawableResource(reactContext, newValue)
- }
- }
+ internal val icon = TabsScreenIcon(reactContext, ::onMenuItemAttributesChange)
+ internal val selectedIcon = TabsScreenIcon(reactContext, ::onMenuItemAttributesChange)
- var icon: Drawable? by Delegates.observable(null) { _, oldValue, newValue ->
- updateMenuItemAttributesIfNeeded(oldValue, newValue)
+ internal fun resolveIconsIfNeeded() {
+ icon.resolveIfNeeded()
+ selectedIcon.resolveIfNeeded()
}
- var selectedIcon: Drawable? by Delegates.observable(null) { _, oldValue, newValue ->
- updateMenuItemAttributesIfNeeded(oldValue, newValue)
+ // Per-tab icon size in dp; 0 means the system default.
+ // The icon box is bar-wide, so a change here invalidates the whole bar, not just this item.
+ var drawableIconSize: Float by Delegates.observable(0f) { _, oldValue, newValue ->
+ if (newValue != oldValue) {
+ tabsScreenDelegate.get()?.onIconSizeChange(this)
+ }
}
// endregion
diff --git a/android/src/main/java/com/swmansion/rnscreens/tabs/screen/TabsScreenDelegate.kt b/android/src/main/java/com/swmansion/rnscreens/tabs/screen/TabsScreenDelegate.kt
index efc776c63a..567f65ef94 100644
--- a/android/src/main/java/com/swmansion/rnscreens/tabs/screen/TabsScreenDelegate.kt
+++ b/android/src/main/java/com/swmansion/rnscreens/tabs/screen/TabsScreenDelegate.kt
@@ -8,6 +8,12 @@ internal interface TabsScreenDelegate {
fun onMenuItemAttributesChange(tabsScreen: TabsScreen)
+ /**
+ * The icon box is bar-wide, so an icon size change requires a whole-bar appearance
+ * update regardless of tab selection.
+ */
+ fun onIconSizeChange(tabsScreen: TabsScreen)
+
/**
* **If a fragment is associated with the tab screen**, notify the delegate that the fragment
* got configuration update.
diff --git a/android/src/main/java/com/swmansion/rnscreens/tabs/screen/TabsScreenIcon.kt b/android/src/main/java/com/swmansion/rnscreens/tabs/screen/TabsScreenIcon.kt
new file mode 100644
index 0000000000..66e22467b6
--- /dev/null
+++ b/android/src/main/java/com/swmansion/rnscreens/tabs/screen/TabsScreenIcon.kt
@@ -0,0 +1,69 @@
+package com.swmansion.rnscreens.tabs.screen
+
+import android.content.Context
+import android.graphics.drawable.Drawable
+import com.swmansion.rnscreens.helpers.IconResolution
+import com.swmansion.rnscreens.helpers.NoTintDrawable
+import com.swmansion.rnscreens.helpers.PropIconResolver
+import kotlin.properties.Delegates
+
+/**
+ * Single icon slot (normal or selected) of a tab bar item. Props stage here and may arrive
+ * in any order within a single update batch; resolution runs once per transaction via
+ * [resolveIfNeeded]. [onChanged] fires only when the resolved [drawable] actually changes.
+ */
+internal class TabsScreenIcon(
+ private val context: Context,
+ private val onChanged: () -> Unit,
+) {
+ var drawableResourceName: String? by Delegates.observable(null) { _, oldValue, newValue ->
+ if (newValue != oldValue) isInvalidated = true
+ }
+
+ var tinted: Boolean by Delegates.observable(true) { _, oldValue, newValue ->
+ if (newValue != oldValue) isInvalidated = true
+ }
+
+ var imageUri: String? by Delegates.observable(null) { _, oldValue, newValue ->
+ if (newValue != oldValue) isInvalidated = true
+ }
+
+ // Resolved output; read by the appearance applicator.
+ var drawable: Drawable? = null
+ private set
+
+ private var isInvalidated = false
+
+ // Handles source precedence (drawable name wins over uri), dedup, and stale async image drops.
+ private val resolver = PropIconResolver()
+
+ // Kept unwrapped so a tint-only change re-wraps without reloading.
+ private var rawDrawable: Drawable? = null
+ private var appliedTinted = true
+
+ fun resolveIfNeeded() {
+ if (!isInvalidated) {
+ return
+ }
+ isInvalidated = false
+ resolver.resolve(context, drawableResourceName, imageUri) { result ->
+ when (result) {
+ IconResolution.Unchanged -> if (tinted != appliedTinted) emit()
+ is IconResolution.Resolved -> {
+ rawDrawable = result.drawable
+ emit()
+ }
+ }
+ }
+ }
+
+ // Tint is read at emit time, so a toggle during an in-flight image load still applies.
+ private fun emit() {
+ appliedTinted = tinted
+ val next = rawDrawable?.let { if (tinted) it else NoTintDrawable(it) }
+ if (next !== drawable) {
+ drawable = next
+ onChanged()
+ }
+ }
+}
diff --git a/android/src/main/java/com/swmansion/rnscreens/tabs/screen/TabsScreenViewManager.kt b/android/src/main/java/com/swmansion/rnscreens/tabs/screen/TabsScreenViewManager.kt
index 41b023feb0..40bfcd7d83 100644
--- a/android/src/main/java/com/swmansion/rnscreens/tabs/screen/TabsScreenViewManager.kt
+++ b/android/src/main/java/com/swmansion/rnscreens/tabs/screen/TabsScreenViewManager.kt
@@ -8,7 +8,6 @@ import com.facebook.react.uimanager.ViewGroupManager
import com.facebook.react.uimanager.ViewManagerDelegate
import com.facebook.react.viewmanagers.RNSTabsScreenAndroidManagerDelegate
import com.facebook.react.viewmanagers.RNSTabsScreenAndroidManagerInterface
-import com.swmansion.rnscreens.helpers.loadImage
import com.swmansion.rnscreens.helpers.makeEventRegistrationInfo
import com.swmansion.rnscreens.helpers.readOptionalBoolean
import com.swmansion.rnscreens.helpers.readOptionalColor
@@ -55,6 +54,11 @@ class TabsScreenViewManager :
view.onViewManagerAddEventEmitters()
}
+ override fun onAfterUpdateTransaction(view: TabsScreen) {
+ super.onAfterUpdateTransaction(view)
+ view.resolveIconsIfNeeded()
+ }
+
override fun setScreenKey(
view: TabsScreen,
value: String?,
@@ -125,42 +129,49 @@ class TabsScreenViewManager :
view: TabsScreen,
value: String?,
) {
- view.drawableIconResourceName = value
+ view.icon.drawableResourceName = value
}
override fun setSelectedDrawableIconResourceName(
view: TabsScreen,
value: String?,
) {
- view.selectedDrawableIconResourceName = value
+ view.selectedIcon.drawableResourceName = value
+ }
+
+ override fun setDrawableIconTinted(
+ view: TabsScreen,
+ value: Boolean,
+ ) {
+ view.icon.tinted = value
+ }
+
+ override fun setSelectedDrawableIconTinted(
+ view: TabsScreen,
+ value: Boolean,
+ ) {
+ view.selectedIcon.tinted = value
+ }
+
+ override fun setDrawableIconSize(
+ view: TabsScreen,
+ value: Float,
+ ) {
+ view.drawableIconSize = value
}
override fun setImageIconResource(
view: TabsScreen,
value: ReadableMap?,
) {
- val uri = value?.getString("uri")
- if (uri != null) {
- loadImage(view.context, uri) { drawable ->
- if (drawable != null) {
- view.icon = drawable
- }
- }
- }
+ view.icon.imageUri = value?.getString("uri")
}
override fun setSelectedImageIconResource(
view: TabsScreen,
value: ReadableMap?,
) {
- val uri = value?.getString("uri")
- if (uri != null) {
- loadImage(view.context, uri) { drawable ->
- if (drawable != null) {
- view.selectedIcon = drawable
- }
- }
- }
+ view.selectedIcon.imageUri = value?.getString("uri")
}
override fun setStandardAppearance(
@@ -189,6 +200,8 @@ class TabsScreenViewManager :
disabled = if (appearance.hasKey("disabled")) parseItemStateAppearance(context, appearance.getMap("disabled")) else null,
tabBarItemActiveIndicatorColor = appearance.readOptionalColor(context, "tabBarItemActiveIndicatorColor"),
tabBarItemActiveIndicatorEnabled = appearance.readOptionalBoolean("tabBarItemActiveIndicatorEnabled"),
+ tabBarItemActiveIndicatorWidth = appearance.readOptionalFloat("tabBarItemActiveIndicatorWidth"),
+ tabBarItemActiveIndicatorHeight = appearance.readOptionalFloat("tabBarItemActiveIndicatorHeight"),
tabBarItemTitleFontFamily = appearance.readOptionalString("tabBarItemTitleFontFamily"),
tabBarItemTitleSmallLabelFontSize = appearance.readOptionalFloat("tabBarItemTitleSmallLabelFontSize"),
tabBarItemTitleLargeLabelFontSize = appearance.readOptionalFloat("tabBarItemTitleLargeLabelFontSize"),
diff --git a/apps/Example.tsx b/apps/Example.tsx
index d4f2dfe8a3..e535c2c21a 100644
--- a/apps/Example.tsx
+++ b/apps/Example.tsx
@@ -27,6 +27,7 @@ import SearchBar from './src/screens/SearchBar';
import Events from './src/screens/Events';
import Gestures from './src/screens/Gestures';
import BarButtonItems from './src/screens/BarButtonItems';
+import CustomNativeTabsIcons from './src/screens/CustomNativeTabsIcons';
import { GestureDetectorProvider } from 'react-native-screens/gesture-handler';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
@@ -68,6 +69,11 @@ const SCREENS: Record<
component: SwipeBackAnimation,
type: 'example',
},
+ CustomNativeTabsIcons: {
+ title: 'Custom Native Tabs Icons',
+ component: CustomNativeTabsIcons,
+ type: 'example',
+ },
StackPresentation: {
title: 'Stack Presentation',
component: StackPresentation,
diff --git a/apps/src/screens/CustomNativeTabsIcons.android.tsx b/apps/src/screens/CustomNativeTabsIcons.android.tsx
new file mode 100644
index 0000000000..a40412a80e
--- /dev/null
+++ b/apps/src/screens/CustomNativeTabsIcons.android.tsx
@@ -0,0 +1,151 @@
+import React from 'react';
+import { StyleSheet, Text, View } from 'react-native';
+import {
+ TabsContainer,
+ type TabRouteConfig,
+ DEFAULT_TAB_ROUTE_OPTIONS,
+} from '@apps/shared/containers/tabs';
+import { Colors } from '@apps/shared/styling';
+
+function TabScreen() {
+ return (
+
+ Custom drawable tab icons
+
+ OG SWM: size unaltered showcases the visual shrink due to its aspect
+ ratio.
+
+
+ Sized SWM: a wide logo sized to 44dp via `drawableIconSize`.
+
+
+ Multicolor Tint: a VectorDrawable that keeps its own colors when focused
+ (`tinted: false`) and is template(system)-tinted otherwise.
+
+
+ Sys (unaltered): a built-in star. Size unaltered defaults to 24dp.
+
+
+ Image Tint: an `imageSource` icon, tinted by default and keeping its own
+ colors when selected (`tinted: false`).
+
+
+ The active indicator is bar-wide via `tabBarItemActiveIndicatorWidth` /
+ `Height`. And shared throughout all icons
+
+
+ );
+}
+
+const INDICATOR = {
+ tabBarItemActiveIndicatorWidth: 80,
+ tabBarItemActiveIndicatorHeight: 40,
+};
+
+const ROUTES: TabRouteConfig[] = [
+ {
+ name: 'OG_SWM',
+ Component: TabScreen,
+ options: {
+ ...DEFAULT_TAB_ROUTE_OPTIONS,
+ title: 'OG SWM',
+ android: {
+ icon: { type: 'drawableResource', name: 'swm_logo' },
+ standardAppearance: INDICATOR,
+ },
+ },
+ },
+ {
+ name: 'SIZED_SWM',
+ Component: TabScreen,
+ options: {
+ ...DEFAULT_TAB_ROUTE_OPTIONS,
+ title: 'Sized SWM',
+ android: {
+ drawableIconSize: 44,
+ icon: { type: 'drawableResource', name: 'swm_logo' },
+ standardAppearance: INDICATOR,
+ },
+ },
+ },
+ {
+ name: 'Multicolor',
+ Component: TabScreen,
+ options: {
+ ...DEFAULT_TAB_ROUTE_OPTIONS,
+ title: 'Multicolor Tint',
+ android: {
+ drawableIconSize: 30,
+ icon: {
+ type: 'drawableResource',
+ name: 'person_walking',
+ tinted: true,
+ },
+ selectedIcon: {
+ type: 'drawableResource',
+ name: 'person_walking',
+ tinted: false,
+ },
+ standardAppearance: INDICATOR,
+ },
+ },
+ },
+ {
+ name: 'System',
+ Component: TabScreen,
+ options: {
+ ...DEFAULT_TAB_ROUTE_OPTIONS,
+ title: 'Sys (unaltered)',
+ android: {
+ icon: { type: 'drawableResource', name: 'star_big_off' },
+ selectedIcon: { type: 'drawableResource', name: 'star_big_on' },
+ standardAppearance: INDICATOR,
+ },
+ },
+ },
+ {
+ name: 'IMAGE_TINT',
+ Component: TabScreen,
+ options: {
+ ...DEFAULT_TAB_ROUTE_OPTIONS,
+ title: 'Image Tint',
+ android: {
+ icon: {
+ type: 'imageSource',
+ imageSource: require('@assets/variableIcons/icon.png'),
+ },
+ selectedIcon: {
+ type: 'imageSource',
+ imageSource: require('@assets/variableIcons/icon.png'),
+ tinted: false,
+ },
+ standardAppearance: INDICATOR,
+ },
+ },
+ },
+];
+
+export default function CustomNativeTabsIcons() {
+ return ;
+}
+
+const styles = StyleSheet.create({
+ screen: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 24,
+ gap: 12,
+ },
+ label: {
+ fontSize: 17,
+ fontWeight: '600',
+ textAlign: 'center',
+ },
+ hint: {
+ fontSize: 13,
+ color: Colors.LightOffNavy,
+ textAlign: 'center',
+ lineHeight: 20,
+ },
+});
diff --git a/apps/src/screens/CustomNativeTabsIcons.ios.tsx b/apps/src/screens/CustomNativeTabsIcons.ios.tsx
new file mode 100644
index 0000000000..8f934c833a
--- /dev/null
+++ b/apps/src/screens/CustomNativeTabsIcons.ios.tsx
@@ -0,0 +1,131 @@
+import React from 'react';
+import { StyleSheet, Text, View } from 'react-native';
+import {
+ TabsContainer,
+ type TabRouteConfig,
+ DEFAULT_TAB_ROUTE_OPTIONS,
+} from '@apps/shared/containers/tabs';
+import { Colors } from '@apps/shared/styling';
+
+function TabScreen() {
+ return (
+
+ Custom asset-catalog tab icons
+
+ Symbol: a custom symbol (`nano.swm` symbolset). It is not a built-in SF
+ Symbol, so it resolves via the custom-symbol fallback. Being a template,
+ it follows the system/host tint.
+
+
+ Tinted: the same custom symbol, tinted RED when selected via
+ `standardAppearance`.
+
+
+ Walker: a multicolor imageset (`nanomc.walker`) rendered in its own
+ colors — it ignores tinting.
+
+
+ System: a built-in SF Symbol star with a filled selected variant.
+
+
+ Image: an `imageSource` icon rendered as a template (`tinted: true`), so
+ it follows the item icon color.
+
+
+ );
+}
+
+const ROUTES: TabRouteConfig[] = [
+ {
+ name: 'SWM_SYMBOL',
+ Component: TabScreen,
+ options: {
+ ...DEFAULT_TAB_ROUTE_OPTIONS,
+ title: 'Symbol',
+ ios: {
+ icon: { type: 'sfSymbol', name: 'nano.swm' },
+ },
+ },
+ },
+ {
+ name: 'SWM_TINTED',
+ Component: TabScreen,
+ options: {
+ ...DEFAULT_TAB_ROUTE_OPTIONS,
+ title: 'Tinted',
+ ios: {
+ icon: { type: 'sfSymbol', name: 'nano.swm' },
+ standardAppearance: {
+ stacked: {
+ selected: {
+ tabBarItemIconColor: Colors.RedLight100,
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: 'WALKER',
+ Component: TabScreen,
+ options: {
+ ...DEFAULT_TAB_ROUTE_OPTIONS,
+ title: 'Walker',
+ ios: {
+ icon: { type: 'xcasset', name: 'nanomc.walker' },
+ },
+ },
+ },
+ {
+ name: 'System',
+ Component: TabScreen,
+ options: {
+ ...DEFAULT_TAB_ROUTE_OPTIONS,
+ title: 'System',
+ ios: {
+ icon: { type: 'sfSymbol', name: 'star' },
+ selectedIcon: { type: 'sfSymbol', name: 'star.fill' },
+ },
+ },
+ },
+ {
+ name: 'IMAGE_TINT',
+ Component: TabScreen,
+ options: {
+ ...DEFAULT_TAB_ROUTE_OPTIONS,
+ title: 'Image',
+ ios: {
+ icon: {
+ type: 'imageSource',
+ imageSource: require('@assets/variableIcons/icon.png'),
+ tinted: true,
+ },
+ },
+ },
+ },
+];
+
+export default function CustomNativeTabsIcons() {
+ return ;
+}
+
+const styles = StyleSheet.create({
+ screen: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 24,
+ gap: 12,
+ },
+ label: {
+ fontSize: 17,
+ fontWeight: '600',
+ textAlign: 'center',
+ },
+ hint: {
+ fontSize: 13,
+ color: Colors.LightOffNavy,
+ textAlign: 'center',
+ lineHeight: 20,
+ },
+});
diff --git a/ios/helpers/image/RNSImageLoadingHelper.h b/ios/helpers/image/RNSImageLoadingHelper.h
index 40dab5fb81..62caa51bd6 100644
--- a/ios/helpers/image/RNSImageLoadingHelper.h
+++ b/ios/helpers/image/RNSImageLoadingHelper.h
@@ -27,6 +27,13 @@
asTemplate:(BOOL)isTemplate
completionBlock:(void (^_Nonnull)(UIImage *_Nullable image))imageLoadingCompletionBlock;
+/**
+ * Loads a symbol image by name: a built-in SF Symbol if one matches, otherwise a custom
+ * symbol from the app's asset catalog (custom symbols are semantically SF Symbols, but
+ * UIKit only resolves them via `imageNamed:`).
+ */
++ (nullable UIImage *)symbolImageNamed:(nonnull NSString *)name;
+
@end
#endif // defined(__cplusplus)
diff --git a/ios/helpers/image/RNSImageLoadingHelper.mm b/ios/helpers/image/RNSImageLoadingHelper.mm
index 64beb499a6..c979ad3bc8 100644
--- a/ios/helpers/image/RNSImageLoadingHelper.mm
+++ b/ios/helpers/image/RNSImageLoadingHelper.mm
@@ -59,6 +59,15 @@ + (void)loadImageFromSource:(nonnull RCTImageSource *)imageSource
}];
}
++ (nullable UIImage *)symbolImageNamed:(nonnull NSString *)name
+{
+ UIImage *image = [UIImage systemImageNamed:name];
+ if (image == nil) {
+ image = [UIImage imageNamed:name];
+ }
+ return image;
+}
+
+ (nullable UIImage *)handleRenderingModeForImage:(nullable UIImage *)image isTemplate:(BOOL)isTemplate
{
if (isTemplate) {
diff --git a/ios/tabs/RNSTabBarAppearanceCoordinator.mm b/ios/tabs/RNSTabBarAppearanceCoordinator.mm
index fee81a8fa6..0e99f7dc25 100644
--- a/ios/tabs/RNSTabBarAppearanceCoordinator.mm
+++ b/ios/tabs/RNSTabBarAppearanceCoordinator.mm
@@ -62,7 +62,7 @@ - (void)setIconsForTabBarItem:(UITabBarItem *)tabBarItem
if (screenView.iconType == RNSTabsIconTypeSfSymbol || screenView.iconType == RNSTabsIconTypeXcasset) {
if (screenView.iconResourceName != nil) {
if (screenView.iconType == RNSTabsIconTypeSfSymbol) {
- tabBarItem.image = [UIImage systemImageNamed:screenView.iconResourceName];
+ tabBarItem.image = [RNSImageLoadingHelper symbolImageNamed:screenView.iconResourceName];
} else {
tabBarItem.image = [UIImage imageNamed:screenView.iconResourceName];
}
@@ -77,7 +77,7 @@ - (void)setIconsForTabBarItem:(UITabBarItem *)tabBarItem
if (screenView.selectedIconResourceName != nil) {
if (screenView.iconType == RNSTabsIconTypeSfSymbol) {
- tabBarItem.selectedImage = [UIImage systemImageNamed:screenView.selectedIconResourceName];
+ tabBarItem.selectedImage = [RNSImageLoadingHelper symbolImageNamed:screenView.selectedIconResourceName];
} else {
tabBarItem.selectedImage = [UIImage imageNamed:screenView.selectedIconResourceName];
}
diff --git a/react-navigation b/react-navigation
index 449fee23ff..40b7e211a7 160000
--- a/react-navigation
+++ b/react-navigation
@@ -1 +1 @@
-Subproject commit 449fee23ff8ebf226dad3328d711d1b95734d23b
+Subproject commit 40b7e211a7737574f5d01aa8820bf7e8a4f95499
diff --git a/src/components/shared/types.ts b/src/components/shared/types.ts
index a49f1df765..344ee446be 100644
--- a/src/components/shared/types.ts
+++ b/src/components/shared/types.ts
@@ -44,13 +44,27 @@ export type UserInterfaceStyle = 'unspecified' | 'light' | 'dark';
export type PlatformIconShared = {
type: 'imageSource';
imageSource: ImageSourcePropType;
+ /**
+ * Whether the host component tints the icon with its state-dependent icon color.
+ * The default is platform-specific: Android tints image icons, while iOS
+ * renders them in their original colors unless `tinted` is `true`.
+ */
+ tinted?: boolean;
};
export type PlatformIconAndroidDrawableResource = {
type: 'drawableResource';
name: string;
+ /**
+ * Whether the host component tints the icon with its state-dependent icon color.
+ * Defaults to `true`; `false` keeps the drawable's own colors.
+ */
+ tinted?: boolean;
};
+/**
+ * @deprecated Use `{ type: 'imageSource', imageSource, tinted: true }` instead.
+ */
export type PlatformIconIOSTemplate = {
type: 'templateSource';
templateSource: ImageSourcePropType;
diff --git a/src/components/tabs/screen/TabsScreen.android.tsx b/src/components/tabs/screen/TabsScreen.android.tsx
index 0aab02fdb1..c48c4d4b8a 100644
--- a/src/components/tabs/screen/TabsScreen.android.tsx
+++ b/src/components/tabs/screen/TabsScreen.android.tsx
@@ -63,6 +63,7 @@ function TabsScreen(props: TabsScreenProps) {
{...iconProps}
{...filteredBaseProps}
// Android-specific
+ drawableIconSize={android?.drawableIconSize}
standardAppearance={mapAppearanceToNativeProps(
android?.standardAppearance,
)}>
@@ -127,14 +128,22 @@ function mapItemStateAppearanceToNativeProp(
};
}
+function iconTintedOf(
+ icon: PlatformIconAndroid | undefined,
+): boolean | undefined {
+ return icon?.tinted;
+}
+
function parseIconsToNativeProps(
icon: PlatformIconAndroid | undefined,
selectedIcon: PlatformIconAndroid | undefined,
): {
imageIconResource?: ImageResolvedAssetSource | undefined;
drawableIconResourceName?: string | undefined;
+ drawableIconTinted?: boolean | undefined;
selectedImageIconResource?: ImageResolvedAssetSource | undefined;
selectedDrawableIconResourceName?: string | undefined;
+ selectedDrawableIconTinted?: boolean | undefined;
} {
const parsedIcon = parseAndroidIconToNativeProps(icon);
const parsedSelectedIcon = parseAndroidIconToNativeProps(selectedIcon);
@@ -142,9 +151,11 @@ function parseIconsToNativeProps(
return {
imageIconResource: parsedIcon.imageIconResource,
drawableIconResourceName: parsedIcon.drawableIconResourceName,
+ drawableIconTinted: iconTintedOf(icon),
selectedImageIconResource: parsedSelectedIcon.imageIconResource,
selectedDrawableIconResourceName:
parsedSelectedIcon.drawableIconResourceName,
+ selectedDrawableIconTinted: iconTintedOf(selectedIcon),
};
}
diff --git a/src/components/tabs/screen/TabsScreen.android.types.ts b/src/components/tabs/screen/TabsScreen.android.types.ts
index 876cbde186..653ad3049a 100644
--- a/src/components/tabs/screen/TabsScreen.android.types.ts
+++ b/src/components/tabs/screen/TabsScreen.android.types.ts
@@ -99,6 +99,20 @@ export interface TabsScreenAppearanceAndroid {
* @platform android
*/
tabBarItemActiveIndicatorEnabled?: boolean | undefined;
+ /**
+ * @summary Active-indicator pill width in dp. If unset, it auto-scales to wrap
+ * the icon box when icons are enlarged via `drawableIconSize`.
+ *
+ * @platform android
+ */
+ tabBarItemActiveIndicatorWidth?: number | undefined;
+ /**
+ * @summary Active-indicator pill height in dp. If unset, it auto-scales to wrap
+ * the icon box when icons are enlarged via `drawableIconSize`.
+ *
+ * @platform android
+ */
+ tabBarItemActiveIndicatorHeight?: number | undefined;
/**
* @summary Specifies the font family used for the title of each tab bar item.
*
@@ -162,8 +176,10 @@ export interface TabsScreenPropsAndroid {
* @summary Specifies the icon for the tab bar item.
*
* Supported values:
- * - `{ type: 'imageSource', imageSource }`
- * Uses an image from the provided resource.
+ * - `{ type: 'imageSource', imageSource, tinted? }`
+ * Uses an image from the provided resource. The tab bar tints it with the
+ * item icon color unless `tinted` is `false`, which keeps the image's own
+ * colors.
*
* Remarks: `imageSource` type doesn't support SVGs on Android.
* For loading SVGs use `drawableResource` type.
@@ -186,4 +202,14 @@ export interface TabsScreenPropsAndroid {
* @platform android
*/
selectedIcon?: PlatformIconAndroid | undefined;
+ /**
+ * @summary Per-tab icon size in dp.
+ *
+ * The bottom bar's icon box is the largest `drawableIconSize` across all tabs;
+ * each tab's icon is inset to its own size within that box. Tabs without a value
+ * use the system default.
+ *
+ * @platform android
+ */
+ drawableIconSize?: number | undefined;
}
diff --git a/src/components/tabs/screen/TabsScreen.ios.tsx b/src/components/tabs/screen/TabsScreen.ios.tsx
index 209fdcd838..30b2027be6 100644
--- a/src/components/tabs/screen/TabsScreen.ios.tsx
+++ b/src/components/tabs/screen/TabsScreen.ios.tsx
@@ -165,7 +165,9 @@ function parseIconsToNativeProps(
parsedSelectedIcon.iconType !== undefined &&
parsedIcon.iconType !== parsedSelectedIcon.iconType
) {
- throw new Error('[RNScreens] icon and selectedIcon must be same type.');
+ throw new Error(
+ '[RNScreens] icon and selectedIcon must be same type. Note that `imageSource` icons with different `tinted` values resolve to different types.',
+ );
} else if (
parsedIcon.iconType === undefined &&
parsedSelectedIcon.iconType !== undefined
@@ -200,7 +202,7 @@ function parseIconToNativeProps(icon: PlatformIconIOS | undefined): {
};
} else if (icon.type === 'imageSource') {
return {
- iconType: 'image',
+ iconType: icon.tinted ? 'template' : 'image',
iconImageSource: icon.imageSource,
};
} else if (icon.type === 'templateSource') {
diff --git a/src/components/tabs/screen/TabsScreen.ios.types.ts b/src/components/tabs/screen/TabsScreen.ios.types.ts
index d2153ebf1f..040bbccb38 100644
--- a/src/components/tabs/screen/TabsScreen.ios.types.ts
+++ b/src/components/tabs/screen/TabsScreen.ios.types.ts
@@ -233,14 +233,19 @@ export interface TabsScreenPropsIOS {
* @summary Specifies the icon for the tab bar item.
*
* Supported values:
- * - `{ type: 'imageSource', imageSource }`
- * Uses an image from the provided resource.
+ * - `{ type: 'imageSource', imageSource, tinted? }`
+ * Uses an image from the provided resource. By default the image renders
+ * in its original colors; with `tinted: true` it is used as a template
+ * image and follows the state-dependent icon color.
* - `{ type: 'sfSymbol', name }`
- * Uses an SF Symbol with the specified name.
+ * Uses an SF Symbol with the specified name. If no system symbol
+ * matches, falls back to an asset with that name (e.g. a custom
+ * SF Symbol from the app's asset catalog).
* - `{ type: 'xcasset', name }`
* Uses asset from Xcassets.
* - `{ type: 'templateSource', templateSource }`
- * Uses the provided image as a template image.
+ * Deprecated: use `{ type: 'imageSource', imageSource, tinted: true }`
+ * instead. Uses the provided image as a template image.
* The icon color will depend on the current state
* of the tab bar item and icon color-related props.
*
diff --git a/src/fabric/tabs/TabsScreenAndroidNativeComponent.ts b/src/fabric/tabs/TabsScreenAndroidNativeComponent.ts
index 104117271d..462f5d799a 100644
--- a/src/fabric/tabs/TabsScreenAndroidNativeComponent.ts
+++ b/src/fabric/tabs/TabsScreenAndroidNativeComponent.ts
@@ -50,6 +50,9 @@ export type Appearance = {
// TabBarItem - Active Indicator
tabBarItemActiveIndicatorColor?: ProcessedColorValue | null | undefined;
tabBarItemActiveIndicatorEnabled?: CT.WithDefault;
+ // Indicator size (dp); unset = auto-scale to the icon box.
+ tabBarItemActiveIndicatorWidth?: CT.Float | undefined;
+ tabBarItemActiveIndicatorHeight?: CT.Float | undefined;
// TabBarItem - Label
tabBarItemTitleFontFamily?: string | undefined;
@@ -102,6 +105,10 @@ export interface NativeProps extends ViewProps {
imageIconResource?: ImageSource | undefined;
selectedDrawableIconResourceName?: string | undefined;
selectedImageIconResource?: ImageSource | undefined;
+ drawableIconTinted?: CT.WithDefault;
+ selectedDrawableIconTinted?: CT.WithDefault;
+ // Per-tab icon size (dp); 0/unset = system default.
+ drawableIconSize?: CT.Float | undefined;
// Appearance
standardAppearance?: Appearance | undefined;