From 9cd91aa9f1359c1a2ca302748e7edb8036e363de Mon Sep 17 00:00:00 2001 From: stachbial Date: Wed, 24 Jun 2026 20:38:57 +0200 Subject: [PATCH 1/5] feat(tabs): support custom drawable tab-bar icons on Android Extend the gamma native bottom tab bar so a drawableResource icon can: - keep its own colors via tintingMode 'original' (NoTintDrawable) - size per tab via drawableIconSize (shared max box + per-item inset) - size the active indicator via activeIndicatorWidth/Height, or auto-scale --- .../rnscreens/gamma/helpers/NoTintDrawable.kt | 18 ++++++ .../appearance/TabsAppearanceApplicator.kt | 57 ++++++++++++++++++- .../appearance/TabsAppearanceCoordinator.kt | 10 ++++ .../rnscreens/gamma/tabs/screen/TabsScreen.kt | 52 +++++++++++++++-- .../tabs/screen/TabsScreenViewManager.kt | 35 ++++++++++++ .../tabs/screen/TabsScreen.android.tsx | 13 +++++ .../tabs/screen/TabsScreen.android.types.ts | 18 ++++++ .../tabs/TabsScreenAndroidNativeComponent.ts | 7 +++ src/types.tsx | 6 ++ 9 files changed, 209 insertions(+), 7 deletions(-) create mode 100644 android/src/main/java/com/swmansion/rnscreens/gamma/helpers/NoTintDrawable.kt diff --git a/android/src/main/java/com/swmansion/rnscreens/gamma/helpers/NoTintDrawable.kt b/android/src/main/java/com/swmansion/rnscreens/gamma/helpers/NoTintDrawable.kt new file mode 100644 index 0000000000..70031dcd72 --- /dev/null +++ b/android/src/main/java/com/swmansion/rnscreens/gamma/helpers/NoTintDrawable.kt @@ -0,0 +1,18 @@ +package com.swmansion.rnscreens.gamma.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/gamma/tabs/appearance/TabsAppearanceApplicator.kt b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/appearance/TabsAppearanceApplicator.kt index 10cd91b286..c607ca6a01 100644 --- a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/appearance/TabsAppearanceApplicator.kt +++ b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/appearance/TabsAppearanceApplicator.kt @@ -3,6 +3,8 @@ package com.swmansion.rnscreens.gamma.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 @@ -22,6 +24,55 @@ import com.swmansion.rnscreens.utils.resolveColorAttr internal class TabsAppearanceApplicator( private val bottomNavigationView: BottomNavigationView, ) { + // Largest effective per-tab icon size (dp); Material allows only one size for all items. + private var iconBoxDp: Float = TabsScreen.DEFAULT_ICON_SIZE_DP + + fun applyIconBox(boxDp: Float) { + iconBoxDp = boxDp + bottomNavigationView.itemIconSize = PixelUtil.toPixelFromDIP(boxDp).toInt() + } + + // Material defaults, captured before we override them so unset bars restore exactly. + private var defaultIndicatorWidthPx: Int = -1 + private var defaultIndicatorHeightPx: Int = -1 + + // Explicit size wins; else auto-scale to wrap an enlarged icon box; else Material default. + fun applyActiveIndicator( + boxDp: Float, + maxWidthDp: Float, + maxHeightDp: Float, + ) { + if (defaultIndicatorWidthPx < 0) { + defaultIndicatorWidthPx = bottomNavigationView.itemActiveIndicatorWidth + defaultIndicatorHeightPx = bottomNavigationView.itemActiveIndicatorHeight + } + val enlarged = boxDp > TabsScreen.DEFAULT_ICON_SIZE_DP + bottomNavigationView.itemActiveIndicatorWidth = + when { + maxWidthDp > 0f -> PixelUtil.toPixelFromDIP(maxWidthDp).toInt() + enlarged -> PixelUtil.toPixelFromDIP(boxDp + 16f).toInt() + else -> defaultIndicatorWidthPx + } + bottomNavigationView.itemActiveIndicatorHeight = + when { + maxHeightDp > 0f -> PixelUtil.toPixelFromDIP(maxHeightDp).toInt() + enlarged -> PixelUtil.toPixelFromDIP(boxDp + 8f).toInt() + else -> defaultIndicatorHeightPx + } + } + + // Inset the icon so it renders at effectiveDp, centered within iconBoxDp. + private fun sizeIcon( + icon: Drawable?, + effectiveDp: 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 @@ -189,8 +240,10 @@ internal class TabsAppearanceApplicator( tabsScreen.icon } - if (menuItem.icon != targetIcon) { - menuItem.icon = targetIcon + val sizedIcon = sizeIcon(targetIcon, tabsScreen.effectiveIconSizeDp) + + if (menuItem.icon != sizedIcon) { + menuItem.icon = sizedIcon } } diff --git a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/appearance/TabsAppearanceCoordinator.kt b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/appearance/TabsAppearanceCoordinator.kt index 0b567e2530..688a4d067a 100644 --- a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/appearance/TabsAppearanceCoordinator.kt +++ b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/appearance/TabsAppearanceCoordinator.kt @@ -20,6 +20,16 @@ internal class TabsAppearanceCoordinator( ) { val selectedTabAppearance = tabsContainer.selectedTab.tabsScreen.appearance appearanceApplicator.updateSharedAppearance(context, selectedTabAppearance, tabsContainer.tabBarHidden) + // Icon box and indicator are bar-wide; take the largest value across tabs. + val iconBoxDp = + tabsScreenFragments.maxOfOrNull { it.tabsScreen.effectiveIconSizeDp } + ?: TabsScreen.DEFAULT_ICON_SIZE_DP + appearanceApplicator.applyIconBox(iconBoxDp) + val indicatorWidthDp = + tabsScreenFragments.maxOfOrNull { it.tabsScreen.activeIndicatorWidth } ?: 0f + val indicatorHeightDp = + tabsScreenFragments.maxOfOrNull { it.tabsScreen.activeIndicatorHeight } ?: 0f + appearanceApplicator.applyActiveIndicator(iconBoxDp, indicatorWidthDp, indicatorHeightDp) updateMenuItems(context, selectedTabAppearance) appearanceApplicator.updateFontStyles(context, selectedTabAppearance) // It needs to be updated after updateMenuItems } diff --git a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/screen/TabsScreen.kt b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/screen/TabsScreen.kt index 24d107f6fe..10a1e9812e 100644 --- a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/screen/TabsScreen.kt +++ b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/screen/TabsScreen.kt @@ -6,6 +6,7 @@ import android.view.ViewGroup import androidx.fragment.app.Fragment import com.facebook.react.uimanager.ThemedReactContext import com.swmansion.rnscreens.gamma.common.FragmentProviding +import com.swmansion.rnscreens.gamma.helpers.NoTintDrawable import com.swmansion.rnscreens.gamma.helpers.getSystemDrawableResource import com.swmansion.rnscreens.gamma.tabs.appearance.TabsAppearance import com.swmansion.rnscreens.utils.RNSLog @@ -72,15 +73,53 @@ class TabsScreen( // Icon var drawableIconResourceName: String? by Delegates.observable(null) { _, oldValue, newValue -> - if (newValue != oldValue) { - icon = getSystemDrawableResource(reactContext, newValue) - } + if (newValue != oldValue) rebuildIcon() } + var drawableIconTintingMode: String = "template" + set(value) { + if (field != value) { + field = value + rebuildIcon() + } + } + var selectedDrawableIconResourceName: String? by Delegates.observable(null) { _, oldValue, newValue -> - if (newValue != oldValue) { - selectedIcon = getSystemDrawableResource(reactContext, newValue) + if (newValue != oldValue) rebuildSelectedIcon() + } + + var selectedDrawableIconTintingMode: String = "template" + set(value) { + if (field != value) { + field = value + rebuildSelectedIcon() + } } + + // Per-tab icon size in dp; 0 means the default. + var drawableIconSize: Float = 0f + + val effectiveIconSizeDp: Float + get() = if (drawableIconSize > 0f) drawableIconSize else DEFAULT_ICON_SIZE_DP + + var activeIndicatorWidth: Float = 0f + var activeIndicatorHeight: Float = 0f + + // "original" keeps the drawable's own colors; the bar can't tint a NoTintDrawable. + private fun resolveIcon( + resourceName: String?, + tintingMode: String, + ): Drawable? { + val drawable = getSystemDrawableResource(reactContext, resourceName) ?: return null + return if (tintingMode == "original") NoTintDrawable(drawable) else drawable + } + + private fun rebuildIcon() { + icon = resolveIcon(drawableIconResourceName, drawableIconTintingMode) + } + + private fun rebuildSelectedIcon() { + selectedIcon = resolveIcon(selectedDrawableIconResourceName, selectedDrawableIconTintingMode) } var icon: Drawable? by Delegates.observable(null) { _, oldValue, newValue -> @@ -141,5 +180,8 @@ class TabsScreen( companion object { const val TAG = "TabsScreen" + + // Material's default bottom-navigation icon size (dp). + internal const val DEFAULT_ICON_SIZE_DP = 24f } } diff --git a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/screen/TabsScreenViewManager.kt b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/screen/TabsScreenViewManager.kt index 60059b4c4a..7db0742f01 100644 --- a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/screen/TabsScreenViewManager.kt +++ b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/screen/TabsScreenViewManager.kt @@ -134,6 +134,41 @@ class TabsScreenViewManager : view.selectedDrawableIconResourceName = value } + override fun setDrawableIconTintingMode( + view: TabsScreen, + value: String?, + ) { + view.drawableIconTintingMode = value ?: "template" + } + + override fun setSelectedDrawableIconTintingMode( + view: TabsScreen, + value: String?, + ) { + view.selectedDrawableIconTintingMode = value ?: "template" + } + + override fun setDrawableIconSize( + view: TabsScreen, + value: Float, + ) { + view.drawableIconSize = value + } + + override fun setActiveIndicatorWidth( + view: TabsScreen, + value: Float, + ) { + view.activeIndicatorWidth = value + } + + override fun setActiveIndicatorHeight( + view: TabsScreen, + value: Float, + ) { + view.activeIndicatorHeight = value + } + override fun setImageIconResource( view: TabsScreen, value: ReadableMap?, diff --git a/src/components/tabs/screen/TabsScreen.android.tsx b/src/components/tabs/screen/TabsScreen.android.tsx index a63fc3b33d..13c16e32df 100644 --- a/src/components/tabs/screen/TabsScreen.android.tsx +++ b/src/components/tabs/screen/TabsScreen.android.tsx @@ -66,6 +66,9 @@ function TabsScreen(props: TabsScreenProps) { {...iconProps} {...filteredBaseProps} // Android-specific + drawableIconSize={android?.drawableIconSize} + activeIndicatorWidth={android?.activeIndicatorWidth} + activeIndicatorHeight={android?.activeIndicatorHeight} standardAppearance={mapAppearanceToNativeProps( android?.standardAppearance, )}> @@ -130,14 +133,22 @@ function mapItemStateAppearanceToNativeProp( }; } +function drawableTintingModeOf( + icon: PlatformIconAndroid | undefined, +): 'template' | 'original' | undefined { + return icon?.type === 'drawableResource' ? icon.tintingMode : undefined; +} + function parseIconsToNativeProps( icon: PlatformIconAndroid | undefined, selectedIcon: PlatformIconAndroid | undefined, ): { imageIconResource?: ImageResolvedAssetSource | undefined; drawableIconResourceName?: string | undefined; + drawableIconTintingMode?: string | undefined; selectedImageIconResource?: ImageResolvedAssetSource | undefined; selectedDrawableIconResourceName?: string | undefined; + selectedDrawableIconTintingMode?: string | undefined; } { const parsedIcon = parseAndroidIconToNativeProps(icon); const parsedSelectedIcon = parseAndroidIconToNativeProps(selectedIcon); @@ -145,9 +156,11 @@ function parseIconsToNativeProps( return { imageIconResource: parsedIcon.imageIconResource, drawableIconResourceName: parsedIcon.drawableIconResourceName, + drawableIconTintingMode: drawableTintingModeOf(icon), selectedImageIconResource: parsedSelectedIcon.imageIconResource, selectedDrawableIconResourceName: parsedSelectedIcon.drawableIconResourceName, + selectedDrawableIconTintingMode: drawableTintingModeOf(selectedIcon), }; } diff --git a/src/components/tabs/screen/TabsScreen.android.types.ts b/src/components/tabs/screen/TabsScreen.android.types.ts index 2e3b25bbeb..0f89dd3ce2 100644 --- a/src/components/tabs/screen/TabsScreen.android.types.ts +++ b/src/components/tabs/screen/TabsScreen.android.types.ts @@ -186,4 +186,22 @@ 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 default 24dp. + * + * @platform android + */ + drawableIconSize?: number | undefined; + /** + * @summary Active-indicator pill size in dp (bar-wide; the largest across tabs + * wins). If unset, it auto-scales to wrap the icon box. + * + * @platform android + */ + activeIndicatorWidth?: number | undefined; + activeIndicatorHeight?: number | undefined; } diff --git a/src/fabric/tabs/TabsScreenAndroidNativeComponent.ts b/src/fabric/tabs/TabsScreenAndroidNativeComponent.ts index 104117271d..4e10d99d70 100644 --- a/src/fabric/tabs/TabsScreenAndroidNativeComponent.ts +++ b/src/fabric/tabs/TabsScreenAndroidNativeComponent.ts @@ -102,6 +102,13 @@ export interface NativeProps extends ViewProps { imageIconResource?: ImageSource | undefined; selectedDrawableIconResourceName?: string | undefined; selectedImageIconResource?: ImageSource | undefined; + // 'template' (default) tints the drawable; 'original' keeps its own colors. + drawableIconTintingMode?: CT.WithDefault; + selectedDrawableIconTintingMode?: CT.WithDefault; + // Per-tab icon size (dp); 0/unset = default. Bar-wide indicator size (dp). + drawableIconSize?: CT.Float | undefined; + activeIndicatorWidth?: CT.Float | undefined; + activeIndicatorHeight?: CT.Float | undefined; // Appearance standardAppearance?: Appearance | undefined; diff --git a/src/types.tsx b/src/types.tsx index 76a83f3acb..c6b5c516da 100644 --- a/src/types.tsx +++ b/src/types.tsx @@ -119,6 +119,12 @@ export type PlatformIconAndroid = | { type: 'drawableResource'; name: string; + /** + * How the bottom tab bar colors the icon: + * - 'template' (default): tint with the item icon color (selected/normal). + * - 'original': keep the drawable's own colors (e.g. multicolor icons). + */ + tintingMode?: 'template' | 'original'; } | PlatformIconShared; From 5c2f8950a83506d9c9a45ef2855e29f8eb3d611f Mon Sep 17 00:00:00 2001 From: stachbial Date: Thu, 25 Jun 2026 12:06:58 +0200 Subject: [PATCH 2/5] refactor(tabs): move tabBarItemActiveIndicator size to standardAppearance The active indicator is bar-wide in Material (one size for all items), so exposing it per tab via activeIndicatorWidth/Height was misleading. Move the size into the standardAppearance struct as tabBarItemActiveIndicatorWidth/Height, applied from the focused tab like the indicator color, keeping the icon-box auto-scale fallback. drawableIconSize stays per-tab. --- .../appearance/TabsAppearanceApplicator.kt | 25 +++++++++++-------- .../appearance/TabsAppearanceCoordinator.kt | 10 +++----- .../tabs/appearance/TabsAppearanceModel.kt | 2 ++ .../rnscreens/gamma/tabs/screen/TabsScreen.kt | 3 --- .../tabs/screen/TabsScreenViewManager.kt | 16 ++---------- .../tabs/screen/TabsScreen.android.tsx | 2 -- .../tabs/screen/TabsScreen.android.types.ts | 16 ++++++------ .../tabs/TabsScreenAndroidNativeComponent.ts | 7 +++--- 8 files changed, 34 insertions(+), 47 deletions(-) diff --git a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/appearance/TabsAppearanceApplicator.kt b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/appearance/TabsAppearanceApplicator.kt index c607ca6a01..cf87221d28 100644 --- a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/appearance/TabsAppearanceApplicator.kt +++ b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/appearance/TabsAppearanceApplicator.kt @@ -36,27 +36,27 @@ internal class TabsAppearanceApplicator( private var defaultIndicatorWidthPx: Int = -1 private var defaultIndicatorHeightPx: Int = -1 - // Explicit size wins; else auto-scale to wrap an enlarged icon box; else Material default. - fun applyActiveIndicator( - boxDp: Float, - maxWidthDp: Float, - maxHeightDp: Float, + // Explicit dp wins; else auto-scale to the enlarged icon box; else Material + // default. Reads iconBoxDp, so applyIconBox must run first. + private fun applyActiveIndicatorSize( + widthDp: Float?, + heightDp: Float?, ) { if (defaultIndicatorWidthPx < 0) { defaultIndicatorWidthPx = bottomNavigationView.itemActiveIndicatorWidth defaultIndicatorHeightPx = bottomNavigationView.itemActiveIndicatorHeight } - val enlarged = boxDp > TabsScreen.DEFAULT_ICON_SIZE_DP + val enlarged = iconBoxDp > TabsScreen.DEFAULT_ICON_SIZE_DP bottomNavigationView.itemActiveIndicatorWidth = when { - maxWidthDp > 0f -> PixelUtil.toPixelFromDIP(maxWidthDp).toInt() - enlarged -> PixelUtil.toPixelFromDIP(boxDp + 16f).toInt() + widthDp != null && widthDp > 0f -> PixelUtil.toPixelFromDIP(widthDp).toInt() + enlarged -> PixelUtil.toPixelFromDIP(iconBoxDp + 16f).toInt() else -> defaultIndicatorWidthPx } bottomNavigationView.itemActiveIndicatorHeight = when { - maxHeightDp > 0f -> PixelUtil.toPixelFromDIP(maxHeightDp).toInt() - enlarged -> PixelUtil.toPixelFromDIP(boxDp + 8f).toInt() + heightDp != null && heightDp > 0f -> PixelUtil.toPixelFromDIP(heightDp).toInt() + enlarged -> PixelUtil.toPixelFromDIP(iconBoxDp + 8f).toInt() else -> defaultIndicatorHeightPx } } @@ -160,6 +160,11 @@ internal class TabsAppearanceApplicator( bottomNavigationView.isItemActiveIndicatorEnabled = tabBarAppearance?.tabBarItemActiveIndicatorEnabled ?: true bottomNavigationView.itemActiveIndicatorColor = ColorStateList.valueOf(activeIndicatorColor) + + applyActiveIndicatorSize( + tabBarAppearance?.tabBarItemActiveIndicatorWidth, + tabBarAppearance?.tabBarItemActiveIndicatorHeight, + ) } fun updateFontStyles( diff --git a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/appearance/TabsAppearanceCoordinator.kt b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/appearance/TabsAppearanceCoordinator.kt index 688a4d067a..1fb2c26f1e 100644 --- a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/appearance/TabsAppearanceCoordinator.kt +++ b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/appearance/TabsAppearanceCoordinator.kt @@ -19,17 +19,13 @@ internal class TabsAppearanceCoordinator( tabsContainer: TabsContainer, ) { val selectedTabAppearance = tabsContainer.selectedTab.tabsScreen.appearance - appearanceApplicator.updateSharedAppearance(context, selectedTabAppearance, tabsContainer.tabBarHidden) - // Icon box and indicator are bar-wide; take the largest value across tabs. + // Icon box is bar-wide: the largest effective size across tabs. Apply it + // before updateSharedAppearance, which sizes the indicator against the box. val iconBoxDp = tabsScreenFragments.maxOfOrNull { it.tabsScreen.effectiveIconSizeDp } ?: TabsScreen.DEFAULT_ICON_SIZE_DP appearanceApplicator.applyIconBox(iconBoxDp) - val indicatorWidthDp = - tabsScreenFragments.maxOfOrNull { it.tabsScreen.activeIndicatorWidth } ?: 0f - val indicatorHeightDp = - tabsScreenFragments.maxOfOrNull { it.tabsScreen.activeIndicatorHeight } ?: 0f - appearanceApplicator.applyActiveIndicator(iconBoxDp, indicatorWidthDp, indicatorHeightDp) + appearanceApplicator.updateSharedAppearance(context, selectedTabAppearance, tabsContainer.tabBarHidden) updateMenuItems(context, selectedTabAppearance) appearanceApplicator.updateFontStyles(context, selectedTabAppearance) // It needs to be updated after updateMenuItems } diff --git a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/appearance/TabsAppearanceModel.kt b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/appearance/TabsAppearanceModel.kt index 766abbf2b9..ce685c6015 100644 --- a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/appearance/TabsAppearanceModel.kt +++ b/android/src/main/java/com/swmansion/rnscreens/gamma/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/gamma/tabs/screen/TabsScreen.kt b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/screen/TabsScreen.kt index 10a1e9812e..8f13821734 100644 --- a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/screen/TabsScreen.kt +++ b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/screen/TabsScreen.kt @@ -102,9 +102,6 @@ class TabsScreen( val effectiveIconSizeDp: Float get() = if (drawableIconSize > 0f) drawableIconSize else DEFAULT_ICON_SIZE_DP - var activeIndicatorWidth: Float = 0f - var activeIndicatorHeight: Float = 0f - // "original" keeps the drawable's own colors; the bar can't tint a NoTintDrawable. private fun resolveIcon( resourceName: String?, diff --git a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/screen/TabsScreenViewManager.kt b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/screen/TabsScreenViewManager.kt index 7db0742f01..9a07abf4e2 100644 --- a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/screen/TabsScreenViewManager.kt +++ b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/screen/TabsScreenViewManager.kt @@ -155,20 +155,6 @@ class TabsScreenViewManager : view.drawableIconSize = value } - override fun setActiveIndicatorWidth( - view: TabsScreen, - value: Float, - ) { - view.activeIndicatorWidth = value - } - - override fun setActiveIndicatorHeight( - view: TabsScreen, - value: Float, - ) { - view.activeIndicatorHeight = value - } - override fun setImageIconResource( view: TabsScreen, value: ReadableMap?, @@ -216,6 +202,8 @@ class TabsScreenViewManager : disabled = if (appearance.hasKey("disabled")) parseItemStateAppearance(appearance.getMap("disabled")) else null, tabBarItemActiveIndicatorColor = appearance.getOptionalColor("tabBarItemActiveIndicatorColor"), tabBarItemActiveIndicatorEnabled = appearance.getOptionalBoolean("tabBarItemActiveIndicatorEnabled"), + tabBarItemActiveIndicatorWidth = appearance.getOptionalFloat("tabBarItemActiveIndicatorWidth"), + tabBarItemActiveIndicatorHeight = appearance.getOptionalFloat("tabBarItemActiveIndicatorHeight"), tabBarItemTitleFontFamily = appearance.getOptionalString("tabBarItemTitleFontFamily"), tabBarItemTitleSmallLabelFontSize = appearance.getOptionalFloat("tabBarItemTitleSmallLabelFontSize"), tabBarItemTitleLargeLabelFontSize = appearance.getOptionalFloat("tabBarItemTitleLargeLabelFontSize"), diff --git a/src/components/tabs/screen/TabsScreen.android.tsx b/src/components/tabs/screen/TabsScreen.android.tsx index 13c16e32df..9098702bda 100644 --- a/src/components/tabs/screen/TabsScreen.android.tsx +++ b/src/components/tabs/screen/TabsScreen.android.tsx @@ -67,8 +67,6 @@ function TabsScreen(props: TabsScreenProps) { {...filteredBaseProps} // Android-specific drawableIconSize={android?.drawableIconSize} - activeIndicatorWidth={android?.activeIndicatorWidth} - activeIndicatorHeight={android?.activeIndicatorHeight} standardAppearance={mapAppearanceToNativeProps( android?.standardAppearance, )}> diff --git a/src/components/tabs/screen/TabsScreen.android.types.ts b/src/components/tabs/screen/TabsScreen.android.types.ts index 0f89dd3ce2..46526a0879 100644 --- a/src/components/tabs/screen/TabsScreen.android.types.ts +++ b/src/components/tabs/screen/TabsScreen.android.types.ts @@ -99,6 +99,14 @@ export interface TabsScreenAppearanceAndroid { * @platform android */ tabBarItemActiveIndicatorEnabled?: boolean | undefined; + /** + * @summary Active-indicator pill size in dp. If unset, it auto-scales to wrap + * the icon box when icons are enlarged via `drawableIconSize`. + * + * @platform android + */ + tabBarItemActiveIndicatorWidth?: number | undefined; + tabBarItemActiveIndicatorHeight?: number | undefined; /** * @summary Specifies the font family used for the title of each tab bar item. * @@ -196,12 +204,4 @@ export interface TabsScreenPropsAndroid { * @platform android */ drawableIconSize?: number | undefined; - /** - * @summary Active-indicator pill size in dp (bar-wide; the largest across tabs - * wins). If unset, it auto-scales to wrap the icon box. - * - * @platform android - */ - activeIndicatorWidth?: number | undefined; - activeIndicatorHeight?: number | undefined; } diff --git a/src/fabric/tabs/TabsScreenAndroidNativeComponent.ts b/src/fabric/tabs/TabsScreenAndroidNativeComponent.ts index 4e10d99d70..eb3d3c99f3 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; @@ -105,10 +108,8 @@ export interface NativeProps extends ViewProps { // 'template' (default) tints the drawable; 'original' keeps its own colors. drawableIconTintingMode?: CT.WithDefault; selectedDrawableIconTintingMode?: CT.WithDefault; - // Per-tab icon size (dp); 0/unset = default. Bar-wide indicator size (dp). + // Per-tab icon size (dp); 0/unset = default 24dp. drawableIconSize?: CT.Float | undefined; - activeIndicatorWidth?: CT.Float | undefined; - activeIndicatorHeight?: CT.Float | undefined; // Appearance standardAppearance?: Appearance | undefined; From 5b240160bd1dfe37ab22bfba66a599e8aa80b36b Mon Sep 17 00:00:00 2001 From: stachbial Date: Thu, 25 Jun 2026 13:31:44 +0200 Subject: [PATCH 3/5] chore: add example --- .../src/main/res/drawable/person_walking.xml | 32 +++++ .../app/src/main/res/drawable/swm_logo.xml | 9 ++ apps/Example.tsx | 6 + apps/src/screens/TabsDrawableIconSize.tsx | 127 ++++++++++++++++++ 4 files changed, 174 insertions(+) create mode 100644 FabricExample/android/app/src/main/res/drawable/person_walking.xml create mode 100644 FabricExample/android/app/src/main/res/drawable/swm_logo.xml create mode 100644 apps/src/screens/TabsDrawableIconSize.tsx 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/apps/Example.tsx b/apps/Example.tsx index d4f2dfe8a3..27a83c7060 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 TabsDrawableIconSize from './src/screens/TabsDrawableIconSize'; 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', }, + TabsDrawableIconSize: { + title: 'Tabs Drawable Icon Size', + component: TabsDrawableIconSize, + type: 'example', + }, StackPresentation: { title: 'Stack Presentation', component: StackPresentation, diff --git a/apps/src/screens/TabsDrawableIconSize.tsx b/apps/src/screens/TabsDrawableIconSize.tsx new file mode 100644 index 0000000000..b981af626c --- /dev/null +++ b/apps/src/screens/TabsDrawableIconSize.tsx @@ -0,0 +1,127 @@ +import React from 'react'; +import { StyleSheet, Text, View } from 'react-native'; +import { + TabsContainer, + type TabRouteConfig, + DEFAULT_TAB_ROUTE_OPTIONS, +} from '@apps/shared/gamma/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 + (`tintingMode: 'original'`) and is template(system)-tinted otherwise. + + + Sys (unaltered): a built-in star. Size unaltered defaults to 24dp. + + + 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', + tintingMode: 'template', + }, + selectedIcon: { + type: 'drawableResource', + name: 'person_walking', + tintingMode: 'original', + }, + 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, + }, + }, + }, +]; + +export default function TabsDrawableIconSize() { + 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, + }, +}); From dd3951133433db9a130c212caa2fb043cb7b7b0c Mon Sep 17 00:00:00 2001 From: stachbial Date: Sun, 28 Jun 2026 16:51:42 +0200 Subject: [PATCH 4/5] feat(tabs): use tinted boolean for drawable icons; add iOS custom symbol fallback --- .../rnscreens/gamma/tabs/screen/TabsScreen.kt | 14 +++++++------- .../gamma/tabs/screen/TabsScreenViewManager.kt | 12 ++++++------ apps/src/screens/TabsDrawableIconSize.tsx | 6 +++--- ios/tabs/RNSTabBarAppearanceCoordinator.mm | 7 +++++-- src/components/tabs/screen/TabsScreen.android.tsx | 14 +++++++------- .../tabs/TabsScreenAndroidNativeComponent.ts | 5 ++--- src/types.tsx | 7 +++---- 7 files changed, 33 insertions(+), 32 deletions(-) diff --git a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/screen/TabsScreen.kt b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/screen/TabsScreen.kt index 8f13821734..60b562b01f 100644 --- a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/screen/TabsScreen.kt +++ b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/screen/TabsScreen.kt @@ -76,7 +76,7 @@ class TabsScreen( if (newValue != oldValue) rebuildIcon() } - var drawableIconTintingMode: String = "template" + var drawableIconTinted: Boolean = true set(value) { if (field != value) { field = value @@ -88,7 +88,7 @@ class TabsScreen( if (newValue != oldValue) rebuildSelectedIcon() } - var selectedDrawableIconTintingMode: String = "template" + var selectedDrawableIconTinted: Boolean = true set(value) { if (field != value) { field = value @@ -102,21 +102,21 @@ class TabsScreen( val effectiveIconSizeDp: Float get() = if (drawableIconSize > 0f) drawableIconSize else DEFAULT_ICON_SIZE_DP - // "original" keeps the drawable's own colors; the bar can't tint a NoTintDrawable. + // A NoTintDrawable keeps the drawable's own colors; the bar can't tint it. private fun resolveIcon( resourceName: String?, - tintingMode: String, + tinted: Boolean, ): Drawable? { val drawable = getSystemDrawableResource(reactContext, resourceName) ?: return null - return if (tintingMode == "original") NoTintDrawable(drawable) else drawable + return if (tinted) drawable else NoTintDrawable(drawable) } private fun rebuildIcon() { - icon = resolveIcon(drawableIconResourceName, drawableIconTintingMode) + icon = resolveIcon(drawableIconResourceName, drawableIconTinted) } private fun rebuildSelectedIcon() { - selectedIcon = resolveIcon(selectedDrawableIconResourceName, selectedDrawableIconTintingMode) + selectedIcon = resolveIcon(selectedDrawableIconResourceName, selectedDrawableIconTinted) } var icon: Drawable? by Delegates.observable(null) { _, oldValue, newValue -> diff --git a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/screen/TabsScreenViewManager.kt b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/screen/TabsScreenViewManager.kt index 9a07abf4e2..8822920015 100644 --- a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/screen/TabsScreenViewManager.kt +++ b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/screen/TabsScreenViewManager.kt @@ -134,18 +134,18 @@ class TabsScreenViewManager : view.selectedDrawableIconResourceName = value } - override fun setDrawableIconTintingMode( + override fun setDrawableIconTinted( view: TabsScreen, - value: String?, + value: Boolean, ) { - view.drawableIconTintingMode = value ?: "template" + view.drawableIconTinted = value } - override fun setSelectedDrawableIconTintingMode( + override fun setSelectedDrawableIconTinted( view: TabsScreen, - value: String?, + value: Boolean, ) { - view.selectedDrawableIconTintingMode = value ?: "template" + view.selectedDrawableIconTinted = value } override fun setDrawableIconSize( diff --git a/apps/src/screens/TabsDrawableIconSize.tsx b/apps/src/screens/TabsDrawableIconSize.tsx index b981af626c..841b0f0313 100644 --- a/apps/src/screens/TabsDrawableIconSize.tsx +++ b/apps/src/screens/TabsDrawableIconSize.tsx @@ -20,7 +20,7 @@ function TabScreen() { Multicolor Tint: a VectorDrawable that keeps its own colors when focused - (`tintingMode: 'original'`) and is template(system)-tinted otherwise. + (`tinted: false`) and is template(system)-tinted otherwise. Sys (unaltered): a built-in star. Size unaltered defaults to 24dp. @@ -75,12 +75,12 @@ const ROUTES: TabRouteConfig[] = [ icon: { type: 'drawableResource', name: 'person_walking', - tintingMode: 'template', + tinted: true, }, selectedIcon: { type: 'drawableResource', name: 'person_walking', - tintingMode: 'original', + tinted: false, }, standardAppearance: INDICATOR, }, diff --git a/ios/tabs/RNSTabBarAppearanceCoordinator.mm b/ios/tabs/RNSTabBarAppearanceCoordinator.mm index fee81a8fa6..9d52ec8a53 100644 --- a/ios/tabs/RNSTabBarAppearanceCoordinator.mm +++ b/ios/tabs/RNSTabBarAppearanceCoordinator.mm @@ -62,7 +62,9 @@ - (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]; + // Fall back to a custom symbol from the app's asset catalog. + tabBarItem.image = [UIImage systemImageNamed:screenView.iconResourceName] + ?: [UIImage imageNamed:screenView.iconResourceName]; } else { tabBarItem.image = [UIImage imageNamed:screenView.iconResourceName]; } @@ -77,7 +79,8 @@ - (void)setIconsForTabBarItem:(UITabBarItem *)tabBarItem if (screenView.selectedIconResourceName != nil) { if (screenView.iconType == RNSTabsIconTypeSfSymbol) { - tabBarItem.selectedImage = [UIImage systemImageNamed:screenView.selectedIconResourceName]; + tabBarItem.selectedImage = [UIImage systemImageNamed:screenView.selectedIconResourceName] + ?: [UIImage imageNamed:screenView.selectedIconResourceName]; } else { tabBarItem.selectedImage = [UIImage imageNamed:screenView.selectedIconResourceName]; } diff --git a/src/components/tabs/screen/TabsScreen.android.tsx b/src/components/tabs/screen/TabsScreen.android.tsx index 9098702bda..4c175fb18f 100644 --- a/src/components/tabs/screen/TabsScreen.android.tsx +++ b/src/components/tabs/screen/TabsScreen.android.tsx @@ -131,10 +131,10 @@ function mapItemStateAppearanceToNativeProp( }; } -function drawableTintingModeOf( +function drawableTintedOf( icon: PlatformIconAndroid | undefined, -): 'template' | 'original' | undefined { - return icon?.type === 'drawableResource' ? icon.tintingMode : undefined; +): boolean | undefined { + return icon?.type === 'drawableResource' ? icon.tinted : undefined; } function parseIconsToNativeProps( @@ -143,10 +143,10 @@ function parseIconsToNativeProps( ): { imageIconResource?: ImageResolvedAssetSource | undefined; drawableIconResourceName?: string | undefined; - drawableIconTintingMode?: string | undefined; + drawableIconTinted?: boolean | undefined; selectedImageIconResource?: ImageResolvedAssetSource | undefined; selectedDrawableIconResourceName?: string | undefined; - selectedDrawableIconTintingMode?: string | undefined; + selectedDrawableIconTinted?: boolean | undefined; } { const parsedIcon = parseAndroidIconToNativeProps(icon); const parsedSelectedIcon = parseAndroidIconToNativeProps(selectedIcon); @@ -154,11 +154,11 @@ function parseIconsToNativeProps( return { imageIconResource: parsedIcon.imageIconResource, drawableIconResourceName: parsedIcon.drawableIconResourceName, - drawableIconTintingMode: drawableTintingModeOf(icon), + drawableIconTinted: drawableTintedOf(icon), selectedImageIconResource: parsedSelectedIcon.imageIconResource, selectedDrawableIconResourceName: parsedSelectedIcon.drawableIconResourceName, - selectedDrawableIconTintingMode: drawableTintingModeOf(selectedIcon), + selectedDrawableIconTinted: drawableTintedOf(selectedIcon), }; } diff --git a/src/fabric/tabs/TabsScreenAndroidNativeComponent.ts b/src/fabric/tabs/TabsScreenAndroidNativeComponent.ts index eb3d3c99f3..ee002450f4 100644 --- a/src/fabric/tabs/TabsScreenAndroidNativeComponent.ts +++ b/src/fabric/tabs/TabsScreenAndroidNativeComponent.ts @@ -105,9 +105,8 @@ export interface NativeProps extends ViewProps { imageIconResource?: ImageSource | undefined; selectedDrawableIconResourceName?: string | undefined; selectedImageIconResource?: ImageSource | undefined; - // 'template' (default) tints the drawable; 'original' keeps its own colors. - drawableIconTintingMode?: CT.WithDefault; - selectedDrawableIconTintingMode?: CT.WithDefault; + drawableIconTinted?: CT.WithDefault; + selectedDrawableIconTinted?: CT.WithDefault; // Per-tab icon size (dp); 0/unset = default 24dp. drawableIconSize?: CT.Float | undefined; diff --git a/src/types.tsx b/src/types.tsx index c6b5c516da..8b923ab612 100644 --- a/src/types.tsx +++ b/src/types.tsx @@ -120,11 +120,10 @@ export type PlatformIconAndroid = type: 'drawableResource'; name: string; /** - * How the bottom tab bar colors the icon: - * - 'template' (default): tint with the item icon color (selected/normal). - * - 'original': keep the drawable's own colors (e.g. multicolor icons). + * Whether the bottom tab bar tints the icon with the item icon color. + * Defaults to `true`; `false` keeps the drawable's own colors. */ - tintingMode?: 'template' | 'original'; + tinted?: boolean; } | PlatformIconShared; From 303c590d51071a93b6d0ee008cbb827cb7b41696 Mon Sep 17 00:00:00 2001 From: stachbial Date: Tue, 30 Jun 2026 18:18:43 +0200 Subject: [PATCH 5/5] chore: update example --- .../nano.swm.symbolset/Contents.json | 15 +++ .../nano.swm.symbolset/nano.swm.svg | 45 +++++++ .../nanomc.walker.imageset/Contents.json | 16 +++ .../nanomc.walker.imageset/nanomc.walker.svg | 26 ++++ apps/Example.tsx | 8 +- ...> CustomNativeBottomTabsIcons.android.tsx} | 2 +- .../CustomNativeBottomTabsIcons.ios.tsx | 112 ++++++++++++++++++ 7 files changed, 219 insertions(+), 5 deletions(-) create mode 100644 FabricExample/ios/FabricExample/Images.xcassets/nano.swm.symbolset/Contents.json create mode 100644 FabricExample/ios/FabricExample/Images.xcassets/nano.swm.symbolset/nano.swm.svg create mode 100644 FabricExample/ios/FabricExample/Images.xcassets/nanomc.walker.imageset/Contents.json create mode 100644 FabricExample/ios/FabricExample/Images.xcassets/nanomc.walker.imageset/nanomc.walker.svg rename apps/src/screens/{TabsDrawableIconSize.tsx => CustomNativeBottomTabsIcons.android.tsx} (98%) create mode 100644 apps/src/screens/CustomNativeBottomTabsIcons.ios.tsx 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 @@ + + + + + + Small + Medium + Large + + + Ultralight + Regular + Black + Generated from nano.swm + Template v.3.0 + Generated by react-native-nano-icons + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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/apps/Example.tsx b/apps/Example.tsx index 27a83c7060..e246c7e67a 100644 --- a/apps/Example.tsx +++ b/apps/Example.tsx @@ -27,7 +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 TabsDrawableIconSize from './src/screens/TabsDrawableIconSize'; +import CustomNativeBottomTabsIcons from './src/screens/CustomNativeBottomTabsIcons'; import { GestureDetectorProvider } from 'react-native-screens/gesture-handler'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; @@ -69,9 +69,9 @@ const SCREENS: Record< component: SwipeBackAnimation, type: 'example', }, - TabsDrawableIconSize: { - title: 'Tabs Drawable Icon Size', - component: TabsDrawableIconSize, + CustomNativeBottomTabsIcons: { + title: 'Custom Native Bottom Tabs Icons', + component: CustomNativeBottomTabsIcons, type: 'example', }, StackPresentation: { diff --git a/apps/src/screens/TabsDrawableIconSize.tsx b/apps/src/screens/CustomNativeBottomTabsIcons.android.tsx similarity index 98% rename from apps/src/screens/TabsDrawableIconSize.tsx rename to apps/src/screens/CustomNativeBottomTabsIcons.android.tsx index 841b0f0313..f17fb59be1 100644 --- a/apps/src/screens/TabsDrawableIconSize.tsx +++ b/apps/src/screens/CustomNativeBottomTabsIcons.android.tsx @@ -101,7 +101,7 @@ const ROUTES: TabRouteConfig[] = [ }, ]; -export default function TabsDrawableIconSize() { +export default function CustomNativeBottomTabsIcons() { return ; } diff --git a/apps/src/screens/CustomNativeBottomTabsIcons.ios.tsx b/apps/src/screens/CustomNativeBottomTabsIcons.ios.tsx new file mode 100644 index 0000000000..d67719e293 --- /dev/null +++ b/apps/src/screens/CustomNativeBottomTabsIcons.ios.tsx @@ -0,0 +1,112 @@ +import React from 'react'; +import { StyleSheet, Text, View } from 'react-native'; +import { + TabsContainer, + type TabRouteConfig, + DEFAULT_TAB_ROUTE_OPTIONS, +} from '@apps/shared/gamma/containers/tabs'; +import { Colors } from '@apps/shared/styling'; + +function TabScreen() { + return ( + + Custom asset-catalog tab icons + + SWM 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. + + + SWM 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. + + + ); +} + +const ROUTES: TabRouteConfig[] = [ + { + name: 'SWM_SYMBOL', + Component: TabScreen, + options: { + ...DEFAULT_TAB_ROUTE_OPTIONS, + title: 'SWM Symbol', + ios: { + icon: { type: 'sfSymbol', name: 'nano.swm' }, + }, + }, + }, + { + name: 'SWM_TINTED', + Component: TabScreen, + options: { + ...DEFAULT_TAB_ROUTE_OPTIONS, + title: 'SWM 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' }, + }, + }, + }, +]; + +export default function CustomNativeBottomTabsIcons() { + 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, + }, +});