
flutter 공식 문서 : 물리 시뮬레이션을 사용하여 위젯 애니메이션하기 번역
물리 시뮬레이션은 앱 상호작용을 현실적이고 상호적으로 느끼게 만들 수 있습니다. 예를 들어, 위젯을 마치 스프링이나 중력에 의해 떨어지는 것처럼 애니메이션하고 싶을 수 있습니다.
이 레시피는 스프링 시뮬레이션을 사용하여 드래그된 지점에서 중심으로 위젯을 이동하는 방법을 보여줍니다.
이 레시피는 다음 단계를 사용합니다:
DraggableCard라는 상태가 있는 위젯으로 시작합니다:
import 'package:flutter/material.dart';
void main() {
runApp(const MaterialApp(home: PhysicsCardDragDemo()));
}
class PhysicsCardDragDemo extends StatelessWidget {
const PhysicsCardDragDemo({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: const DraggableCard(
child: FlutterLogo(
size: 128,
),
),
);
}
}
class DraggableCard extends StatefulWidget {
const DraggableCard({required this.child, super.key});
final Widget child;
State<DraggableCard> createState() => _DraggableCardState();
}
class _DraggableCardState extends State<DraggableCard> {
void initState() {
super.initState();
}
void dispose() {
super.dispose();
}
Widget build(BuildContext context) {
return Align(
child: Card(
child: widget.child,
),
);
}
}
_DraggableCardState 클래스를 SingleTickerProviderStateMixin에서 확장하게 만들고, initState에서 AnimationController를 구성하고 vsync를 this로 설정합니다.
참고: SingleTickerProviderStateMixin을 확장하면 상태 객체가 AnimationController의 TickerProvider가 될 수 있습니다. 자세한 정보는 TickerProvider 문서를 참조하세요.
위젯이 드래그될 때 이동하게 만들고, _DraggableCardState 클래스에 Alignment 필드를 추가합니다:
// _DraggableCardState 클래스에 Alignment 필드 추가
Alignment _dragAlignment = Alignment.center;
onPanDown, onPanUpdate, onPanEnd 콜백을 처리하는 GestureDetector를 추가합니다. Align 위젯의 alignment를 _dragAlignment로 설정하여 정렬을 조정합니다:
Widget build(BuildContext context) {
var size = MediaQuery.of(context).size;
return GestureDetector(
onPanDown: (details) {},
onPanUpdate: (details) {
setState(() {
_dragAlignment += Alignment(
details.delta.dx / (size.width / 2),
details.delta.dy / (size.height / 2),
);
});
},
onPanEnd: (details) {},
child: Align(
alignment: _dragAlignment,
child: Card(
child: widget.child,
),
),
);
}
위젯이 놓였을 때, 그것이 중심으로 스프링 되돌아가야 합니다.
Animation<Alignment> 필드와 _runAnimation 메소드를 추가합니다. 이 메소드는 위젯이 드래그된 지점에서 중심점으로 보간하는 Tween을 정의합니다:
void _runAnimation() {
_animation = _controller.drive(
AlignmentTween(
begin: _dragAlignment,
end: Alignment.center,
),
);
_controller.reset();
_controller.forward();
}
AnimationController가 값을 생성할 때 _dragAlignment를 업데이트합니다.
드래그가 끝난 후 위젯의 속도를 계산하여, 위젯이 그 속도로 실제로 계속 움직인 후에 스냅되도록 합니다. 먼저 flutter/physics.dart 패키지를 가져옵니다:
import 'package:flutter/physics.dart';
onPanEnd 콜백은 화면에서 손가락이 떨어졌을 때 포인터의 속도를 제공하는 DragEndDetails 객체를 제공합니다. 이 속도는 초당 픽셀 단위이지만, Align 위젯은 픽셀을 사용하지 않습니다. 대신 [-1.0, -1.0]에서 [1.0, 1.0] 사이의 좌표 값을 사용합니다. 여기서 [0.0, 0.0]은 중심을 나타냅니다. 2단계에서 계산된 크기를 사용하여 픽셀을 이 범위의 좌표 값으로 변환합니다.
마지막으로, AnimationController에는 SpringSimulation을 제공할 수 있는 animateWith() 메소드가 있습니다:
void _runAnimation(Offset pixelsPerSecond, Size size) {
_animation = _controller.drive(
AlignmentTween(
begin: _dragAlignment,
end: Alignment.center,
),
);
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);
}
속도와 크기를 사용하여 _runAnimation()을 호출하는 것을 잊지 마세요:
onPanEnd: (details) {
_runAnimation(details.velocity.pixelsPerSecond, size);
}
```dart
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});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: const DraggableCard(
child: FlutterLogo(
size: 128,
),
),
);
}
}
/// 놓았을 때 [Alignment.center]로 돌아가는 드래그 가능한 카드입니다.
class DraggableCard extends StatefulWidget {
const DraggableCard({required this.child, super.key});
final Widget child;
State<DraggableCard> createState() => _DraggableCardState();
}
class _DraggableCardState extends State<DraggableCard>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
/// 카드가 드래그되거나 애니메이션되는 동안의 정렬입니다.
///
/// 카드가 드래그되는 동안 이 값은 GestureDetector onPanUpdate 콜백에서 계산된 값으로 설정됩니다.
/// 애니메이션이 실행 중이면, 이 값은 [_animation]의 값으로 설정됩니다.
Alignment _dragAlignment = Alignment.center;
late Animation<Alignment> _animation;
/// [SpringSimulation]을 계산하고 실행합니다.
void _runAnimation(Offset pixelsPerSecond, Size size) {
_animation = _controller.drive(
AlignmentTween(
begin: _dragAlignment,
end: Alignment.center,
),
);
// 애니메이션 컨트롤러에서 사용되는 단위 구간, [0,1]에 대해 상대적인 속도를 계산합니다.
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);
}
void initState() {
super.initState();
_controller = AnimationController(vsync: this);
_controller.addListener(() {
setState(() {
_dragAlignment = _animation.value;
});
});
}
void dispose() {
_controller.dispose();
super.dispose();
}
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,
),
),
);
}
}