Skip to content

Commit

Permalink
Fix animated icon glitches
Browse files Browse the repository at this point in the history
Swapping the eye icon was leading to color glitches, due to some bug deep within react-native-reanimated.

The solution involves rendering a single `Animated.Text` component, and simply changing the text content inside. This avoids re-wiring the Reanimated dependency graph, which is what seems to be causing the problem.

We achieve this with *almost* the same icon API as before by adding an `off` property to the eye icon, to switch between the two variants. We also refactor the animated icon implementation to use a raw text element, instead of the react-native-vector-icon wrapper component.
  • Loading branch information
swansontec committed Jan 17, 2024
1 parent 435537d commit 2462984
Show file tree
Hide file tree
Showing 7 changed files with 116 additions and 182 deletions.
50 changes: 14 additions & 36 deletions src/__tests__/modals/__snapshots__/CategoryModal.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -704,25 +704,14 @@ exports[`CategoryModal should render with a subcategory 1`] = `
}
>
<Text
adjustsFontSizeToFit={true}
allowFontScaling={false}
style={
[
{
"color": undefined,
"fontSize": 12,
},
{
"color": "#FFFFFF",
"fontSize": 22,
},
{
"fontFamily": "anticon",
"fontStyle": "normal",
"fontWeight": "normal",
},
{},
]
{
"color": "#FFFFFF",
"fontFamily": "anticon",
"fontSize": 22,
"fontStyle": "normal",
"fontWeight": "normal",
}
}
>
Expand Down Expand Up @@ -2135,25 +2124,14 @@ exports[`CategoryModal should render with an empty subcategory 1`] = `
}
>
<Text
adjustsFontSizeToFit={true}
allowFontScaling={false}
style={
[
{
"color": undefined,
"fontSize": 12,
},
{
"color": "#FFFFFF",
"fontSize": 0,
},
{
"fontFamily": "anticon",
"fontStyle": "normal",
"fontWeight": "normal",
},
{},
]
{
"color": "#FFFFFF",
"fontFamily": "anticon",
"fontSize": 0,
"fontStyle": "normal",
"fontWeight": "normal",
}
}
>
Expand Down
50 changes: 14 additions & 36 deletions src/__tests__/modals/__snapshots__/WalletListModal.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -209,25 +209,14 @@ exports[`WalletListModal should render with loading props 1`] = `
}
>
<Text
adjustsFontSizeToFit={true}
allowFontScaling={false}
style={
[
{
"color": undefined,
"fontSize": 12,
},
{
"color": "#FFFFFF",
"fontSize": 22,
},
{
"fontFamily": "anticon",
"fontStyle": "normal",
"fontWeight": "normal",
},
{},
]
{
"color": "#FFFFFF",
"fontFamily": "anticon",
"fontSize": 22,
"fontStyle": "normal",
"fontWeight": "normal",
}
}
>
Expand Down Expand Up @@ -392,25 +381,14 @@ exports[`WalletListModal should render with loading props 1`] = `
}
>
<Text
adjustsFontSizeToFit={true}
allowFontScaling={false}
style={
[
{
"color": undefined,
"fontSize": 12,
},
{
"color": "#FFFFFF",
"fontSize": 0,
},
{
"fontFamily": "anticon",
"fontStyle": "normal",
"fontWeight": "normal",
},
{},
]
{
"color": "#FFFFFF",
"fontFamily": "anticon",
"fontSize": 0,
"fontStyle": "normal",
"fontWeight": "normal",
}
}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -639,25 +639,14 @@ exports[`CreateWalletAccountSelect renders 1`] = `
}
>
<Text
adjustsFontSizeToFit={true}
allowFontScaling={false}
style={
[
{
"color": undefined,
"fontSize": 12,
},
{
"color": "#FFFFFF",
"fontSize": 0,
},
{
"fontFamily": "anticon",
"fontStyle": "normal",
"fontWeight": "normal",
},
{},
]
{
"color": "#FFFFFF",
"fontFamily": "anticon",
"fontSize": 0,
"fontStyle": "normal",
"fontWeight": "normal",
}
}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -613,25 +613,14 @@ exports[`CreateWalletImportScene should render with loading props 1`] = `
}
>
<Text
adjustsFontSizeToFit={true}
allowFontScaling={false}
style={
[
{
"color": undefined,
"fontSize": 12,
},
{
"color": "#FFFFFF",
"fontSize": 0,
},
{
"fontFamily": "anticon",
"fontStyle": "normal",
"fontWeight": "normal",
},
{},
]
{
"color": "#FFFFFF",
"fontFamily": "anticon",
"fontSize": 0,
"fontStyle": "normal",
"fontWeight": "normal",
}
}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -379,25 +379,14 @@ exports[`CreateWalletSelectCrypto should render with loading props 1`] = `
}
>
<Text
adjustsFontSizeToFit={true}
allowFontScaling={false}
style={
[
{
"color": undefined,
"fontSize": 12,
},
{
"color": "#FFFFFF",
"fontSize": 22,
},
{
"fontFamily": "anticon",
"fontStyle": "normal",
"fontWeight": "normal",
},
{},
]
{
"color": "#FFFFFF",
"fontFamily": "anticon",
"fontSize": 22,
"fontStyle": "normal",
"fontWeight": "normal",
}
}
>
Expand Down Expand Up @@ -563,25 +552,14 @@ exports[`CreateWalletSelectCrypto should render with loading props 1`] = `
}
>
<Text
adjustsFontSizeToFit={true}
allowFontScaling={false}
style={
[
{
"color": undefined,
"fontSize": 12,
},
{
"color": "#FFFFFF",
"fontSize": 0,
},
{
"fontFamily": "anticon",
"fontStyle": "normal",
"fontWeight": "normal",
},
{},
]
{
"color": "#FFFFFF",
"fontFamily": "anticon",
"fontSize": 0,
"fontStyle": "normal",
"fontWeight": "normal",
}
}
>
Expand Down
94 changes: 58 additions & 36 deletions src/components/icons/ThemedIcons.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import React from 'react'
import Animated, { AnimatedProps, SharedValue, useAnimatedStyle } from 'react-native-reanimated'
import Animated, { SharedValue, useAnimatedStyle } from 'react-native-reanimated'
import AntDesignIcon from 'react-native-vector-icons/AntDesign'
import { IconProps as VectorIconProps } from 'react-native-vector-icons/Icon'
import { type Icon } from 'react-native-vector-icons/Icon'
import IonIcon from 'react-native-vector-icons/Ionicons'

