Flutter 튜토리얼 27편: 물리 기반 애니메이션
요약
핵심 요지
문서가 설명하는 범위
- 물리 기반 애니메이션의 개념과 장점
- SpringSimulation으로 튕기는 효과 구현
- GravitySimulation으로 낙하 효과 구현
- FrictionSimulation으로 감속 효과 구현
- 제스처 속도를 애니메이션에 연결하기
읽는 시간: 15분 | 난이도: 중급
참고 자료
- Animate a widget using a physics simulation - 물리 시뮬레이션 공식 튜토리얼
- SpringSimulation class - 스프링 시뮬레이션 API
- GravitySimulation class - 중력 시뮬레이션 API
- FrictionSimulation class - 마찰 시뮬레이션 API
문제 상황
버튼을 탭하면 위젯이 움직이는 애니메이션을 만들었습니다.
duration과 curve를 지정해서 동작하긴 하는데, 뭔가 어색합니다.
기계적인 느낌의 애니메이션
고정 시간 애니메이션 물리 기반 애니메이션
시작 ──300ms──→ 끝 시작 ──?ms──→ 끝(항상 똑같은 시간) (속도에 따라 달라짐)
천천히 밀어도 300ms 천천히 밀면 천천히세게 밀어도 300ms 세게 밀면 빠르게문제는 다음과 같습니다.
- 사용자가 빠르게 드래그해도 애니메이션 속도가 똑같다.
- 공을 던지면 포물선을 그려야 하는데, 직선으로 움직인다.
- 스프링처럼 튕겨야 하는데, 한 번에 멈춘다.
현실 세계에서는 속도, 가속도, 마찰이 움직임에 영향을 줍니다.
Flutter에서도 이런 물리 법칙을 적용할 수 있습니다.
해결 방법
Flutter의 physics 라이브러리는 현실적인 움직임을 시뮬레이션하는 클래스들을 제공합니다.
AnimationController.animateWith()에 시뮬레이션을 전달하면 duration 없이 물리 법칙에 따라 애니메이션이 동작합니다.
챕터 1: 물리 기반 애니메이션 이해하기
Why
NOTE현실에서 공을 던지면 어떻게 될까요?
던진 속도, 중력, 공기 저항에 따라 자연스럽게 움직입니다.애니메이션도 마찬가지입니다.
고정된 시간 대신 물리 법칙을 적용하면 사용자 입력에 반응하는 자연스러운 움직임을 만들 수 있습니다.graph LR A[사용자 제스처] -->|속도 전달| B[물리 시뮬레이션] B -->|위치 계산| C[애니메이션 값] C -->|화면 갱신| D[위젯 움직임]
What
NOTEFlutter는 세 가지 주요 물리 시뮬레이션을 제공합니다.
시뮬레이션 동작 사용 예시 SpringSimulation3스프링처럼 튕김 드래그 후 원위치로 복귀 GravitySimulation4일정한 가속도로 낙하 떨어지는 물체 FrictionSimulation5점점 느려지다 멈춤 스크롤 관성 이들의 공통점은 Simulation 클래스를 상속한다는 것입니다.
모든 시뮬레이션은x(time),dx(time),isDone(time)메서드를 가집니다.
How
TIP기본 패턴은 이렇습니다.
1단계:
physics라이브러리 임포트import 'package:flutter/physics.dart';2단계: 시뮬레이션 생성
final simulation = SpringSimulation(SpringDescription(mass: 1, stiffness: 100, damping: 10),0, // 시작 위치1, // 끝 위치-500, // 초기 속도 (픽셀/초));3단계:
animateWith()로 애니메이션 실행_controller.animateWith(simulation);핵심 포인트:
duration을 지정하지 않습니다.
시뮬레이션이 스스로 “언제 끝날지” 계산합니다.
Watch out
WARNINGAnimationController 설정 주의
물리 시뮬레이션 사용 시
AnimationController에duration을 지정하지 않아도 됩니다.
하지만vsync는 반드시 필요합니다.// ✅ 물리 시뮬레이션용 컨트롤러_controller = AnimationController(vsync: this);// ❌ duration이 있어도 되지만, animateWith()가 무시함_controller = AnimationController(vsync: this,duration: Duration(seconds: 1), // animateWith()에서 무시됨);
결론: animateWith(simulation)을 사용하면 duration 없이 물리 법칙에 따라 애니메이션이 동작합니다.
챕터 2: SpringSimulation으로 튕기는 효과
Why
NOTE드래그한 카드를 놓으면 원래 위치로 돌아가야 합니다.
단순히 직선으로 돌아가면 딱딱한 느낌이 듭니다.스프링처럼 튕기면서 돌아가면 훨씬 자연스럽습니다.
실제로 많은 iOS/Android 앱에서 이 효과를 사용합니다.
What
NOTE
SpringSimulation은 스프링에 매달린 물체의 움직임을 시뮬레이션합니다.SpringDescription 매개변수:
매개변수 의미 값이 크면? mass물체의 질량 무거워서 느리게 움직임 stiffness스프링의 강성 빠르게 원위치로 복귀 damping감쇠 (저항) 튕김이 빨리 멈춤 stiffness 높음 + damping 낮음 = 많이 튕김stiffness 낮음 + damping 높음 = 부드럽게 정착
How
TIP드래그 후 튕기며 돌아오는 카드 만들기
핵심 코드만 보겠습니다.
class _DraggableCardState extends State<DraggableCard>with SingleTickerProviderStateMixin {late AnimationController _controller;Alignment _dragAlignment = Alignment.center;@overridevoid initState() {super.initState();_controller = AnimationController(vsync: this);_controller.addListener(() {setState(() {_dragAlignment = _animation.value;});});}void _runAnimation(Offset velocity, Size size) {// 속도를 애니메이션 단위로 변환final pixelsPerSecond = velocity;final unitsPerSecondX = pixelsPerSecond.dx / size.width;final unitsPerSecondY = pixelsPerSecond.dy / size.height;final unitVelocity = Offset(unitsPerSecondX, unitsPerSecondY).distance;// 스프링 설정const spring = SpringDescription(mass: 1,stiffness: 100,damping: 10,);// 시뮬레이션 생성 (현재 위치 → 중앙으로)final simulation = SpringSimulation(spring, 0, 1, -unitVelocity);// Tween 설정_animation = _controller.drive(AlignmentTween(begin: _dragAlignment, end: Alignment.center),);_controller.animateWith(simulation);}@overrideWidget build(BuildContext context) {final size = MediaQuery.of(context).size;return GestureDetector(onPanUpdate: (details) {setState(() {_dragAlignment += Alignment(details.delta.dx / (size.width / 2),details.delta.dy / (size.height / 2),);});},onPanEnd: (details) {_runAnimation(details.velocity.pixelsPerSecond, size);},child: Align(alignment: _dragAlignment,child: Card(child: FlutterLogo(size: 100)),),);}}매개변수 튜닝 가이드:
효과 mass stiffness damping 가볍게 튕김 0.5 200 10 무겁게 튕김 2 100 5 부드럽게 정착 1 50 15 빠르게 정착 1 300 20
Watch out
WARNING속도 단위 변환을 잊지 마세요
GestureDetector의 속도는 픽셀/초 단위입니다.
AnimationController는 0~1 범위에서 동작합니다.변환 공식:
// 픽셀/초 → 애니메이션 단위final unitVelocityX = pixelsPerSecond.dx / screenWidth;final unitVelocityY = pixelsPerSecond.dy / screenHeight;변환하지 않으면 애니메이션이 너무 빠르거나 느려집니다.
결론: SpringSimulation과 SpringDescription으로 튕기는 효과를 만들고, 매개변수를 조절해 원하는 느낌을 만듭니다.
챕터 3: GravitySimulation으로 낙하 효과
Why
NOTE게임에서 물체가 떨어질 때 중력의 영향을 받습니다.
처음에는 천천히, 점점 빠르게 떨어집니다.이런 등가속도 운동을 구현하려면
GravitySimulation을 사용합니다.
What
NOTE
GravitySimulation은 일정한 가속도로 물체가 움직이는 것을 시뮬레이션합니다.생성자 매개변수:
GravitySimulation(acceleration, // 가속도 (픽셀/초²)distance, // 시작 위치endDistance, // 종료 조건 (이 거리를 넘으면 끝)velocity, // 초기 속도)
매개변수 의미 예시 값 acceleration중력 가속도 9.8 (현실), 500~2000 (앱) distance시작 위치 0.0 endDistance바닥 위치 화면 높이 velocity초기 속도 0.0 (정지 상태에서 시작)
How
TIP떨어지는 공 애니메이션
void _startFalling() {final screenHeight = MediaQuery.of(context).size.height;final simulation = GravitySimulation(1000, // 가속도: 아래로 1000 픽셀/초²0, // 시작: 맨 위 (0)screenHeight, // 끝: 화면 바닥0, // 초기 속도: 0 (정지 상태));_controller.animateWith(simulation);}위로 던지고 떨어지는 효과:
final simulation = GravitySimulation(500, // 가속도screenHeight, // 시작: 화면 아래0, // 끝: 화면 위-800, // 초기 속도: 위로 800 픽셀/초);초기 속도가 음수면 위로 올라갔다가 중력에 의해 아래로 떨어집니다.
Watch out
WARNINGendDistance는 방향에 따라 다르게 설정
GravitySimulation은 위치가endDistance를 초과하면 종료됩니다.// 아래로 떨어질 때: endDistance > 시작 위치GravitySimulation(500, 0, 300, 0); // 0에서 시작, 300 넘으면 끝// 위로 던질 때: 주의! endDistance 설정에 따라 다름// 위로 갔다가 아래로 돌아올 때까지 기다리려면 endDistance를 크게
결론: GravitySimulation으로 떨어지거나 던지는 물리 효과를 구현합니다.
챕터 4: FrictionSimulation으로 감속 효과
Why
NOTE스크롤을 빠르게 밀고 손을 떼면 어떻게 될까요?
바로 멈추지 않고 서서히 느려지다가 멈춥니다.이것이 마찰(friction) 효과입니다.
FrictionSimulation은 이런 감속 움직임을 시뮬레이션합니다.
What
NOTE
FrictionSimulation은 마찰력에 의해 점점 느려지는 움직임을 시뮬레이션합니다.생성자 매개변수:
FrictionSimulation(drag, // 마찰 계수 (단위 없음)position, // 시작 위치velocity, // 초기 속도)
매개변수 의미 값이 크면? drag마찰 계수 빨리 멈춤 position시작 위치 - velocity초기 속도 더 멀리 이동 drag 값 가이드:
drag 값 효과 사용 예시 0.01~0.05 매우 미끄러움 빙판 위 0.1~0.3 적당한 마찰 일반 스크롤 0.5~1.0 높은 마찰 빠른 정지
How
TIP스크롤 관성 효과 구현
void _onPanEnd(DragEndDetails details) {final velocity = details.velocity.pixelsPerSecond.dx;final simulation = FrictionSimulation(0.15, // 마찰 계수_offset, // 현재 위치velocity, // 드래그 속도);_controller.animateWith(simulation);}팩토리 생성자로 정확한 위치까지 이동:
// 시작 위치에서 끝 위치까지, 지정한 속도로 감속 이동final simulation = FrictionSimulation.through(0, // 시작 위치300, // 끝 위치100, // 시작 속도0, // 끝 속도 (멈춤));
through()팩토리는 원하는 시작/끝 조건에 맞는drag값을 자동 계산합니다.
Watch out
WARNING시뮬레이션 종료 조건
FrictionSimulation은 속도가 거의 0이 되면 종료됩니다.
drag값이 너무 작으면 매우 오래 걸릴 수 있습니다.// tolerance로 종료 조건 조절final simulation = FrictionSimulation(0.05,0,1000,tolerance: Tolerance(velocity: 0.1), // 속도가 0.1 이하면 종료);
결론: FrictionSimulation으로 스크롤 관성처럼 서서히 멈추는 효과를 구현합니다.
챕터 5: 제스처 속도를 애니메이션에 연결하기
Why
NOTE물리 시뮬레이션의 핵심은 사용자 입력을 반영하는 것입니다.
빠르게 밀면 빠르게, 천천히 밀면 천천히 반응해야 자연스럽습니다.
GestureDetector의onPanEnd에서 제공하는velocity를 시뮬레이션에 전달하면 됩니다.
What
NOTE
DragEndDetails는 드래그가 끝날 때의 속도 정보를 제공합니다.onPanEnd: (DragEndDetails details) {final velocity = details.velocity.pixelsPerSecond;// velocity.dx: 수평 속도 (픽셀/초)// velocity.dy: 수직 속도 (픽셀/초)}이 속도를 시뮬레이션의
velocity매개변수로 전달합니다.
How
TIP완전한 드래그-스프링 애니메이션 예제
import 'package:flutter/material.dart';import 'package:flutter/physics.dart';class PhysicsCard extends StatefulWidget {@override_PhysicsCardState createState() => _PhysicsCardState();}class _PhysicsCardState extends State<PhysicsCard>with SingleTickerProviderStateMixin {late AnimationController _controller;late Animation<Alignment> _animation;Alignment _dragAlignment = Alignment.center;@overridevoid initState() {super.initState();_controller = AnimationController(vsync: this);_controller.addListener(() {setState(() => _dragAlignment = _animation.value);});}@overridevoid dispose() {_controller.dispose();super.dispose();}void _runAnimation(Offset pixelsPerSecond, Size size) {// 1. Tween 설정: 현재 위치 → 중앙_animation = _controller.drive(AlignmentTween(begin: _dragAlignment, end: Alignment.center),);// 2. 속도 변환: 픽셀/초 → 애니메이션 단위final unitsPerSecond = Offset(pixelsPerSecond.dx / size.width,pixelsPerSecond.dy / size.height,);final unitVelocity = unitsPerSecond.distance;// 3. 스프링 시뮬레이션 생성const spring = SpringDescription(mass: 1,stiffness: 180,damping: 15,);final simulation = SpringSimulation(spring, 0, 1, -unitVelocity);// 4. 애니메이션 실행_controller.animateWith(simulation);}@overrideWidget build(BuildContext context) {final size = MediaQuery.of(context).size;return GestureDetector(onPanDown: (_) => _controller.stop(), // 드래그 시작: 애니메이션 중지onPanUpdate: (details) {setState(() {_dragAlignment += Alignment(details.delta.dx / (size.width / 2),details.delta.dy / (size.height / 2),);});},onPanEnd: (details) {_runAnimation(details.velocity.pixelsPerSecond, size);},child: Align(alignment: _dragAlignment,child: Card(child: FlutterLogo(size: 128),),),);}}코드 흐름 요약:
graph TD A[onPanDown] -->|_controller.stop| B[드래그 시작] B --> C[onPanUpdate] C -->|_dragAlignment 갱신| C C --> D[onPanEnd] D -->|velocity 전달| E[_runAnimation] E -->|SpringSimulation| F[animateWith] F -->|addListener| G[setState]
Watch out
WARNINGonPanDown에서 진행 중인 애니메이션을 멈추세요
카드가 튕기며 돌아오는 중에 다시 드래그하면 어떻게 될까요?
애니메이션이 계속 진행되면서 드래그가 꼬입니다.onPanDown: (details) {_controller.stop(); // 필수!},이 한 줄로 “드래그하면 애니메이션 중지”를 보장합니다.
결론: GestureDetector의 속도 정보를 시뮬레이션에 전달하면 사용자 입력에 반응하는 자연스러운 애니메이션이 됩니다.
챕터 6: 시뮬레이션 조합하기
Why
NOTE실제 앱에서는 하나의 시뮬레이션만 사용하지 않을 수 있습니다.
예를 들어, 위로 던지고(Gravity) → 바닥에서 튕기기(Spring) 같은 조합이 필요할 수 있습니다.Flutter는 시뮬레이션을 순차적으로 연결할 수 있습니다.
What
NOTE시뮬레이션이 끝났는지 확인하려면
isDone(time)또는 컨트롤러의status를 사용합니다._controller.addStatusListener((status) {if (status == AnimationStatus.completed) {// 다음 시뮬레이션 시작_startNextSimulation();}});
How
TIP던지고 튕기는 공
void _throwAndBounce(double throwVelocity) {// 1단계: 위로 던지기 (중력 시뮬레이션)final gravitySimulation = GravitySimulation(500, // 중력0, // 시작 위치300, // 바닥 위치-throwVelocity, // 위로 던지는 속도);_controller.animateWith(gravitySimulation);// 2단계: 바닥에 닿으면 튕기기_controller.addStatusListener((status) {if (status == AnimationStatus.completed) {final bounceSimulation = SpringSimulation(SpringDescription(mass: 1, stiffness: 500, damping: 20),_controller.value, // 현재 위치0, // 목표: 바닥0, // 속도: 0 (정지));_controller.animateWith(bounceSimulation);}});}
Watch out
WARNING상태 리스너 중복 등록 주의
addStatusListener를 여러 번 호출하면 리스너가 중복 등록됩니다.// ❌ 매번 새 리스너 추가void _runAnimation() {_controller.addStatusListener((status) { ... }); // 누적됨!_controller.animateWith(simulation);}// ✅ initState에서 한 번만 등록@overridevoid initState() {super.initState();_controller.addStatusListener(_onAnimationStatusChanged);}
결론: 시뮬레이션을 순차적으로 연결하면 복잡한 물리 효과를 만들 수 있습니다.
한계
물리 기반 애니메이션은 자연스럽지만 모든 상황에 적합하지는 않습니다.
- 매개변수 튜닝: mass, stiffness, damping 등의 값을 찾는 데 시행착오가 필요합니다.
- 예측 불가능한 시간: 애니메이션 종료 시점을 정확히 알기 어렵습니다.
- 복잡한 물리: 충돌, 회전, 다중 물체 상호작용은 추가 구현이 필요합니다.
- 성능: 매우 복잡한 시뮬레이션은 프레임 드롭을 유발할 수 있습니다.
Footnotes
-
animateWith(시뮬레이션): AnimationController의 메서드로, duration 대신 Simulation 객체에 따라 애니메이션을 실행한다. ↩
-
Simulation(시뮬레이션): Flutter physics 라이브러리의 추상 클래스로, x(time), dx(time), isDone(time) 메서드를 통해 시간에 따른 위치와 속도를 계산한다. ↩
-
SpringSimulation(스프링 시뮬레이션): 스프링에 매달린 물체의 움직임을 시뮬레이션하는 클래스다. 후크의 법칙을 따른다. ↩
-
GravitySimulation(중력 시뮬레이션): 일정한 가속도로 물체가 움직이는 것을 시뮬레이션하는 클래스다. 뉴턴의 제2법칙을 따른다. ↩
-
FrictionSimulation(마찰 시뮬레이션): 마찰력에 의해 점점 느려지는 움직임을 시뮬레이션하는 클래스다. 유체 저항을 모델링한다. ↩
공유
이 글이 도움이 되었다면 다른 사람과 공유해주세요!