Module 09 — Animation

Nên có ⏱ 8-10 giờ 📋 Prerequisites: Module 01-04, 08

🎯 Mục tiêu học tập

📖 Hướng dẫn học

1 Đọc RN Animated API docsreactnative.dev/docs/animated
Focus: Animated.Value, timing, spring, interpolate, useNativeDriver. Thời gian: ~2h
2 Build 3 animations cơ bản
Fade in, Slide up, Scale on press. Dùng Animated API thuần. Thời gian: ~2h
3 Đọc Reanimated docsdocs.swmansion.com/react-native-reanimated
Focus: SharedValue, useAnimatedStyle, worklets concept. Thời gian: ~2h
4 Build swipe card với Gesture Handler + Reanimated
Tinder-style swipe left/right. Thời gian: ~2-3h
5 Tìm hiểu Lottieairbnb.io/lottie
Import After Effects animations. Thời gian: ~30min

⚡ Ôn nhanh

🧭 Vòng lặp học module này

📚 Lý thuyết chi tiết

1. Animated API (Built-in)

Animated API là built-in animation system của RN. Hoạt động bằng cách tạo Animated.Value → drive property changes qua Animated.timing/spring/decay.

Animated.Value & Basic Animation

const fadeAnim = useRef(new Animated.Value(0)).current;

const fadeIn = () => {
  Animated.timing(fadeAnim, {
    toValue: 1,
    duration: 500,
    useNativeDriver: true,
  }).start();
};

<Animated.View style={{ opacity: fadeAnim }}>
  <Text>Hello</Text>
</Animated.View>

Animation Types

TypeMô tảUse case
timingDuration-based, easing curvesFade, slide, controlled transitions
springPhysics-based, bouncyButton press, card flip, natural motion
decayDeceleration từ initial velocityFling gesture, scroll momentum

Composed Animations

Animated.parallel([
  Animated.timing(fadeAnim, { toValue: 1, duration: 300, useNativeDriver: true }),
  Animated.timing(slideAnim, { toValue: 0, duration: 300, useNativeDriver: true }),
]).start();

Animated.sequence([
  Animated.timing(a, { toValue: 1, duration: 200, useNativeDriver: true }),
  Animated.timing(b, { toValue: 1, duration: 200, useNativeDriver: true }),
]).start();

Animated.stagger(100, [
  Animated.timing(item1, { toValue: 1, duration: 300, useNativeDriver: true }),
  Animated.timing(item2, { toValue: 1, duration: 300, useNativeDriver: true }),
  Animated.timing(item3, { toValue: 1, duration: 300, useNativeDriver: true }),
]).start();

2. useNativeDriver

Tại sao mượt: Khi useNativeDriver: true, animation chạy hoàn toàn trên UI thread — không cần giao tiếp qua Bridge mỗi frame. JS thread có thể bận mà animation vẫn 60fps.

Limitations: Chỉ support non-layout properties:

Workaround: Dùng transform: [{ scaleX }] thay vì animate width. Hoặc dùng Reanimated.

3. Interpolation

const rotation = scrollY.interpolate({
  inputRange: [0, 100],
  outputRange: ['0deg', '360deg'],
  extrapolate: 'clamp',
});

<Animated.View style={{ transform: [{ rotate: rotation }] }} />

Extrapolation:

ValueBehavior khi vượt inputRange
extendTiếp tục theo tỷ lệ (default)
clampDừng tại outputRange boundary
identityTrả về input value trực tiếp

4. Reanimated 2/3

Tại sao cần Reanimated?

Core Concepts

import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withTiming,
  withSpring,
} from 'react-native-reanimated';

const offset = useSharedValue(0);

const animatedStyle = useAnimatedStyle(() => ({
  transform: [{ translateX: offset.value }],
  backgroundColor: interpolateColor(
    offset.value,
    [0, 200],
    ['#6c63ff', '#00d4aa']
  ),
}));