import { Fontello } from '../../assets/vector'
import { useTheme } from '../services/ThemeContext'

//
// Types

//

export interface AnimatedIconProps {
Expand All @@ -27,56 +26,79 @@ export interface IconProps {
export type IconComponent = React.FunctionComponent<IconProps>

//
// HOCs
// Inner components
//

function makeAnimatedFontIcon(IconComponent: React.ComponentType<AnimatedProps<VectorIconProps>>, name: string): AnimatedIconComponent {
return (props: AnimatedIconProps) => {
const { accessible, color, size } = props
const { icon, rem } = useTheme()
const oneRem = rem(1)
interface IconChoice {
IconComponent: typeof Icon
name: string
}

function AnimatedFontIcon(props: AnimatedIconProps & IconChoice): JSX.Element {
const { accessible, color, IconComponent, name, size } = props
const theme = useTheme()
const defaultColor = theme.icon
const defaultSize = theme.rem(1)

const fontFamily = IconComponent.getFontFamily()
const glyphMap = IconComponent.getRawGlyphMap()

const style = useAnimatedStyle(() => ({
color: color?.value ?? defaultColor,
fontFamily,
fontSize: size?.value ?? defaultSize,
fontStyle: 'normal',
fontWeight: 'normal'
}))

return (
<Animated.Text accessible={accessible} style={style}>
{String.fromCodePoint(glyphMap[name])}
</Animated.Text>
)
}

const style = useAnimatedStyle(() => ({
color: color?.value ?? icon,
fontSize: size?.value ?? oneRem
}))
function ThemedFontIcon(props: IconProps & IconChoice): JSX.Element {
const theme = useTheme()
const { accessible, color = theme.icon, IconComponent, name, size = theme.rem(1) } = props

return <IconComponent accessible={accessible} name={name} adjustsFontSizeToFit style={style} />
const style = {
color: color,
fontSize: size
}
return <IconComponent accessible={accessible} name={name} adjustsFontSizeToFit style={style} />
}

function makeFontIcon(IconComponent: React.ComponentType<VectorIconProps>, name: string): IconComponent {
return (props: IconProps) => {
const { icon, rem } = useTheme()
const { accessible, color = icon, size = rem(1) } = props
//
// HOC's
//

function makeAnimatedFontIcon(IconComponent: typeof Icon, name: string): AnimatedIconComponent {
return props => AnimatedFontIcon({ ...props, IconComponent, name })
}

const style = {
color: color,
fontSize: size
}
return <IconComponent accessible={accessible} name={name} adjustsFontSizeToFit style={style} />
}
function makeFontIcon(IconComponent: typeof Icon, name: string): IconComponent {
return props => ThemedFontIcon({ ...props, IconComponent, name })
}

//
// Font Icons
//

const AnimatedAntDesignIcon = Animated.createAnimatedComponent(AntDesignIcon)
const AnimatedFontello = Animated.createAnimatedComponent(Fontello)
const AnimatedIonIcon = Animated.createAnimatedComponent(IonIcon)
export function EyeIconAnimated(props: AnimatedIconProps & { off: boolean }): JSX.Element {
const { off, ...rest } = props
return AnimatedFontIcon({
...rest,
IconComponent: IonIcon,
name: off ? 'eye-off-outline' : 'eye-outline'
})
}

export const CloseIcon = makeFontIcon(AntDesignIcon, 'close')
export const CloseIconAnimated = makeAnimatedFontIcon(AnimatedAntDesignIcon, 'close')

export const EyeIcon = makeFontIcon(IonIcon, 'eye-outline')
export const EyeIconAnimated = makeAnimatedFontIcon(AnimatedIonIcon, 'eye-outline')

export const EyeOffIcon = makeFontIcon(IonIcon, 'eye-off-outline')
export const EyeOffIconAnimated = makeAnimatedFontIcon(AnimatedIonIcon, 'eye-off-outline')
export const CloseIconAnimated = makeAnimatedFontIcon(AntDesignIcon, 'close')

export const FlipIcon = makeFontIcon(Fontello, 'exchange')
export const FlipIconAnimated = makeAnimatedFontIcon(AnimatedFontello, 'exchange')
export const FlipIconAnimated = makeAnimatedFontIcon(Fontello, 'exchange')

export const SearchIcon = makeFontIcon(AntDesignIcon, 'search1')
export const SearchIconAnimated = makeAnimatedFontIcon(AnimatedAntDesignIcon, 'search1')
export const SearchIconAnimated = makeAnimatedFontIcon(AntDesignIcon, 'search1')
Loading

0 comments on commit 2462984

Please sign in to comment.