Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 22 additions & 70 deletions apps/mobile/src/components/IconFilter.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import React, { useEffect, useRef, useState } from 'react'
import { Animated, StyleSheet, TouchableOpacity, View } from 'react-native'
import { Animated, TouchableOpacity } from 'react-native'
import { Ionicons } from '@expo/vector-icons'

import { palette } from '../styles'

import { hexToRgb } from '../utils/color'

export function IconFilter({
onPress,
disabled = false,
Expand All @@ -17,29 +19,15 @@ export function IconFilter({
isPulsating?: boolean,
testID?: string,
}) {
const defaultIconColor = color || palette.textPrimary

const pulseAnim = useRef(new Animated.Value(0)).current
const iconColorAnim = useRef(new Animated.Value(0)).current
const [iconColor, setIconColor] = useState('#fff')
const [iconColor, setIconColor] = useState(defaultIconColor)

useEffect(() => {
if (isPulsating) {
// Pulsate the background circle
Animated.loop(
Animated.sequence([
Animated.timing(pulseAnim, {
toValue: 1,
duration: 1000,
useNativeDriver: true,
}),
Animated.timing(pulseAnim, {
toValue: 0,
duration: 1000,
useNativeDriver: true,
}),
])
).start()

// Pulsate the icon color (white to black)
// Pulsate the icon color
const colorAnimation = Animated.loop(
Animated.sequence([
Animated.timing(iconColorAnim, {
Expand All @@ -57,12 +45,14 @@ export function IconFilter({

colorAnimation.start()

const targetRgb = hexToRgb(defaultIconColor)

// Listen to color animation changes
const listenerId = iconColorAnim.addListener(({ value }) => {
// Interpolate from white (#fff) to black (#000)
const r = Math.round(255 * (1 - value))
const g = Math.round(255 * (1 - value))
const b = Math.round(255 * (1 - value))
// Interpolate from white (#fff) to defaultIconColor
const r = Math.round(255 + (targetRgb.r - 255) * value)
const g = Math.round(255 + (targetRgb.g - 255) * value)
const b = Math.round(255 + (targetRgb.b - 255) * value)
setIconColor(`rgb(${r}, ${g}, ${b})`)
})

Expand All @@ -73,47 +63,19 @@ export function IconFilter({
} else {
pulseAnim.setValue(0)
iconColorAnim.setValue(0)
setIconColor('#fff')
setIconColor(defaultIconColor)
}
}, [isPulsating, pulseAnim, iconColorAnim])
}, [isPulsating, pulseAnim, iconColorAnim, defaultIconColor])

const backgroundOpacity = pulseAnim.interpolate({
inputRange: [0, 1],
outputRange: [0, 1],
})

const backgroundScale = pulseAnim.interpolate({
inputRange: [0, 1],
outputRange: [0.8, 1],
})

const icon = isPulsating ? (
<Animated.View style={{ alignItems: 'center', justifyContent: 'center', width: 32, height: 32 }}>
<Animated.View
style={[
styles.pulsatingBackground,
{
opacity: backgroundOpacity,
transform: [{ scale: backgroundScale }],
},
]}
const icon = (
<Animated.View>
<Ionicons
name='options'
size={24}
color={iconColor}
style={{ opacity: disabled ? 0.25 : 1 }}
/>
<View style={{ position: 'absolute' }}>
<Ionicons
name="options"
size={24}
color={iconColor}
style={{ opacity: disabled ? 0.25 : 1 }}
/>
</View>
</Animated.View>
) : (
<Ionicons
name="options"
size={24}
color={color || palette.textPrimary}
style={{ opacity: disabled ? 0.25 : 1 }}
/>
)

if (onPress) {
Expand All @@ -134,13 +96,3 @@ export function IconFilter({

return icon
}

const styles = StyleSheet.create({
pulsatingBackground: {
position: 'absolute',
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: '#fff',
},
})
20 changes: 20 additions & 0 deletions apps/mobile/src/utils/color.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export function hexToRgb(hex: string) {
try {
let c = hex.replace('#', '')
if (c.length === 3) {
c = c.split('').map(x => x + x).join('')
}
const num = parseInt(c, 16)

if (isNaN(num)) throw new Error('Invalid hex')

return {
r: (num >> 16) & 255,
g: (num >> 8) & 255,
b: num & 255,
}
} catch (e) {
// Fallback to textPrimary rgb(31, 41, 51)
return { r: 31, g: 41, b: 51 }
}
}