const moveRight = () => {
  offset.value = withSpring(200, { damping: 15 });
};

<Animated.View style={[styles.box, animatedStyle]} />

SharedValue vs Animated.Value:

Animated.ValueSharedValue
ThreadJS thread (hoặc UI với useNativeDriver)UI thread (worklet)
PropertiesLimited (useNativeDriver)Tất cả properties
SyncAsyncSynchronous trên UI thread
GestureHạn chếTích hợp tốt với Gesture Handler

5. Gesture Handler

import { Gesture, GestureDetector } from 'react-native-gesture-handler';

const pan = Gesture.Pan()
  .onStart(() => {
    context.value = { x: offsetX.value, y: offsetY.value };
  })
  .onUpdate((event) => {
    offsetX.value = context.value.x + event.translationX;
    offsetY.value = context.value.y + event.translationY;
  })
  .onEnd(() => {
    offsetX.value = withSpring(0);
    offsetY.value = withSpring(0);
  });

<GestureDetector gesture={pan}>
  <Animated.View style={animatedStyle} />
</GestureDetector>

6. LayoutAnimation

Simple API cho layout transitions — animate changes khi state update gây layout change.

import { LayoutAnimation, UIManager, Platform } from 'react-native';

if (Platform.OS === 'android') {
  UIManager.setLayoutAnimationEnabledExperimental?.(true);
}

const toggleExpand = () => {
  LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
  setExpanded(!expanded);
};

Khi nào dùng: Simple expand/collapse, add/remove items. Không dùng cho complex gesture animations.

7. Lottie

import LottieView from 'lottie-react-native';

<LottieView
  source={require('./animations/loading.json')}
  autoPlay
  loop
  style={{ width: 200, height: 200 }}
/>

Use cases: Loading indicators, success checkmarks, onboarding illustrations, empty states. Tìm animations tại lottiefiles.com.

8. Common Patterns

Bottom Sheet

Dùng @gorhom/bottom-sheet — dựa trên Reanimated + Gesture Handler. Snap points, backdrop, keyboard handling built-in.

Skeleton/Shimmer Loading

Animated gradient placeholder khi data loading. Libraries: react-native-skeleton-placeholder, rn-placeholder. Hoặc tự build với Reanimated + LinearGradient.

Shared Element Transitions

Smooth transition giữa screens sharing cùng element (ảnh, card). Dùng react-native-shared-element hoặc Reanimated 3 shared transitions.

9. Ví dụ thực tế: Swipe row action

const translateX = useSharedValue(0);

const pan = Gesture.Pan()
  .activeOffsetX([-10, 10])
  .failOffsetY([-8, 8])
  .onUpdate((event) => {
    translateX.value = Math.max(-96, Math.min(0, event.translationX));
  })
  .onEnd(() => {
    translateX.value = withSpring(translateX.value < -48 ? -96 : 0);
  });

const rowStyle = useAnimatedStyle(() => ({
  transform: [{ translateX: translateX.value }],
}));

Điểm cần nhớ: activeOffsetXfailOffsetY giúp swipe ngang không tranh gesture với scroll dọc của FlatList.

❓ Câu hỏi phỏng vấn + Đáp án

Q1: useNativeDriver là gì? Limitations?

useNativeDriver: true gửi animation config sang native (UI thread) 1 lần → native code drive animation mỗi frame mà không cần Bridge communication. Kết quả: 60fps ngay cả khi JS thread bận.

Limitations: Chỉ animate non-layout properties (opacity, transform). Không animate width/height/padding/margin/backgroundColor vì cần layout recalculation — phải dùng Reanimated thay thế.

Q2: Animated API vs Reanimated — khi nào dùng cái nào?

Animated API: Simple animations (fade, slide), built-in (không cần extra dependency), đủ cho hầu hết UI transitions.

