forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {useEffect, useRef, useState} from 'react'
2import {View} from 'react-native'
3import Animated, {
4 Easing,
5 LayoutAnimationConfig,
6 useReducedMotion,
7 withTiming,
8} from 'react-native-reanimated'
9
10import {decideShouldRoll} from '#/lib/custom-animations/util'
11import {atoms as a} from '#/alf'
12
13const animationConfig = {
14 duration: 400,
15 easing: Easing.out(Easing.cubic),
16}
17
18function EnteringUp() {
19 'worklet'
20 const animations = {
21 opacity: withTiming(1, animationConfig),
22 transform: [{translateY: withTiming(0, animationConfig)}],
23 }
24 const initialValues = {
25 opacity: 0,
26 transform: [{translateY: 18}],
27 }
28 return {
29 animations,
30 initialValues,
31 }
32}
33
34function EnteringDown() {
35 'worklet'
36 const animations = {
37 opacity: withTiming(1, animationConfig),
38 transform: [{translateY: withTiming(0, animationConfig)}],
39 }
40 const initialValues = {
41 opacity: 0,
42 transform: [{translateY: -18}],
43 }
44 return {
45 animations,
46 initialValues,
47 }
48}
49
50function ExitingUp() {
51 'worklet'
52 const animations = {
53 opacity: withTiming(0, animationConfig),
54 transform: [
55 {
56 translateY: withTiming(-18, animationConfig),
57 },
58 ],
59 }
60 const initialValues = {
61 opacity: 1,
62 transform: [{translateY: 0}],
63 }
64 return {
65 animations,
66 initialValues,
67 }
68}
69
70function ExitingDown() {
71 'worklet'
72 const animations = {
73 opacity: withTiming(0, animationConfig),
74 transform: [{translateY: withTiming(18, animationConfig)}],
75 }
76 const initialValues = {
77 opacity: 1,
78 transform: [{translateY: 0}],
79 }
80 return {
81 animations,
82 initialValues,
83 }
84}
85
86export function CountWheel({
87 count,
88 isToggled,
89 hasBeenToggled,
90 renderCount,
91}: {
92 count: number
93 isToggled: boolean
94 hasBeenToggled: boolean
95 renderCount: (props: {count: number}) => React.ReactNode
96}) {
97 const shouldAnimate = !useReducedMotion() && hasBeenToggled
98 const shouldRoll = decideShouldRoll(isToggled, count)
99
100 // Incrementing the key will cause the `Animated.View` to re-render, with the newly selected entering/exiting
101 // animation
102 // The initial entering/exiting animations will get skipped, since these will happen on screen mounts and would
103 // be unnecessary
104 const [key, setKey] = useState(0)
105 const [prevCount, setPrevCount] = useState(count)
106 const prevIsToggled = useRef(isToggled)
107
108 useEffect(() => {
109 if (isToggled === prevIsToggled.current) {
110 return
111 }
112
113 const newPrevCount = isToggled ? count - 1 : count + 1
114 setKey(prev => prev + 1)
115 setPrevCount(newPrevCount)
116 prevIsToggled.current = isToggled
117 }, [isToggled, count])
118
119 const enteringAnimation =
120 shouldAnimate && shouldRoll
121 ? isToggled
122 ? EnteringUp
123 : EnteringDown
124 : undefined
125 const exitingAnimation =
126 shouldAnimate && shouldRoll
127 ? isToggled
128 ? ExitingUp
129 : ExitingDown
130 : undefined
131
132 return (
133 <LayoutAnimationConfig skipEntering skipExiting>
134 {count > 0 ? (
135 <View style={[a.justify_center]}>
136 <Animated.View entering={enteringAnimation} key={key}>
137 {renderCount({count})}
138 </Animated.View>
139 {shouldAnimate && (count > 1 || !isToggled) ? (
140 <Animated.View
141 entering={exitingAnimation}
142 // Add 2 to the key so there are never duplicates
143 key={key + 2}
144 style={[a.absolute, {width: 50, opacity: 0}]}
145 aria-disabled={true}>
146 {renderCount({count: prevCount})}
147 </Animated.View>
148 ) : null}
149 </View>
150 ) : null}
151 </LayoutAnimationConfig>
152 )
153}