diff --git a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/host/TabsHostAppearanceApplicator.kt b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/host/TabsHostAppearanceApplicator.kt index 4cca642026..0f219dc3bd 100644 --- a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/host/TabsHostAppearanceApplicator.kt +++ b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/host/TabsHostAppearanceApplicator.kt @@ -2,6 +2,7 @@ package com.swmansion.rnscreens.gamma.tabs.host import android.annotation.SuppressLint import android.content.res.ColorStateList +import android.graphics.drawable.StateListDrawable import android.util.TypedValue import android.view.MenuItem import android.view.ViewGroup @@ -160,8 +161,18 @@ class TabsHostAppearanceApplicator( menuItem.title = tabsScreen.tabTitle } - if (menuItem.icon != tabsScreen.icon) { - menuItem.icon = tabsScreen.icon + val targetIcon = + if (tabsScreen.selectedIcon != null && tabsScreen.icon != null) { + StateListDrawable().apply { + addState(intArrayOf(android.R.attr.state_checked), tabsScreen.selectedIcon?.mutate()) + addState(intArrayOf(), tabsScreen.icon?.mutate()) + } + } else { + tabsScreen.icon + } + + if (menuItem.icon != targetIcon) { + menuItem.icon = targetIcon } } diff --git a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/image/TabsImageLoader.kt b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/image/TabsImageLoader.kt index 578448927a..4626e83c43 100644 --- a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/image/TabsImageLoader.kt +++ b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/image/TabsImageLoader.kt @@ -23,6 +23,7 @@ internal fun loadTabImage( context: Context, uri: String, view: TabsScreen, + isSelected: Boolean, ) { // Since image loading might happen on a background thread // ref. https://frescolib.org/docs/intro-image-pipeline.html @@ -30,7 +31,11 @@ internal fun loadTabImage( val resolvedUri = ImageSource(context, uri).getUri(context) ?: return loadTabImageInternal(context, resolvedUri) { drawable -> Handler(Looper.getMainLooper()).post { - view.icon = drawable + if (isSelected) { + view.selectedIcon = drawable + } else { + view.icon = drawable + } } } } 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 8ba0c387ed..20416e4bd5 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 @@ -73,10 +73,20 @@ class TabsScreen( } } + var selectedDrawableIconResourceName: String? by Delegates.observable(null) { _, oldValue, newValue -> + if (newValue != oldValue) { + selectedIcon = getSystemDrawableResource(reactContext, newValue) + } + } + var icon: Drawable? by Delegates.observable(null) { _, oldValue, newValue -> updateMenuItemAttributesIfNeeded(oldValue, newValue) } + var selectedIcon: Drawable? by Delegates.observable(null) { _, oldValue, newValue -> + updateMenuItemAttributesIfNeeded(oldValue, newValue) + } + var shouldUseRepeatedTabSelectionScrollToTopSpecialEffect: Boolean = true var shouldUseRepeatedTabSelectionPopToRootSpecialEffect: Boolean = true 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 77cc5b6e1d..e8b31357dd 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 @@ -214,6 +214,14 @@ class TabsScreenViewManager : view.drawableIconResourceName = value } + @ReactProp(name = "selectedDrawableIconResourceName") + override fun setSelectedDrawableIconResourceName( + view: TabsScreen, + value: String?, + ) { + view.selectedDrawableIconResourceName = value + } + override fun setOrientation( view: TabsScreen, value: String?, @@ -236,7 +244,18 @@ class TabsScreenViewManager : ) { val uri = value?.getString("uri") if (uri != null) { - loadTabImage(view.context, uri, view) + loadTabImage(view.context, uri, view, false) + } + } + + @ReactProp(name = "selectedImageIconResource") + override fun setSelectedImageIconResource( + view: TabsScreen, + value: ReadableMap?, + ) { + val uri = value?.getString("uri") + if (uri != null) { + loadTabImage(view.context, uri, view, true) } } 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..2c7fc7c432 100644 --- a/android/src/paper/java/com/facebook/react/viewmanagers/RNSTabsScreenManagerDelegate.java +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNSTabsScreenManagerDelegate.java @@ -56,6 +56,12 @@ public void setProperty(T view, String propName, @Nullable Object value) { case "imageIconResource": mViewManager.setImageIconResource(view, (ReadableMap) value); break; + case "selectedDrawableIconResourceName": + mViewManager.setSelectedDrawableIconResourceName(view, value == null ? null : (String) value); + break; + case "selectedImageIconResource": + mViewManager.setSelectedImageIconResource(view, (ReadableMap) value); + break; case "tabBarItemBadgeTextColor": mViewManager.setTabBarItemBadgeTextColor(view, ColorPropConverter.getColor(value, view.getContext())); break; 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..d2dc24436c 100644 --- a/android/src/paper/java/com/facebook/react/viewmanagers/RNSTabsScreenManagerInterface.java +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNSTabsScreenManagerInterface.java @@ -26,6 +26,8 @@ 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 setSelectedDrawableIconResourceName(T view, @Nullable String value); + void setSelectedImageIconResource(T view, @Nullable ReadableMap value); void setTabBarItemBadgeTextColor(T view, @Nullable Integer value); void setTabBarItemBadgeBackgroundColor(T view, @Nullable Integer value); void setStandardAppearance(T view, Dynamic value); diff --git a/apps/src/tests/issue-tests/Test3443.tsx b/apps/src/tests/issue-tests/Test3443.tsx index 4f58aac766..252dac1967 100644 --- a/apps/src/tests/issue-tests/Test3443.tsx +++ b/apps/src/tests/issue-tests/Test3443.tsx @@ -44,8 +44,10 @@ const TAB_CONFIGS: TabConfiguration[] = [ }, }, selectedIcon: { - type: 'xcasset', - name: 'custom-icon-fill', + ios: { + type: 'xcasset', + name: 'custom-icon-fill', + }, }, }, component: makeTab( diff --git a/apps/src/tests/issue-tests/TestBottomTabs/index.tsx b/apps/src/tests/issue-tests/TestBottomTabs/index.tsx index 31b096f684..4b3dadc7ad 100644 --- a/apps/src/tests/issue-tests/TestBottomTabs/index.tsx +++ b/apps/src/tests/issue-tests/TestBottomTabs/index.tsx @@ -45,12 +45,18 @@ const TAB_CONFIGS: TabConfiguration[] = [ }, android: { type: 'imageSource', - imageSource: require('../../../../assets/variableIcons/icon_fill.png'), + imageSource: require('../../../../assets/variableIcons/icon.png'), }, }, selectedIcon: { - type: 'sfSymbol', - name: 'house.fill', + ios: { + type: 'sfSymbol', + name: 'house.fill', + }, + android: { + type: 'imageSource', + imageSource: require('../../../../assets/variableIcons/icon_fill.png'), + } }, }, component: Tab1, @@ -120,8 +126,14 @@ const TAB_CONFIGS: TabConfiguration[] = [ }, }, selectedIcon: { - type: 'templateSource', - templateSource: require('../../../../assets/variableIcons/icon_fill.png'), + ios: { + type: 'templateSource', + templateSource: require('../../../../assets/variableIcons/icon_fill.png'), + }, + android: { + type: 'drawableResource', + name: 'sym_call_incoming', + } }, title: 'Tab2', }, @@ -163,8 +175,10 @@ const TAB_CONFIGS: TabConfiguration[] = [ }, }, selectedIcon: { - type: 'imageSource', - imageSource: require('../../../../assets/variableIcons/icon_fill.png'), + shared: { + type: 'imageSource', + imageSource: require('../../../../assets/variableIcons/icon_fill.png'), + } }, title: 'Tab3', // systemItem: 'search', // iOS specific @@ -191,8 +205,14 @@ const TAB_CONFIGS: TabConfiguration[] = [ }, }, selectedIcon: { - type: 'sfSymbol', - name: 'rectangle.stack.fill', + ios: { + type: 'sfSymbol', + name: 'rectangle.stack.fill', + }, + android: { + type: 'drawableResource', + name: 'custom_home_icon', + }, }, title: 'Tab4', systemItem: 'search', // iOS specific diff --git a/apps/src/tests/issue-tests/TestBottomTabsOrientation.tsx b/apps/src/tests/issue-tests/TestBottomTabsOrientation.tsx index fc3e4d9c79..b2ee0b0f96 100644 --- a/apps/src/tests/issue-tests/TestBottomTabsOrientation.tsx +++ b/apps/src/tests/issue-tests/TestBottomTabsOrientation.tsx @@ -92,8 +92,14 @@ function makeTabConfigs( }, }, selectedIcon: { - type: 'sfSymbol', - name: 'house.fill', + ios: { + type: 'sfSymbol', + name: 'house.fill', + }, + android: { + type: 'imageSource', + imageSource: require('../../../assets/variableIcons/icon_fill.png'), + }, }, orientation: tabsOrientations.home.tabScreen, }, @@ -117,8 +123,14 @@ function makeTabConfigs( }, }, selectedIcon: { - type: 'templateSource', - templateSource: require('../../../assets/variableIcons/icon_fill.png'), + ios: { + type: 'templateSource', + templateSource: require('../../../assets/variableIcons/icon_fill.png'), + }, + android: { + type: 'drawableResource', + name: 'sym_call_missed', + }, }, orientation: tabsOrientations.portrait.tabScreen, }, @@ -137,8 +149,10 @@ function makeTabConfigs( }, }, selectedIcon: { - type: 'imageSource', - imageSource: require('../../../assets/variableIcons/icon_fill.png'), + shared: { + type: 'imageSource', + imageSource: require('../../../assets/variableIcons/icon_fill.png'), + } }, orientation: tabsOrientations.landscape.tabScreen, }, diff --git a/src/components/tabs/TabsScreen.tsx b/src/components/tabs/TabsScreen.tsx index a147810f7a..e466801adf 100644 --- a/src/components/tabs/TabsScreen.tsx +++ b/src/components/tabs/TabsScreen.tsx @@ -315,22 +315,37 @@ function parseIOSIconToNativeProps(icon: PlatformIconIOS | undefined): { function parseIconsToNativeProps( icon: PlatformIcon | undefined, - selectedIcon: PlatformIconIOS | undefined, + selectedIcon: PlatformIcon | undefined, ): { imageIconResource?: ImageResolvedAssetSource; drawableIconResourceName?: string; iconType?: IconType; iconImageSource?: ImageSourcePropType; iconResourceName?: string; + // android + selectedImageIconResource?: ImageSourcePropType; + selectedDrawableIconResourceName?: string; + // iOS selectedIconImageSource?: ImageSourcePropType; selectedIconResourceName?: string; } { if (Platform.OS === 'android') { - const androidNativeProps = parseAndroidIconToNativeProps( - icon?.android || icon?.shared, - ); + const { imageIconResource, drawableIconResourceName } = + parseAndroidIconToNativeProps(icon?.android || icon?.shared); + + const androidSelectedSource = + (selectedIcon as PlatformIcon)?.android || + (selectedIcon as PlatformIcon)?.shared; + const selectedIconProps = androidSelectedSource + ? parseAndroidIconToNativeProps(androidSelectedSource) + : {}; + return { - ...androidNativeProps, + imageIconResource, + drawableIconResourceName, + selectedImageIconResource: selectedIconProps.imageIconResource, + selectedDrawableIconResourceName: + selectedIconProps.drawableIconResourceName, }; } @@ -338,11 +353,15 @@ function parseIconsToNativeProps( const { iconImageSource, iconResourceName, iconType } = parseIOSIconToNativeProps(icon?.ios || icon?.shared); + const iosSelectedSource = + (selectedIcon as PlatformIcon)?.ios || + (selectedIcon as PlatformIcon)?.shared; + const { iconImageSource: selectedIconImageSource, iconResourceName: selectedIconResourceName, iconType: selectedIconType, - } = parseIOSIconToNativeProps(selectedIcon); + } = parseIOSIconToNativeProps(iosSelectedSource); if ( iconType !== undefined && diff --git a/src/components/tabs/TabsScreen.types.ts b/src/components/tabs/TabsScreen.types.ts index 3fba9df140..d78530c8ac 100644 --- a/src/components/tabs/TabsScreen.types.ts +++ b/src/components/tabs/TabsScreen.types.ts @@ -8,7 +8,6 @@ import type { } from 'react-native'; import type { PlatformIcon, - PlatformIconIOS, UserInterfaceStyle, ScrollEdgeEffect, } from '../../types'; @@ -343,12 +342,22 @@ export interface TabsScreenProps { * * Remarks: Requires passing a drawable to resources via Android Studio. * - * On iOS, if no `selectedIcon` is provided, this icon will also + * If no `selectedIcon` is provided, this icon will also * be used as the selected state icon. * * @platform android, ios */ icon?: PlatformIcon; + /** + * @summary Specifies the icon for tab bar item when it is selected. + * + * Supports the same values as `icon` property for given platform. + * + * To use `selectedIcon`, `icon` must also be provided. + * + * @platform android, ios + */ + selectedIcon?: PlatformIcon; /** * @summary Specifies which special effects (also known as microinteractions) * are enabled for the tab screen. @@ -502,16 +511,6 @@ export interface TabsScreenProps { * @platform ios */ scrollEdgeAppearance?: TabsScreenAppearance; - /** - * @summary Specifies the icon for tab bar item when it is selected. - * - * Supports the same values as `icon` property for iOS. - * - * To use `selectedIcon`, `icon` must also be provided. - * - * @platform ios - */ - selectedIcon?: PlatformIconIOS; /** * @summary System-provided tab bar item with predefined icon and title * diff --git a/src/fabric/tabs/TabsScreenNativeComponent.ts b/src/fabric/tabs/TabsScreenNativeComponent.ts index f6ce662399..5306c1c8a7 100644 --- a/src/fabric/tabs/TabsScreenNativeComponent.ts +++ b/src/fabric/tabs/TabsScreenNativeComponent.ts @@ -134,6 +134,10 @@ export interface NativeProps extends ViewProps { // Android-specific image handling drawableIconResourceName?: string; imageIconResource?: ImageSource; + + selectedDrawableIconResourceName?: string; + selectedImageIconResource?: ImageSource; + tabBarItemBadgeTextColor?: ColorValue; tabBarItemBadgeBackgroundColor?: ColorValue;