Reanimated: Complex gesture-driven animations, animate mọi property, cần synchronous response (swipe cards, drag-drop, parallax scroll), cần integrate với Gesture Handler.

Rule: Bắt đầu với Animated API. Chuyển sang Reanimated khi cần animate layout properties hoặc gesture-driven logic phức tạp.

Q3: Worklet trong Reanimated là gì?

Worklet = JS function được compile sang code chạy được trên UI thread. Đánh dấu bằng 'worklet'; directive. Cho phép animation logic chạy synchronous trên UI thread mà không cần Bridge → 60fps. Tương tự Web Workers nhưng cho animations.

Q4: Animated.spring vs Animated.timing?

timing: Duration-based, predictable. Bạn control chính xác bao lâu animation chạy + easing curve. Dùng cho: fade, slide, controlled transitions.

spring: Physics-based, "bouncy". Config bằng damping/stiffness/mass. Không có fixed duration — chạy cho tới khi đạt equilibrium. Dùng cho: interactive animations, button press, tự nhiên hơn.

Q5: Interpolation dùng khi nào?

Map 1 Animated.Value range sang output range khác. Ví dụ: scroll position (0-300) → opacity (1-0), hoặc scroll → rotation (0deg-360deg). Rất hữu ích cho scroll-based animations, parallax effects, progress indicators.

Q6: LayoutAnimation vs Animated API?

LayoutAnimation: Declarative, simple API. Animate layout changes (add/remove/resize) tự động. Nhưng: ít control, không gesture-driven, Android cần enable experimental.

Animated API: Imperative, fine-grained control. Manual setup nhưng linh hoạt hơn nhiều.

🏋️ Bài tập

Bài 1: Tinder Swipe Card
Build card stack + swipe left (reject) / right (like) gesture.
Yêu cầu: Pan gesture, rotation theo drag direction, opacity feedback, snap back hoặc fly off, next card scale up.
Stack: Reanimated + Gesture Handler.
Bài 2: Skeleton Loading
Build shimmer/skeleton loading placeholder cho list item (avatar circle + 3 text lines).
Yêu cầu: Animated gradient sweep effect, loop smooth, dùng Animated API hoặc Reanimated.
Bài 3: Scale on Press (Micro-interaction)
Button scale down 0.95 on press, scale back 1.0 on release, spring physics.
Yêu cầu: Pressable + Animated.spring, useNativeDriver.
Bài 4: Parallax Scroll Header
Header image shrinks/fades khi scroll down, title moves up. Scroll-based animation.
Yêu cầu: Animated.event + onScroll, interpolation cho opacity + translateY + scale.

⚠️ Lỗi thường gặp

🏢 Case đi làm thật

Case: Swipe-to-delete trong notification list hoạt động tốt khi test riêng, nhưng trong FlatList thật thì kéo dọc bị nhầm thành swipe ngang.

Cách xử lý: Dùng Gesture Handler với ngưỡng activeOffsetX/failOffsetY, chỉ mở action khi vượt threshold, và đóng row đang mở khi row khác được swipe.

🧠 Ghi nhớ nhanh

Chọn API theo độ phức tạp: Animated cho transition đơn giản, Reanimated + Gesture Handler cho tương tác theo gesture, thư viện có sẵn cho bottom sheet/shared element nếu requirement phổ biến.

❔ Câu hỏi tự kiểm tra

1. Vì sao useNativeDriver không animate layout property?
2. Khi nào bạn chọn thư viện bottom sheet thay vì tự viết?
3. Làm sao tránh gesture ngang conflict với scroll dọc?
4. Bạn đo animation có bị drop frame bằng cách nào?

✅ Checklist tự đánh giá

📎 Tài nguyên

📘 RN Animated API Docs
📘 Reanimated Documentation
📘 Gesture Handler Documentation
📺 William Candillon — YouTube, best RN animation tutorials
📘 LottieFiles — Free Lottie animations
📘 @gorhom/bottom-sheet