Skip to content

Commit 8650ec1

Browse files
committed
Add animating and hidesWhenStopped props
1 parent e8d585e commit 8650ec1

File tree

15 files changed

+893
-702
lines changed

15 files changed

+893
-702
lines changed

example/App.tsx

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1-
import React from 'react'
2-
import { View, StatusBar, Text, StyleSheet } from 'react-native'
1+
import React, { useReducer } from 'react'
2+
import {
3+
View,
4+
StatusBar,
5+
Text,
6+
StyleSheet,
7+
TouchableOpacity,
8+
} from 'react-native'
39
import {
410
Plane,
511
Chase,
@@ -30,7 +36,23 @@ const spinners = [
3036
{ component: Fold, backgroundColor: '#2980b9' },
3137
]
3238

39+
function reducer(state, action) {
40+
switch (action.type) {
41+
case 'toggle_loading':
42+
return { ...state, [action.spinnerIndex]: !state[action.spinnerIndex] }
43+
default:
44+
return state
45+
}
46+
}
47+
48+
const initialState = spinners.reduce(
49+
(acc, _, index) => ({ ...acc, [index]: true }),
50+
{}
51+
)
52+
3353
export default function App() {
54+
const [state, dispatch] = useReducer(reducer, initialState)
55+
3456
return (
3557
<View style={styles.container}>
3658
<StatusBar barStyle="light-content" />
@@ -42,21 +64,28 @@ export default function App() {
4264
.slice(rowIndex * 3, rowIndex * 3 + 3)
4365
.map((spinner, index) => {
4466
const Spinner = spinner.component
67+
const spinnerIndex = rowIndex * 3 + index
4568
return (
46-
<View
69+
<TouchableOpacity
4770
style={[
4871
styles.cell,
4972
{
5073
backgroundColor: spinner.backgroundColor,
5174
},
5275
]}
5376
key={index}
77+
onPress={() => {
78+
dispatch({
79+
type: 'toggle_loading',
80+
spinnerIndex: spinnerIndex,
81+
})
82+
}}
5483
>
55-
<Spinner color="#FFF" />
84+
<Spinner color="#FFF" animating={state[spinnerIndex]} />
5685
<Text style={styles.componentLabel}>
57-
{`<${spinner.component.name} \/>`}
86+
{`<${spinner.component.name} />`}
5887
</Text>
59-
</View>
88+
</TouchableOpacity>
6089
)
6190
})}
6291
</View>

src/AnimationContainer.tsx

Lines changed: 71 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,93 @@
11
import * as React from 'react'
22
import { Animated } from 'react-native'
33

4-
export interface Props {
4+
interface AnimationNode {
55
animation: Animated.CompositeAnimation
6+
values: Animated.AnimatedInterpolation[]
67
}
7-
export default class AnimationContainer extends React.Component<Props> {
8+
9+
export interface Props<T extends string> {
10+
initAnimation: () => Record<T, (value: Animated.Value) => AnimationNode>
11+
children: (
12+
interpolationsByKey: Record<T, Animated.AnimatedInterpolation[]>
13+
) => React.ReactNode
14+
animating: boolean
15+
}
16+
17+
export default class AnimationContainer<
18+
T extends string
19+
> extends React.Component<Props<T>> {
820
animation: Animated.CompositeAnimation
21+
animatedValuesByKey: Record<T, Animated.Value> = {} as Record<
22+
T,
23+
Animated.Value
24+
>
25+
interpolationsByKey: Record<
26+
T,
27+
Animated.AnimatedInterpolation[]
28+
> = {} as Record<T, Animated.AnimatedInterpolation[]>
29+
30+
static defaultProps = {
31+
animating: true,
32+
}
933

10-
constructor(props: Props) {
34+
constructor(props: Props<T>) {
1135
super(props)
36+
const { initAnimation } = props
37+
38+
const animationInitializersByKey = initAnimation()
39+
const animations: Animated.CompositeAnimation[] = []
1240

13-
const { animation } = this.props
14-
this.animation = animation
41+
for (const key in animationInitializersByKey) {
42+
const animationInitializer = animationInitializersByKey[key]
43+
const animationValue = new Animated.Value(0)
44+
this.animatedValuesByKey[key] = animationValue
45+
const { animation, values } = animationInitializer(animationValue)
46+
animations.push(animation)
47+
this.interpolationsByKey[key] = values
48+
}
49+
50+
if (animations.length === 1) {
51+
this.animation = animations[0]
52+
} else {
53+
this.animation = Animated.parallel(animations)
54+
}
1555
}
1656

1757
componentDidMount() {
58+
this.startAnimation()
59+
}
60+
61+
componentDidUpdate(prevProps: Props<T>) {
62+
const { animating } = this.props
63+
64+
if (animating !== prevProps.animating) {
65+
if (animating) {
66+
this.startAnimation()
67+
} else {
68+
this.stopAnimation()
69+
}
70+
}
71+
}
72+
73+
startAnimation = () => {
1874
this.animation.start()
1975
}
2076

77+
stopAnimation = () => {
78+
this.animation.stop()
79+
80+
for (const key in this.animatedValuesByKey) {
81+
this.animatedValuesByKey[key].setValue(0)
82+
}
83+
}
84+
2185
componentWillUnmount() {
2286
this.animation.stop()
2387
}
2488

2589
render() {
26-
return this.props.children
90+
const { children } = this.props
91+
return children ? children(this.interpolationsByKey) : null
2792
}
2893
}

src/Bounce.tsx

Lines changed: 52 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,15 @@ import { stagger } from './utils'
77
export default class Bounce extends React.Component<SpinnerProps> {
88
static defaultProps = defaultProps
99

10-
value = new Animated.Value(0)
11-
animation: Animated.CompositeAnimation
12-
values: Animated.AnimatedInterpolation[]
13-
14-
constructor(props: SpinnerProps) {
15-
super(props)
16-
17-
const { animation, values } = stagger(1000, 2, {
18-
duration: 2000,
19-
value: this.value,
20-
keyframes: [0, 45, 55, 100],
21-
})
22-
23-
this.animation = animation
24-
this.values = values
25-
}
26-
2710
render() {
28-
const { size, color, style, ...rest } = this.props
11+
const {
12+
size,
13+
color,
14+
style,
15+
animating,
16+
hidesWhenStopped,
17+
...rest
18+
} = this.props
2919
const circleStyle = {
3020
position: 'absolute',
3121
width: size,
@@ -34,28 +24,51 @@ export default class Bounce extends React.Component<SpinnerProps> {
3424
borderRadius: size / 2,
3525
opacity: 0.6,
3626
}
27+
3728
return (
38-
<AnimationContainer animation={this.animation}>
39-
<View style={[{ width: size, height: size }, style]} {...rest}>
40-
{this.values.map((value, index) => (
41-
<Animated.View
42-
key={index}
43-
style={[
44-
circleStyle,
45-
{
46-
transform: [
47-
{
48-
scale: value.interpolate({
49-
inputRange: [0, 45, 55, 100],
50-
outputRange: [0.01, 1, 1, 0.01],
51-
}),
52-
},
53-
],
54-
},
55-
]}
56-
/>
57-
))}
58-
</View>
29+
<AnimationContainer<'bounce'>
30+
initAnimation={() => ({
31+
bounce: (value) =>
32+
stagger(1000, 2, {
33+
duration: 2000,
34+
value: value,
35+
keyframes: [0, 45, 55, 100],
36+
}),
37+
})}
38+
animating={animating}
39+
>
40+
{(values) => (
41+
<View
42+
style={[
43+
{
44+
width: size,
45+
height: size,
46+
opacity: !animating && hidesWhenStopped ? 0 : 1,
47+
},
48+
style,
49+
]}
50+
{...rest}
51+
>
52+
{values.bounce.map((value, index) => (
53+
<Animated.View
54+
key={index}
55+
style={[
56+
circleStyle,
57+
{
58+
transform: [
59+
{
60+
scale: value.interpolate({
61+
inputRange: [0, 45, 55, 100],
62+
outputRange: [0.01, 1, 1, 0.01],
63+
}),
64+
},
65+
],
66+
},
67+
]}
68+
/>
69+
))}
70+
</View>
71+
)}
5972
</AnimationContainer>
6073
)
6174
}

0 commit comments

Comments
 (0)