0
# React Native Animation & Interaction
1
2
React Native provides powerful animation and interaction APIs for creating smooth, engaging user experiences with declarative animations and gesture handling.
3
4
## Installation
5
6
```bash
7
npm install react-native
8
```
9
10
## Core Animation API
11
12
### Animated
13
14
The primary animation library providing declarative, composable animations with performance optimization.
15
16
```javascript { .api }
17
// ESM
18
import {Animated} from 'react-native';
19
20
// CommonJS
21
const {Animated} = require('react-native');
22
23
// Basic animated value
24
const fadeAnim = new Animated.Value(0);
25
26
// Animate to value
27
Animated.timing(fadeAnim, {
28
toValue: 1,
29
duration: 1000,
30
useNativeDriver: true, // Use native driver for performance
31
}).start();
32
33
// Basic fade in animation
34
function FadeInView({children, style, ...props}) {
35
const fadeAnim = useRef(new Animated.Value(0)).current;
36
37
useEffect(() => {
38
Animated.timing(fadeAnim, {
39
toValue: 1,
40
duration: 1000,
41
useNativeDriver: true,
42
}).start();
43
}, [fadeAnim]);
44
45
return (
46
<Animated.View
47
style={[style, {opacity: fadeAnim}]}
48
{...props}
49
>
50
{children}
51
</Animated.View>
52
);
53
}
54
55
// Multiple animated values
56
function SlideUpView({children}) {
57
const slideAnim = useRef(new Animated.Value(50)).current;
58
const fadeAnim = useRef(new Animated.Value(0)).current;
59
60
useEffect(() => {
61
Animated.parallel([
62
Animated.timing(slideAnim, {
63
toValue: 0,
64
duration: 500,
65
useNativeDriver: true,
66
}),
67
Animated.timing(fadeAnim, {
68
toValue: 1,
69
duration: 500,
70
useNativeDriver: true,
71
}),
72
]).start();
73
}, []);
74
75
return (
76
<Animated.View
77
style={{
78
opacity: fadeAnim,
79
transform: [{translateY: slideAnim}],
80
}}
81
>
82
{children}
83
</Animated.View>
84
);
85
}
86
87
// Animated components
88
const AnimatedTouchable = Animated.createAnimatedComponent(TouchableOpacity);
89
const AnimatedTextInput = Animated.createAnimatedComponent(TextInput);
90
91
// Scale animation on press
92
function ScaleButton({children, onPress}) {
93
const scaleAnim = useRef(new Animated.Value(1)).current;
94
95
const handlePressIn = () => {
96
Animated.spring(scaleAnim, {
97
toValue: 0.95,
98
useNativeDriver: true,
99
}).start();
100
};
101
102
const handlePressOut = () => {
103
Animated.spring(scaleAnim, {
104
toValue: 1,
105
useNativeDriver: true,
106
}).start();
107
};
108
109
return (
110
<AnimatedTouchable
111
onPressIn={handlePressIn}
112
onPressOut={handlePressOut}
113
onPress={onPress}
114
style={{
115
transform: [{scale: scaleAnim}],
116
}}
117
>
118
{children}
119
</AnimatedTouchable>
120
);
121
}
122
123
// Interpolation for complex animations
124
function RotatingView({children}) {
125
const rotateAnim = useRef(new Animated.Value(0)).current;
126
127
useEffect(() => {
128
Animated.loop(
129
Animated.timing(rotateAnim, {
130
toValue: 1,
131
duration: 2000,
132
useNativeDriver: true,
133
})
134
).start();
135
}, []);
136
137
const rotate = rotateAnim.interpolate({
138
inputRange: [0, 1],
139
outputRange: ['0deg', '360deg'],
140
});
141
142
return (
143
<Animated.View style={{transform: [{rotate}]}}>
144
{children}
145
</Animated.View>
146
);
147
}
148
149
// Color interpolation
150
function ColorChangingView() {
151
const colorAnim = useRef(new Animated.Value(0)).current;
152
153
useEffect(() => {
154
Animated.loop(
155
Animated.sequence([
156
Animated.timing(colorAnim, {
157
toValue: 1,
158
duration: 1000,
159
useNativeDriver: false, // Color animations need JS driver
160
}),
161
Animated.timing(colorAnim, {
162
toValue: 0,
163
duration: 1000,
164
useNativeDriver: false,
165
}),
166
])
167
).start();
168
}, []);
169
170
const backgroundColor = colorAnim.interpolate({
171
inputRange: [0, 1],
172
outputRange: ['rgb(255, 0, 0)', 'rgb(0, 0, 255)'],
173
});
174
175
return (
176
<Animated.View style={{backgroundColor, width: 100, height: 100}} />
177
);
178
}
179
180
// Chained animations
181
function ChainedAnimation() {
182
const translateX = useRef(new Animated.Value(0)).current;
183
const scale = useRef(new Animated.Value(1)).current;
184
185
const startAnimation = () => {
186
Animated.sequence([
187
Animated.timing(translateX, {
188
toValue: 100,
189
duration: 500,
190
useNativeDriver: true,
191
}),
192
Animated.spring(scale, {
193
toValue: 1.5,
194
useNativeDriver: true,
195
}),
196
Animated.delay(500),
197
Animated.parallel([
198
Animated.timing(translateX, {
199
toValue: 0,
200
duration: 500,
201
useNativeDriver: true,
202
}),
203
Animated.spring(scale, {
204
toValue: 1,
205
useNativeDriver: true,
206
}),
207
]),
208
]).start();
209
};
210
211
return (
212
<View>
213
<Animated.View
214
style={{
215
transform: [
216
{translateX},
217
{scale},
218
],
219
}}
220
>
221
<Text>Animated Box</Text>
222
</Animated.View>
223
<Button title="Start Animation" onPress={startAnimation} />
224
</View>
225
);
226
}
227
228
// Value listeners
229
function AnimationWithCallback() {
230
const slideAnim = useRef(new Animated.Value(0)).current;
231
232
const animate = () => {
233
slideAnim.setValue(0);
234
235
Animated.timing(slideAnim, {
236
toValue: 100,
237
duration: 1000,
238
useNativeDriver: true,
239
}).start((finished) => {
240
if (finished) {
241
console.log('Animation completed');
242
} else {
243
console.log('Animation was interrupted');
244
}
245
});
246
};
247
248
// Listen to value changes
249
useEffect(() => {
250
const listener = slideAnim.addListener(({value}) => {
251
console.log('Current value:', value);
252
});
253
254
return () => slideAnim.removeListener(listener);
255
}, []);
256
257
return (
258
<View>
259
<Animated.View
260
style={{
261
transform: [{translateX: slideAnim}],
262
}}
263
>
264
<Text>Moving Box</Text>
265
</Animated.View>
266
<Button title="Animate" onPress={animate} />
267
</View>
268
);
269
}
270
```
271
272
```typescript { .api }
273
// Animated Value types
274
interface AnimatedValue {
275
constructor(value: number): AnimatedValue;
276
setValue(value: number): void;
277
setOffset(offset: number): void;
278
flattenOffset(): void;
279
extractOffset(): void;
280
addListener(callback: (value: {value: number}) => void): string;
281
removeListener(id: string): void;
282
removeAllListeners(): void;
283
stopAnimation(callback?: (value: number) => void): void;
284
resetAnimation(callback?: (value: number) => void): void;
285
interpolate(config: InterpolationConfig): AnimatedValue;
286
}
287
288
interface InterpolationConfig {
289
inputRange: number[];
290
outputRange: number[] | string[];
291
easing?: (input: number) => number;
292
extrapolate?: 'extend' | 'identity' | 'clamp';
293
extrapolateLeft?: 'extend' | 'identity' | 'clamp';
294
extrapolateRight?: 'extend' | 'identity' | 'clamp';
295
}
296
297
// Animation configuration types
298
interface TimingAnimationConfig {
299
toValue: number;
300
easing?: (input: number) => number;
301
duration?: number;
302
delay?: number;
303
useNativeDriver?: boolean;
304
isInteraction?: boolean;
305
}
306
307
interface SpringAnimationConfig {
308
toValue: number;
309
restDisplacementThreshold?: number;
310
overshootClamping?: boolean;
311
restSpeedThreshold?: number;
312
velocity?: number;
313
bounciness?: number;
314
speed?: number;
315
tension?: number;
316
friction?: number;
317
stiffness?: number;
318
damping?: number;
319
mass?: number;
320
useNativeDriver?: boolean;
321
isInteraction?: boolean;
322
}
323
324
interface DecayAnimationConfig {
325
velocity: number;
326
deceleration?: number;
327
isInteraction?: boolean;
328
useNativeDriver?: boolean;
329
}
330
331
// Animation composition types
332
interface CompositeAnimation {
333
start(callback?: (finished: boolean) => void): void;
334
stop(): void;
335
reset(): void;
336
}
337
338
// Animated component types
339
interface AnimatedStatic {
340
// Core values
341
Value: typeof AnimatedValue;
342
ValueXY: typeof AnimatedValueXY;
343
344
// Animated components
345
View: React.ComponentType<Animated.AnimatedProps<ViewProps>>;
346
Text: React.ComponentType<Animated.AnimatedProps<TextProps>>;
347
Image: React.ComponentType<Animated.AnimatedProps<ImageProps>>;
348
ScrollView: React.ComponentType<Animated.AnimatedProps<ScrollViewProps>>;
349
FlatList: React.ComponentType<Animated.AnimatedProps<FlatListProps<any>>>;
350
SectionList: React.ComponentType<Animated.AnimatedProps<SectionListProps<any>>>;
351
352
// Animation methods
353
timing(value: AnimatedValue, config: TimingAnimationConfig): CompositeAnimation;
354
spring(value: AnimatedValue, config: SpringAnimationConfig): CompositeAnimation;
355
decay(value: AnimatedValue, config: DecayAnimationConfig): CompositeAnimation;
356
357
// Composition methods
358
parallel(animations: CompositeAnimation[], config?: {stopTogether?: boolean}): CompositeAnimation;
359
sequence(animations: CompositeAnimation[]): CompositeAnimation;
360
stagger(time: number, animations: CompositeAnimation[]): CompositeAnimation;
361
delay(time: number): CompositeAnimation;
362
loop(animation: CompositeAnimation, config?: {iterations?: number}): CompositeAnimation;
363
364
// Utility methods
365
add(a: AnimatedValue, b: AnimatedValue): AnimatedValue;
366
subtract(a: AnimatedValue, b: AnimatedValue): AnimatedValue;
367
divide(a: AnimatedValue, b: AnimatedValue): AnimatedValue;
368
multiply(a: AnimatedValue, b: AnimatedValue): AnimatedValue;
369
modulo(a: AnimatedValue, modulus: number): AnimatedValue;
370
diffClamp(a: AnimatedValue, min: number, max: number): AnimatedValue;
371
372
// Component creation
373
createAnimatedComponent<T extends React.ComponentType<any>>(component: T): React.ComponentType<Animated.AnimatedProps<React.ComponentProps<T>>>;
374
375
// Events
376
event(argMapping: any[], config?: {useNativeDriver?: boolean; listener?: Function}): Function;
377
}
378
379
namespace Animated {
380
interface AnimatedProps<T> {
381
[K in keyof T]: K extends 'style'
382
? StyleProp<T[K]> | AnimatedValue | {[P in keyof T[K]]: T[K][P] | AnimatedValue}
383
: T[K];
384
}
385
}
386
```
387
388
### Easing
389
390
Easing functions for smooth animation transitions and natural motion curves.
391
392
```javascript { .api }
393
import {Easing} from 'react-native';
394
395
// Basic easing functions
396
const fadeIn = () => {
397
Animated.timing(fadeAnim, {
398
toValue: 1,
399
duration: 1000,
400
easing: Easing.ease, // Default easing
401
useNativeDriver: true,
402
}).start();
403
};
404
405
// Different easing curves
406
const easingExamples = {
407
// Basic curves
408
linear: Easing.linear,
409
ease: Easing.ease,
410
quad: Easing.quad,
411
cubic: Easing.cubic,
412
413
// Directional easing
414
easeIn: Easing.in(Easing.quad),
415
easeOut: Easing.out(Easing.quad),
416
easeInOut: Easing.inOut(Easing.quad),
417
418
// Bezier curves
419
customBezier: Easing.bezier(0.25, 0.1, 0.25, 1),
420
421
// Bounce and elastic
422
bounce: Easing.bounce,
423
elastic: Easing.elastic(1),
424
425
// Stepped animation
426
steps: Easing.step0,
427
428
// Circle easing
429
circle: Easing.circle,
430
431
// Sine easing
432
sine: Easing.sin,
433
434
// Exponential easing
435
expo: Easing.exp,
436
437
// Back easing (overshoot)
438
back: Easing.back(1.5),
439
440
// Polynomial easing
441
poly: Easing.poly(4),
442
};
443
444
// Custom easing function
445
const customEasing = (t) => {
446
return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
447
};
448
449
// Easing comparison component
450
function EasingComparison() {
451
const animations = useRef(
452
Object.keys(easingExamples).reduce((acc, key) => {
453
acc[key] = new Animated.Value(0);
454
return acc;
455
}, {})
456
).current;
457
458
const startAnimations = () => {
459
Object.keys(easingExamples).forEach((key) => {
460
animations[key].setValue(0);
461
462
Animated.timing(animations[key], {
463
toValue: 200,
464
duration: 2000,
465
easing: easingExamples[key],
466
useNativeDriver: true,
467
}).start();
468
});
469
};
470
471
return (
472
<View>
473
<Button title="Start Easing Comparison" onPress={startAnimations} />
474
{Object.keys(easingExamples).map((key) => (
475
<View key={key} style={styles.easingRow}>
476
<Text style={styles.easingLabel}>{key}</Text>
477
<Animated.View
478
style={[
479
styles.easingBox,
480
{transform: [{translateX: animations[key]}]},
481
]}
482
/>
483
</View>
484
))}
485
</View>
486
);
487
}
488
489
// Bouncy button with back easing
490
function BouncyButton({children, onPress}) {
491
const scaleAnim = useRef(new Animated.Value(1)).current;
492
493
const handlePress = () => {
494
Animated.sequence([
495
Animated.timing(scaleAnim, {
496
toValue: 0.8,
497
duration: 100,
498
easing: Easing.in(Easing.back(2)),
499
useNativeDriver: true,
500
}),
501
Animated.timing(scaleAnim, {
502
toValue: 1,
503
duration: 300,
504
easing: Easing.out(Easing.back(2)),
505
useNativeDriver: true,
506
}),
507
]).start();
508
509
onPress?.();
510
};
511
512
return (
513
<AnimatedTouchable
514
onPress={handlePress}
515
style={{
516
transform: [{scale: scaleAnim}],
517
}}
518
>
519
{children}
520
</AnimatedTouchable>
521
);
522
}
523
524
// Progress indicator with stepped animation
525
function SteppedProgress({progress}) {
526
const stepAnim = useRef(new Animated.Value(0)).current;
527
528
useEffect(() => {
529
Animated.timing(stepAnim, {
530
toValue: progress,
531
duration: 1000,
532
easing: Easing.step0, // Stepped animation
533
useNativeDriver: false,
534
}).start();
535
}, [progress]);
536
537
const width = stepAnim.interpolate({
538
inputRange: [0, 1],
539
outputRange: ['0%', '100%'],
540
});
541
542
return (
543
<View style={styles.progressContainer}>
544
<Animated.View style={[styles.progressBar, {width}]} />
545
</View>
546
);
547
}
548
```
549
550
```typescript { .api }
551
interface EasingStatic {
552
// Basic easing functions
553
linear: (t: number) => number;
554
ease: (t: number) => number;
555
quad: (t: number) => number;
556
cubic: (t: number) => number;
557
poly(n: number): (t: number) => number;
558
sin: (t: number) => number;
559
circle: (t: number) => number;
560
exp: (t: number) => number;
561
562
// Directional modifiers
563
in(easing: (t: number) => number): (t: number) => number;
564
out(easing: (t: number) => number): (t: number) => number;
565
inOut(easing: (t: number) => number): (t: number) => number;
566
567
// Special easing functions
568
elastic(bounciness?: number): (t: number) => number;
569
back(s?: number): (t: number) => number;
570
bounce: (t: number) => number;
571
572
// Bezier curve
573
bezier(x1: number, y1: number, x2: number, y2: number): (t: number) => number;
574
575
// Stepped animation
576
step0: (t: number) => number;
577
step1: (t: number) => number;
578
}
579
```
580
581
### LayoutAnimation
582
583
Automatically animate layout changes without explicit animation code.
584
585
```javascript { .api }
586
import {LayoutAnimation} from 'react-native';
587
588
// Basic layout animation
589
function AnimatedList() {
590
const [items, setItems] = useState(['Item 1', 'Item 2', 'Item 3']);
591
592
const addItem = () => {
593
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
594
setItems([...items, `Item ${items.length + 1}`]);
595
};
596
597
const removeItem = (index) => {
598
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
599
setItems(items.filter((_, i) => i !== index));
600
};
601
602
return (
603
<View>
604
<Button title="Add Item" onPress={addItem} />
605
{items.map((item, index) => (
606
<TouchableOpacity
607
key={index}
608
onPress={() => removeItem(index)}
609
style={styles.listItem}
610
>
611
<Text>{item}</Text>
612
</TouchableOpacity>
613
))}
614
</View>
615
);
616
}
617
618
// Custom layout animation config
619
const customLayoutAnimation = {
620
duration: 300,
621
create: {
622
type: LayoutAnimation.Types.easeInEaseOut,
623
property: LayoutAnimation.Properties.opacity,
624
},
625
update: {
626
type: LayoutAnimation.Types.easeInEaseOut,
627
},
628
delete: {
629
type: LayoutAnimation.Types.easeInEaseOut,
630
property: LayoutAnimation.Properties.opacity,
631
},
632
};
633
634
function CustomAnimatedComponent() {
635
const [isExpanded, setIsExpanded] = useState(false);
636
637
const toggleExpanded = () => {
638
LayoutAnimation.configureNext(customLayoutAnimation);
639
setIsExpanded(!isExpanded);
640
};
641
642
return (
643
<View>
644
<TouchableOpacity onPress={toggleExpanded} style={styles.header}>
645
<Text>Tap to expand</Text>
646
</TouchableOpacity>
647
648
{isExpanded && (
649
<View style={styles.content}>
650
<Text>This content animates in and out</Text>
651
<Text>Using LayoutAnimation</Text>
652
</View>
653
)}
654
</View>
655
);
656
}
657
658
// Grid layout animation
659
function AnimatedGrid() {
660
const [numColumns, setNumColumns] = useState(2);
661
662
const toggleColumns = () => {
663
LayoutAnimation.configureNext(LayoutAnimation.Presets.spring);
664
setNumColumns(numColumns === 2 ? 3 : 2);
665
};
666
667
return (
668
<View>
669
<Button
670
title={`Switch to ${numColumns === 2 ? 3 : 2} columns`}
671
onPress={toggleColumns}
672
/>
673
674
<FlatList
675
data={Array.from({length: 20}, (_, i) => ({id: i, title: `Item ${i}`}))}
676
numColumns={numColumns}
677
key={numColumns} // Force re-render for column change
678
renderItem={({item}) => (
679
<View style={[styles.gridItem, {flex: 1/numColumns}]}>
680
<Text>{item.title}</Text>
681
</View>
682
)}
683
/>
684
</View>
685
);
686
}
687
688
// Layout animation with callbacks
689
function CallbackLayoutAnimation() {
690
const [items, setItems] = useState(['A', 'B', 'C']);
691
692
const shuffleItems = () => {
693
LayoutAnimation.configureNext(
694
LayoutAnimation.Presets.easeInEaseOut,
695
() => {
696
console.log('Layout animation finished');
697
},
698
(error) => {
699
console.error('Layout animation failed:', error);
700
}
701
);
702
703
const shuffled = [...items].sort(() => Math.random() - 0.5);
704
setItems(shuffled);
705
};
706
707
return (
708
<View>
709
<Button title="Shuffle Items" onPress={shuffleItems} />
710
{items.map((item, index) => (
711
<View key={item} style={styles.shuffleItem}>
712
<Text>{item}</Text>
713
</View>
714
))}
715
</View>
716
);
717
}
718
719
// Enable layout animations on Android
720
if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
721
UIManager.setLayoutAnimationEnabledExperimental(true);
722
}
723
```
724
725
```typescript { .api }
726
interface LayoutAnimationStatic {
727
// Configure next layout change
728
configureNext(
729
config: LayoutAnimationConfig,
730
onAnimationDidEnd?: () => void,
731
onAnimationDidFail?: (error: any) => void
732
): void;
733
734
// Preset configurations
735
Presets: {
736
easeInEaseOut: LayoutAnimationConfig;
737
linear: LayoutAnimationConfig;
738
spring: LayoutAnimationConfig;
739
};
740
741
// Animation types
742
Types: {
743
spring: string;
744
linear: string;
745
easeInEaseOut: string;
746
easeIn: string;
747
easeOut: string;
748
keyboard: string;
749
};
750
751
// Animation properties
752
Properties: {
753
opacity: string;
754
scaleX: string;
755
scaleY: string;
756
scaleXY: string;
757
};
758
}
759
760
interface LayoutAnimationConfig {
761
duration?: number;
762
create?: LayoutAnimationAnim;
763
update?: LayoutAnimationAnim;
764
delete?: LayoutAnimationAnim;
765
}
766
767
interface LayoutAnimationAnim {
768
duration?: number;
769
delay?: number;
770
springDamping?: number;
771
initialVelocity?: number;
772
type?: string;
773
property?: string;
774
}
775
```
776
777
## Gesture and Interaction APIs
778
779
### PanResponder
780
781
Handle complex gesture recognition and touch events for custom interactive components.
782
783
```javascript { .api }
784
import {PanResponder} from 'react-native';
785
786
// Basic draggable component
787
function DraggableView({children}) {
788
const pan = useRef(new Animated.ValueXY()).current;
789
790
const panResponder = useRef(
791
PanResponder.create({
792
onMoveShouldSetPanResponder: () => true,
793
onPanResponderGrant: () => {
794
pan.setOffset({
795
x: pan.x._value,
796
y: pan.y._value,
797
});
798
},
799
onPanResponderMove: Animated.event([
800
null,
801
{dx: pan.x, dy: pan.y},
802
], {useNativeDriver: false}),
803
onPanResponderRelease: () => {
804
pan.flattenOffset();
805
},
806
})
807
).current;
808
809
return (
810
<Animated.View
811
style={{
812
transform: [{translateX: pan.x}, {translateY: pan.y}],
813
}}
814
{...panResponder.panHandlers}
815
>
816
{children}
817
</Animated.View>
818
);
819
}
820
821
// Swipe-to-dismiss component
822
function SwipeToDismiss({children, onDismiss}) {
823
const translateX = useRef(new Animated.Value(0)).current;
824
const [dismissed, setDismissed] = useState(false);
825
826
const panResponder = useRef(
827
PanResponder.create({
828
onMoveShouldSetPanResponder: (_, gestureState) => {
829
return Math.abs(gestureState.dx) > Math.abs(gestureState.dy);
830
},
831
832
onPanResponderMove: (_, gestureState) => {
833
translateX.setValue(gestureState.dx);
834
},
835
836
onPanResponderRelease: (_, gestureState) => {
837
const threshold = 120;
838
839
if (Math.abs(gestureState.dx) > threshold) {
840
// Dismiss
841
const toValue = gestureState.dx > 0 ? 300 : -300;
842
843
Animated.timing(translateX, {
844
toValue,
845
duration: 200,
846
useNativeDriver: true,
847
}).start(() => {
848
setDismissed(true);
849
onDismiss?.();
850
});
851
} else {
852
// Return to original position
853
Animated.spring(translateX, {
854
toValue: 0,
855
useNativeDriver: true,
856
}).start();
857
}
858
},
859
})
860
).current;
861
862
if (dismissed) return null;
863
864
return (
865
<Animated.View
866
style={{
867
transform: [{translateX}],
868
}}
869
{...panResponder.panHandlers}
870
>
871
{children}
872
</Animated.View>
873
);
874
}
875
876
// Scalable and rotatable view
877
function ScalableRotatableView({children}) {
878
const scale = useRef(new Animated.Value(1)).current;
879
const rotate = useRef(new Animated.Value(0)).current;
880
881
const lastScale = useRef(1);
882
const lastRotate = useRef(0);
883
884
const panResponder = useRef(
885
PanResponder.create({
886
onMoveShouldSetPanResponder: () => true,
887
onMoveShouldSetPanResponderCapture: () => true,
888
889
onPanResponderGrant: () => {
890
lastScale.current = scale._value;
891
lastRotate.current = rotate._value;
892
},
893
894
onPanResponderMove: (evt, gestureState) => {
895
// Multi-touch gestures would require additional logic
896
// This is a simplified version for single touch
897
898
// Scale based on vertical movement
899
const newScale = lastScale.current + gestureState.dy / 200;
900
scale.setValue(Math.max(0.5, Math.min(2, newScale)));
901
902
// Rotate based on horizontal movement
903
const newRotate = lastRotate.current + gestureState.dx / 100;
904
rotate.setValue(newRotate);
905
},
906
907
onPanResponderRelease: () => {
908
// Optionally snap back to default values
909
Animated.parallel([
910
Animated.spring(scale, {
911
toValue: 1,
912
useNativeDriver: true,
913
}),
914
Animated.spring(rotate, {
915
toValue: 0,
916
useNativeDriver: true,
917
}),
918
]).start();
919
},
920
})
921
).current;
922
923
const rotateStr = rotate.interpolate({
924
inputRange: [-1, 1],
925
outputRange: ['-45deg', '45deg'],
926
});
927
928
return (
929
<Animated.View
930
style={{
931
transform: [
932
{scale},
933
{rotate: rotateStr},
934
],
935
}}
936
{...panResponder.panHandlers}
937
>
938
{children}
939
</Animated.View>
940
);
941
}
942
943
// Pull-to-refresh with PanResponder
944
function CustomPullToRefresh({children, onRefresh}) {
945
const translateY = useRef(new Animated.Value(0)).current;
946
const [isRefreshing, setIsRefreshing] = useState(false);
947
948
const panResponder = useRef(
949
PanResponder.create({
950
onMoveShouldSetPanResponder: (_, gestureState) => {
951
return gestureState.dy > 0 && gestureState.vy > 0;
952
},
953
954
onPanResponderMove: (_, gestureState) => {
955
if (gestureState.dy > 0 && !isRefreshing) {
956
translateY.setValue(Math.min(gestureState.dy, 100));
957
}
958
},
959
960
onPanResponderRelease: (_, gestureState) => {
961
if (gestureState.dy > 60 && !isRefreshing) {
962
setIsRefreshing(true);
963
964
Animated.timing(translateY, {
965
toValue: 60,
966
duration: 200,
967
useNativeDriver: true,
968
}).start();
969
970
// Simulate refresh
971
onRefresh?.().finally(() => {
972
setIsRefreshing(false);
973
974
Animated.timing(translateY, {
975
toValue: 0,
976
duration: 300,
977
useNativeDriver: true,
978
}).start();
979
});
980
} else {
981
Animated.timing(translateY, {
982
toValue: 0,
983
duration: 200,
984
useNativeDriver: true,
985
}).start();
986
}
987
},
988
})
989
).current;
990
991
return (
992
<View style={{flex: 1}}>
993
<Animated.View
994
style={{
995
position: 'absolute',
996
top: -50,
997
left: 0,
998
right: 0,
999
height: 50,
1000
justifyContent: 'center',
1001
alignItems: 'center',
1002
transform: [{translateY}],
1003
}}
1004
>
1005
{isRefreshing ? (
1006
<ActivityIndicator size="small" />
1007
) : (
1008
<Text>Pull to refresh</Text>
1009
)}
1010
</Animated.View>
1011
1012
<Animated.View
1013
style={{
1014
flex: 1,
1015
transform: [{translateY}],
1016
}}
1017
{...panResponder.panHandlers}
1018
>
1019
{children}
1020
</Animated.View>
1021
</View>
1022
);
1023
}
1024
```
1025
1026
```typescript { .api }
1027
interface PanResponderStatic {
1028
create(config: PanResponderConfig): PanResponderInstance;
1029
}
1030
1031
interface PanResponderInstance {
1032
panHandlers: {
1033
onStartShouldSetResponder: (evt: GestureResponderEvent) => boolean;
1034
onMoveShouldSetResponder: (evt: GestureResponderEvent, gestureState: PanResponderGestureState) => boolean;
1035
onResponderGrant: (evt: GestureResponderEvent, gestureState: PanResponderGestureState) => void;
1036
onResponderMove: (evt: GestureResponderEvent, gestureState: PanResponderGestureState) => void;
1037
onResponderRelease: (evt: GestureResponderEvent, gestureState: PanResponderGestureState) => void;
1038
onResponderTerminate: (evt: GestureResponderEvent, gestureState: PanResponderGestureState) => void;
1039
onStartShouldSetResponderCapture: (evt: GestureResponderEvent, gestureState: PanResponderGestureState) => boolean;
1040
onMoveShouldSetResponderCapture: (evt: GestureResponderEvent, gestureState: PanResponderGestureState) => boolean;
1041
};
1042
}
1043
1044
interface PanResponderConfig {
1045
onStartShouldSetPanResponder?: (evt: GestureResponderEvent, gestureState: PanResponderGestureState) => boolean;
1046
onMoveShouldSetPanResponder?: (evt: GestureResponderEvent, gestureState: PanResponderGestureState) => boolean;
1047
onPanResponderGrant?: (evt: GestureResponderEvent, gestureState: PanResponderGestureState) => void;
1048
onPanResponderMove?: (evt: GestureResponderEvent, gestureState: PanResponderGestureState) => void;
1049
onPanResponderRelease?: (evt: GestureResponderEvent, gestureState: PanResponderGestureState) => void;
1050
onPanResponderTerminate?: (evt: GestureResponderEvent, gestureState: PanResponderGestureState) => void;
1051
onPanResponderReject?: (evt: GestureResponderEvent, gestureState: PanResponderGestureState) => void;
1052
onStartShouldSetPanResponderCapture?: (evt: GestureResponderEvent, gestureState: PanResponderGestureState) => boolean;
1053
onMoveShouldSetPanResponderCapture?: (evt: GestureResponderEvent, gestureState: PanResponderGestureState) => boolean;
1054
onPanResponderStart?: (evt: GestureResponderEvent, gestureState: PanResponderGestureState) => void;
1055
onPanResponderEnd?: (evt: GestureResponderEvent, gestureState: PanResponderGestureState) => void;
1056
onShouldBlockNativeResponder?: (evt: GestureResponderEvent, gestureState: PanResponderGestureState) => boolean;
1057
}
1058
1059
interface PanResponderGestureState {
1060
stateID: number;
1061
moveX: number;
1062
moveY: number;
1063
x0: number;
1064
y0: number;
1065
dx: number;
1066
dy: number;
1067
vx: number;
1068
vy: number;
1069
numberActiveTouches: number;
1070
}
1071
```
1072
1073
### InteractionManager
1074
1075
Schedule work after interactions have completed to maintain smooth animations and user experience.
1076
1077
```javascript { .api }
1078
import {InteractionManager} from 'react-native';
1079
1080
// Defer expensive operations after interactions
1081
function ExpensiveComponent() {
1082
const [data, setData] = useState(null);
1083
const [isLoading, setIsLoading] = useState(true);
1084
1085
useEffect(() => {
1086
// Show loading state immediately
1087
setIsLoading(true);
1088
1089
// Wait for interactions to complete, then load data
1090
const interactionPromise = InteractionManager.runAfterInteractions(() => {
1091
// Expensive operation
1092
return fetchLargeDataset().then(result => {
1093
setData(result);
1094
setIsLoading(false);
1095
});
1096
});
1097
1098
return () => {
1099
interactionPromise.cancel();
1100
};
1101
}, []);
1102
1103
if (isLoading) {
1104
return <ActivityIndicator size="large" />;
1105
}
1106
1107
return (
1108
<FlatList
1109
data={data}
1110
renderItem={({item}) => <ExpensiveListItem item={item} />}
1111
/>
1112
);
1113
}
1114
1115
// Navigation transition with deferred loading
1116
function NavigationScreen({route}) {
1117
const [content, setContent] = useState(null);
1118
1119
useEffect(() => {
1120
// Run after navigation animation completes
1121
InteractionManager.runAfterInteractions(() => {
1122
loadScreenContent(route.params.id).then(setContent);
1123
});
1124
}, [route.params.id]);
1125
1126
return (
1127
<View style={styles.container}>
1128
<Header title="Screen Title" />
1129
{content ? (
1130
<Content data={content} />
1131
) : (
1132
<LoadingSkeleton />
1133
)}
1134
</View>
1135
);
1136
}
1137
1138
// Custom hook for deferred execution
1139
function useAfterInteractions(callback, deps = []) {
1140
const [isReady, setIsReady] = useState(false);
1141
1142
useEffect(() => {
1143
let cancelled = false;
1144
1145
const handle = InteractionManager.runAfterInteractions(() => {
1146
if (!cancelled) {
1147
callback();
1148
setIsReady(true);
1149
}
1150
});
1151
1152
return () => {
1153
cancelled = true;
1154
handle.cancel();
1155
};
1156
}, deps);
1157
1158
return isReady;
1159
}
1160
1161
// Usage with custom hook
1162
function DeferredComponent() {
1163
const [heavyData, setHeavyData] = useState(null);
1164
1165
const isReady = useAfterInteractions(() => {
1166
processHeavyData().then(setHeavyData);
1167
}, []);
1168
1169
return (
1170
<View>
1171
{isReady && heavyData ? (
1172
<HeavyDataView data={heavyData} />
1173
) : (
1174
<PlaceholderView />
1175
)}
1176
</View>
1177
);
1178
}
1179
1180
// Creating interaction handles manually
1181
function InteractiveAnimation() {
1182
const scaleAnim = useRef(new Animated.Value(1)).current;
1183
const [isInteracting, setIsInteracting] = useState(false);
1184
1185
const startAnimation = () => {
1186
// Create interaction handle
1187
const handle = InteractionManager.createInteractionHandle();
1188
setIsInteracting(true);
1189
1190
Animated.sequence([
1191
Animated.timing(scaleAnim, {
1192
toValue: 1.2,
1193
duration: 500,
1194
useNativeDriver: true,
1195
}),
1196
Animated.timing(scaleAnim, {
1197
toValue: 1,
1198
duration: 500,
1199
useNativeDriver: true,
1200
}),
1201
]).start(() => {
1202
// Clear interaction handle when animation completes
1203
InteractionManager.clearInteractionHandle(handle);
1204
setIsInteracting(false);
1205
});
1206
};
1207
1208
return (
1209
<View>
1210
<Animated.View style={{transform: [{scale: scaleAnim}]}}>
1211
<TouchableOpacity onPress={startAnimation}>
1212
<Text>Animate</Text>
1213
</TouchableOpacity>
1214
</Animated.View>
1215
1216
{isInteracting && (
1217
<Text>Animation in progress...</Text>
1218
)}
1219
</View>
1220
);
1221
}
1222
1223
// Batch processing with interaction management
1224
function BatchProcessor({items}) {
1225
const [processedItems, setProcessedItems] = useState([]);
1226
const [isProcessing, setIsProcessing] = useState(false);
1227
1228
const processBatch = async () => {
1229
setIsProcessing(true);
1230
1231
// Process items in batches after interactions
1232
const batchSize = 10;
1233
const batches = [];
1234
1235
for (let i = 0; i < items.length; i += batchSize) {
1236
batches.push(items.slice(i, i + batchSize));
1237
}
1238
1239
for (const batch of batches) {
1240
await new Promise(resolve => {
1241
InteractionManager.runAfterInteractions(() => {
1242
const processed = batch.map(processItem);
1243
setProcessedItems(prev => [...prev, ...processed]);
1244
resolve();
1245
});
1246
});
1247
}
1248
1249
setIsProcessing(false);
1250
};
1251
1252
return (
1253
<View>
1254
<Button
1255
title="Process Items"
1256
onPress={processBatch}
1257
disabled={isProcessing}
1258
/>
1259
1260
<FlatList
1261
data={processedItems}
1262
renderItem={({item}) => <ProcessedItem item={item} />}
1263
/>
1264
1265
{isProcessing && (
1266
<ActivityIndicator style={styles.processingIndicator} />
1267
)}
1268
</View>
1269
);
1270
}
1271
```
1272
1273
```typescript { .api }
1274
interface InteractionManagerStatic {
1275
// Run after interactions
1276
runAfterInteractions(callback: () => void | Promise<any>): {
1277
then: (callback: () => void) => {cancel: () => void};
1278
done: (...args: any[]) => any;
1279
cancel: () => void;
1280
};
1281
1282
// Manual interaction handles
1283
createInteractionHandle(): number;
1284
clearInteractionHandle(handle: number): void;
1285
1286
// Event listeners
1287
addListener?(callback: () => void): void;
1288
1289
// Promise-based API
1290
setDeadline?(deadline: number): void;
1291
}
1292
1293
interface InteractionHandle {
1294
then(callback: () => void): {cancel: () => void};
1295
done(...args: any[]): any;
1296
cancel(): void;
1297
}
1298
```
1299
1300
This comprehensive animation and interaction documentation provides developers with all the tools needed to create smooth, engaging user experiences with React Native's powerful animation APIs and gesture handling capabilities.