물리 시뮬레이션을 사용하면 앱과의 상호작용을 사실적으로 느낄 수 있다.
스프링 시뮬레이션을 사용하여 위젯을 드래그한 지점에서 중앙으로 돌아오는 애니메이션을 넣어보자
State를 확장하고 컨트롤러를 초기화 시킨다.
class _DraggableCardState extends State<DraggableCard>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
void initState() {
super.initState();
_controller =
AnimationController(vsync: this, duration: const Duration(seconds: 1));
_controller.addListener(() {
setState(() {
_dragAlignment = _animation.value;
});
});
}
void dispose() {
_controller.dispose();
super.dispose();
}
}
class _DraggableCardState extends State<DraggableCard>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
+ Alignment _dragAlignment = Alignment.center;
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,
),
),
);
}
Widget
제스처가 끝나면 위젯이 다시 중앙으로 돌아갈 수 있도록 애니메이션을 추가하자
state 안에서 _runAnimation() 를 정의해준다.
class _DraggableCardState extends State<DraggableCard>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
+ late Animation<Alignment> _animation;
Alignment _dragAlignment = Alignment.center;
/// 위젯이 중앙으로 돌아가는 애니메이션
void _runAnimation() {
_animation = _controller.drive(
AlignmentTween(
begin: _dragAlignment,
end: Alignment.center,
),
);
_controller.reset();
_controller.forward();
}
_controller.addListener(() {
setState(() {
_dragAlignment = _animation.value;
});
});
child: Align(
alignment: _dragAlignment,
child: Card(
child: widget.child,
),
),
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();
},
);
/// 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);
}
호출된 곳에서 인자값도 업데이트 해주자
onPanEnd: (details) {
_runAnimation(details.velocity.pixelsPerSecond, size);
},
import 'package:flutter/material.dart';
import 'package:flutter/physics.dart';
void main() {
runApp(
const MaterialApp(
home: Page1(),
),
);
}
class Page1 extends StatelessWidget {
const Page1({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: const Center(
child: 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>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<Alignment> _animation;
Alignment _dragAlignment = Alignment.center;
void initState() {
super.initState();
_controller =
AnimationController(vsync: this, duration: const Duration(seconds: 1));
_controller.addListener(() {
setState(() {
_dragAlignment = _animation.value;
});
});
}
void dispose() {
_controller.dispose();
super.dispose();
}
/// 위젯이 중앙으로 돌아가는 애니메이션
/// 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);
}
Widget build(BuildContext context) {
var 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,
),
),
);
}
}