티스토리 뷰
Flutter에서 애니메이션을 구현할 때, 물리 법칙을 적용한 애니메이션은 사용자에게 보다 현실적이고 직관적인 상호작용을 제공할 수 있습니다. 이번 포스트에서는 Flutter에서 Physics Simulation(물리 시뮬레이션)을 사용하여 애니메이션을 구현하는 방법을 설명하겠습니다. 특히 SpringSimulation을 통해 물리적인 탄성 효과를 적용해, 위젯이 자연스럽게 움직이고 원래 위치로 돌아가는 과정을 단계별로 알아봅니다.
AnimationController 설정하기
Flutter에서 애니메이션을 관리하기 위해서는 AnimationController가 필수적입니다. 이 컨트롤러는 애니메이션의 시작과 끝, 그리고 진행 상태를 제어합니다. 먼저, StatefulWidget을 사용해 위젯을 관리하고, SingleTickerProviderStateMixin을 사용하여 AnimationController에 필요한 Ticker를 제공하도록 설정합니다.
class _DraggableCardState extends State<DraggableCard>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 1),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
여기서 vsync는 애니메이션 성능을 최적화하기 위한 옵션으로, 애니메이션이 보이지 않는 상황에서 리소스를 낭비하지 않도록 도와줍니다.
제스처로 위젯 이동시키기
사용자가 위젯을 드래그할 수 있도록 제스처 인식기를 추가합니다. GestureDetector를 활용하여 사용자의 터치 이벤트를 감지하고, 이를 통해 위젯이 화면에서 움직이도록 만듭니다.
@override
Widget build(BuildContext context) {
return GestureDetector(
onPanUpdate: (details) {
setState(() {
_dragAlignment += Alignment(
details.delta.dx / (size.width / 2),
details.delta.dy / (size.height / 2),
);
});
},
child: Align(
alignment: _dragAlignment,
child: Card(child: widget.child),
),
);
}
여기서 onPanUpdate는 드래그 중일 때 호출되는 콜백 함수로, 사용자의 움직임에 따라 위젯의 위치를 조정합니다. _dragAlignment를 사용하여 위젯의 현재 위치를 추적합니다.
애니메이션으로 위젯 이동시키기
위젯이 드래그에서 손을 떼면, 자연스럽게 원래 위치로 돌아오도록 애니메이션을 적용합니다. 이를 위해 Animation와 Tween을 사용하여 위젯의 시작 위치와 끝 위치를 정의합니다.
void _runAnimation() {
_animation = _controller.drive(
AlignmentTween(
begin: _dragAlignment,
end: Alignment.center,
),
);
_controller.reset();
_controller.forward();
}
위 코드는 애니메이션을 시작할 때 사용되며, 위젯이 현재 위치에서 Alignment.center로 돌아오는 과정을 관리합니다.
속도 계산을 통한 자연스러운 움직임
위젯이 움직이는 속도를 계산하여, 물리 법칙에 따라 자연스럽게 애니메이션이 진행되도록 해야 합니다. 이를 위해 onPanEnd에서 DragEndDetails 객체를 사용하여 드래그 종료 시의 속도를 얻고, 이를 물리 시뮬레이션에 적용합니다.
void _runAnimation(Offset pixelsPerSecond, Size size) {
final unitsPerSecondX = pixelsPerSecond.dx / size.width;
final unitsPerSecondY = pixelsPerSecond.dy / size.height;
final unitsPerSecond = Offset(unitsPerSecondX, unitsPerSecondY);
final unitVelocity = unitsPerSecond.distance;
final spring = SpringDescription(
mass: 30,
stiffness: 1,
damping: 1,
);
final simulation = SpringSimulation(spring, 0, 1, -unitVelocity);
_controller.animateWith(simulation);
}
SpringSimulation은 위젯이 자연스럽게 제자리로 돌아오는 탄성 효과를 구현합니다. 이를 통해 사용자는 물리적인 상호작용을 경험할 수 있으며, 애니메이션이 더욱 현실감 있게 작동합니다.
결론: Flutter에서 물리 기반 애니메이션으로 사용자 경험 향상
Flutter에서 제공하는 Physics Simulation을 사용하면 애니메이션이 물리적 법칙을 따르며 더욱 현실적이고 자연스럽게 동작하도록 만들 수 있습니다. AnimationController와 SpringSimulation을 적절히 활용하여, 사용자와의 상호작용에서 보다 직관적이고 부드러운 애니메이션을 구현할 수 있습니다. 실제 앱 개발에서 이러한 물리 애니메이션을 적용하면 사용자 경험을 크게 향상시킬 수 있으며, 특히 게임이나 동적인 UI 요소를 사용할 때 매우 유용하게 적용됩니다.
https://docs.flutter.dev/cookbook/animation/physics-simulation
import 'package:flutter/material.dart';
import 'package:flutter/physics.dart';
void main() {
runApp(const MaterialApp(home: PhysicsCardDragDemo()));
}
class PhysicsCardDragDemo extends StatelessWidget {
const PhysicsCardDragDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: const DraggableCard(
child: FlutterLogo(
size: 128,
),
),
);
}
}
/// A draggable card that moves back to [Alignment.center] when it's
/// released.
class DraggableCard extends StatefulWidget {
const DraggableCard({required this.child, super.key});
final Widget child;
@override
State<DraggableCard> createState() => _DraggableCardState();
}
class _DraggableCardState extends State<DraggableCard>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
/// The alignment of the card as it is dragged or being animated.
///
/// While the card is being dragged, this value is set to the values computed
/// in the GestureDetector onPanUpdate callback. If the animation is running,
/// this value is set to the value of the [_animation].
Alignment _dragAlignment = Alignment.center;
late Animation<Alignment> _animation;
/// Calculates and runs a [SpringSimulation].
void _runAnimation(Offset pixelsPerSecond, Size size) {
_animation = _controller.drive(
AlignmentTween(
begin: _dragAlignment,
end: Alignment.center,
),
);
// Calculate the velocity relative to the unit interval, [0,1],
// used by the animation controller.
final unitsPerSecondX = pixelsPerSecond.dx / size.width;
final unitsPerSecondY = pixelsPerSecond.dy / size.height;
final unitsPerSecond = Offset(unitsPerSecondX, unitsPerSecondY);
final unitVelocity = unitsPerSecond.distance;
const spring = SpringDescription(
mass: 30,
stiffness: 1,
damping: 1,
);
final simulation = SpringSimulation(spring, 0, 1, -unitVelocity);
_controller.animateWith(simulation);
}
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this);
_controller.addListener(() {
setState(() {
_dragAlignment = _animation.value;
});
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
return GestureDetector(
onPanDown: (details) {
_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: widget.child,
),
),
);
}
}
'Flutter Cookbook' 카테고리의 다른 글
SnackBar 구현하기: 사용자가 놓치지 않을 메시지를 전달하는 방법 (3) | 2024.10.12 |
---|---|
Drawer 위젯을 사용하는 방법: 단계별 가이드 (0) | 2024.10.11 |
Flutter에서 Opacity 애니메이션을 쉽게 구현하는 방법 (0) | 2024.10.10 |
Flutter의 애니메이션 기초: AnimatedContainer로 시작하는 간단한 애니메이션 구현 (5) | 2024.10.09 |
Flutter 페이지 전환 애니메이션 쉽게 이해하기: PageRouteBuilder 활용법 (0) | 2024.10.08 |