Flutter의 GestureDetector는 어떻게 사용하나요?
질문
Flutter에서 GestureDetector는 어떻게 사용하며, 다양한 제스처를 어떻게 인식할 수 있나요?
답변
Flutter의 GestureDetector
는 사용자의 다양한 터치 제스처를 감지하고 처리할 수 있게 해주는 위젯입니다. 이 위젯을 사용하면 탭, 더블 탭, 길게 누르기, 드래그, 스와이프 등 다양한 사용자 상호작용을 쉽게 구현할 수 있습니다.
기본 사용법
GestureDetector
의 가장 기본적인 사용법은 다음과 같습니다:
GestureDetector(
onTap: () {
print('위젯이 탭되었습니다!');
},
child: Container(
width: 100,
height: 100,
color: Colors.blue,
child: Center(
child: Text('탭 해보세요', style: TextStyle(color: Colors.white)),
),
),
)
위 코드는 파란색 상자를 만들고, 사용자가 이 상자를 탭하면 콘솔에 메시지를 출력합니다.
주요 제스처 콜백
GestureDetector
는 다양한 제스처를 감지할 수 있는 여러 콜백을 제공합니다:
1. 탭 관련 콜백
GestureDetector(
// 단일 탭
onTap: () {
print('탭되었습니다');
},
// 더블 탭
onDoubleTap: () {
print('더블 탭되었습니다');
},
// 길게 누르기
onLongPress: () {
print('길게 눌렸습니다');
},
// 길게 누르기 시작
onLongPressStart: (LongPressStartDetails details) {
print('길게 누르기 시작: ${details.globalPosition}');
},
// 길게 누르기 종료
onLongPressEnd: (LongPressEndDetails details) {
print('길게 누르기 종료: ${details.globalPosition}');
},
// 탭 다운 (손가락이 화면에 닿는 순간)
onTapDown: (TapDownDetails details) {
print('탭 다운: ${details.globalPosition}');
},
// 탭 업 (손가락이 화면에서 떨어지는 순간)
onTapUp: (TapUpDetails details) {
print('탭 업: ${details.globalPosition}');
},
// 탭 취소 (다른 제스처로 인식되어 탭이 취소된 경우)
onTapCancel: () {
print('탭이 취소되었습니다');
},
child: Container(
width: 150,
height: 150,
color: Colors.green,
child: Center(
child: Text('다양한 탭 테스트', style: TextStyle(color: Colors.white)),
),
),
)
2. 드래그(팬) 관련 콜백
GestureDetector(
// 드래그 시작
onPanStart: (DragStartDetails details) {
print('드래그 시작: ${details.globalPosition}');
},
// 드래그 중
onPanUpdate: (DragUpdateDetails details) {
print('드래그 업데이트: ${details.delta}');
// details.delta로 이동 거리를 알 수 있음
},
// 드래그 종료
onPanEnd: (DragEndDetails details) {
print('드래그 종료, 속도: ${details.velocity}');
// details.velocity로 손가락이 떨어질 때의 속도를 알 수 있음
},
child: Container(
width: 150,
height: 150,
color: Colors.orange,
child: Center(
child: Text('드래그 테스트', style: TextStyle(color: Colors.white)),
),
),
)
3. 방향성 드래그(스와이프) 콜백
특정 방향으로만 드래그를 감지하고 싶다면:
GestureDetector(
// 수평 드래그
onHorizontalDragStart: (DragStartDetails details) {
print('수평 드래그 시작');
},
onHorizontalDragUpdate: (DragUpdateDetails details) {
print('수평 드래그 업데이트: ${details.delta.dx}');
},
onHorizontalDragEnd: (DragEndDetails details) {
print('수평 드래그 종료');
},
// 수직 드래그
onVerticalDragStart: (DragStartDetails details) {
print('수직 드래그 시작');
},
onVerticalDragUpdate: (DragUpdateDetails details) {
print('수직 드래그 업데이트: ${details.delta.dy}');
},
onVerticalDragEnd: (DragEndDetails details) {
print('수직 드래그 종료');
},
child: Container(
width: 150,
height: 150,
color: Colors.purple,
child: Center(
child: Text('방향성 드래그 테스트', style: TextStyle(color: Colors.white)),
),
),
)
4. 스케일(확대/축소) 콜백
두 손가락을 사용한 확대/축소 제스처를 감지합니다:
GestureDetector(
// 스케일 시작
onScaleStart: (ScaleStartDetails details) {
print('스케일 시작: ${details.focalPoint}');
},
// 스케일 업데이트
onScaleUpdate: (ScaleUpdateDetails details) {
print('스케일 업데이트, 스케일: ${details.scale}, 회전: ${details.rotation}');
// details.scale: 확대/축소 비율
// details.rotation: 회전 각도 (라디안)
},
// 스케일 종료
onScaleEnd: (ScaleEndDetails details) {
print('스케일 종료, 속도: ${details.velocity}');
},
child: Container(
width: 200,
height: 200,
color: Colors.red,
child: Center(
child: Text('확대/축소 테스트', style: TextStyle(color: Colors.white)),
),
),
)
실전 예제: 드래그 가능한 위젯
드래그로 위치를 변경할 수 있는 위젯을 만들어 보겠습니다:
class DraggableWidget extends StatefulWidget {
@override
_DraggableWidgetState createState() => _DraggableWidgetState();
}
class _DraggableWidgetState extends State<DraggableWidget> {
double _x = 0.0;
double _y = 0.0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('드래그 가능한 위젯')),
body: Stack(
children: [
Positioned(
left: _x,
top: _y,
child: GestureDetector(
onPanUpdate: (details) {
setState(() {
_x += details.delta.dx;
_y += details.delta.dy;
});
},
child: Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(10),
),
child: Center(
child: Text(
'드래그하세요',
style: TextStyle(color: Colors.white),
),
),
),
),
),
],
),
);
}
}
실전 예제: 확대/축소 가능한 이미지
두 손가락으로 이미지를 확대/축소할 수 있는 위젯을 만들어 보겠습니다:
class ZoomableImage extends StatefulWidget {
final String imageUrl;
ZoomableImage({required this.imageUrl});
@override
_ZoomableImageState createState() => _ZoomableImageState();
}
class _ZoomableImageState extends State<ZoomableImage> {
double _scale = 1.0;
double _previousScale = 1.0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('확대/축소 가능한 이미지')),
body: Center(
child: GestureDetector(
onScaleStart: (ScaleStartDetails details) {
_previousScale = _scale;
},
onScaleUpdate: (ScaleUpdateDetails details) {
setState(() {
_scale = _previousScale * details.scale;
// 너무 작거나 크게 확대되는 것을 방지
_scale = _scale.clamp(0.5, 3.0);
});
},
child: Transform.scale(
scale: _scale,
child: Image.network(widget.imageUrl),
),
),
),
);
}
}
실전 예제: 회전 가능한 위젯
두 손가락으로 위젯을 회전할 수 있는 예제입니다:
class RotatableWidget extends StatefulWidget {
@override
_RotatableWidgetState createState() => _RotatableWidgetState();
}
class _RotatableWidgetState extends State<RotatableWidget> {
double _rotation = 0.0;
double _previousRotation = 0.0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('회전 가능한 위젯')),
body: Center(
child: GestureDetector(
onScaleStart: (ScaleStartDetails details) {
_previousRotation = _rotation;
},
onScaleUpdate: (ScaleUpdateDetails details) {
setState(() {
_rotation = _previousRotation + details.rotation;
});
},
child: Transform.rotate(
angle: _rotation,
child: Container(
width: 200,
height: 200,
color: Colors.green,
child: Center(
child: Text(
'회전하세요',
style: TextStyle(color: Colors.white, fontSize: 20),
),
),
),
),
),
),
);
}
}
실전 예제: 스와이프로 항목 삭제
스와이프 제스처로 목록 항목을 삭제하는 예제입니다:
class SwipeToDeleteList extends StatefulWidget {
@override
_SwipeToDeleteListState createState() => _SwipeToDeleteListState();
}
class _SwipeToDeleteListState extends State<SwipeToDeleteList> {
List<String> _items = List.generate(20, (index) => '항목 ${index + 1}');
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('스와이프로 삭제')),
body: ListView.builder(
itemCount: _items.length,
itemBuilder: (context, index) {
return GestureDetector(
onHorizontalDragEnd: (DragEndDetails details) {
// 왼쪽으로 스와이프 감지 (음수 속도)
if (details.velocity.pixelsPerSecond.dx < -1000) {
setState(() {
_items.removeAt(index);
});
}
},
child: Container(
height: 60,
margin: EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: Colors.blue[100],
borderRadius: BorderRadius.circular(5),
),
child: Center(
child: Text(
_items[index],
style: TextStyle(fontSize: 18),
),
),
),
);
},
),
);
}
}
실제 앱에서는 Dismissible
위젯을 사용하는 것이 더 좋습니다. 이 위젯은 스와이프 제스처와 애니메이션을 자동으로 처리합니다.
실전 예제: 복합 제스처 (드래그, 확대/축소, 회전)
드래그, 확대/축소, 회전을 모두 지원하는 복합 제스처 예제입니다:
class ComplexGestureWidget extends StatefulWidget {
@override
_ComplexGestureWidgetState createState() => _ComplexGestureWidgetState();
}
class _ComplexGestureWidgetState extends State<ComplexGestureWidget> {
double _x = 0.0;
double _y = 0.0;
double _scale = 1.0;
double _rotation = 0.0;
double _previousScale = 1.0;
double _previousRotation = 0.0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('복합 제스처')),
body: GestureDetector(
onScaleStart: (ScaleStartDetails details) {
_previousScale = _scale;
_previousRotation = _rotation;
},
onScaleUpdate: (ScaleUpdateDetails details) {
setState(() {
// 손가락이 1개면 드래그, 2개 이상이면 확대/축소와 회전
if (details.pointerCount == 1) {
_x += details.focalPointDelta.dx;
_y += details.focalPointDelta.dy;
} else {
_scale = (_previousScale * details.scale).clamp(0.5, 3.0);
_rotation = _previousRotation + details.rotation;
}
});
},
child: Stack(
children: [
Positioned(
left: _x,
top: _y,
child: Transform.scale(
scale: _scale,
child: Transform.rotate(
angle: _rotation,
child: Container(
width: 200,
height: 200,
decoration: BoxDecoration(
color: Colors.purple,
borderRadius: BorderRadius.circular(10),
),
child: Center(
child: Text(
'복합 제스처 테스트',
style: TextStyle(color: Colors.white, fontSize: 16),
textAlign: TextAlign.center,
),
),
),
),
),
),
],
),
),
);
}
}
제스처 인식 우선순위 및 경쟁
Flutter에서는 여러 제스처 인식기가 동시에 작동할 수 있습니다. 이로 인해 제스처 인식에 경쟁이 발생할 수 있습니다.
경쟁 해결 방법
- behavior 매개변수 사용
GestureDetector(
behavior: HitTestBehavior.opaque, // 또는 translucent, deferToChild
onTap: () {
print('탭 감지');
},
child: Container(/* ... */),
)
HitTestBehavior.opaque
: 자식 위젯이 투명하더라도 제스처를 감지합니다.HitTestBehavior.translucent
: 자식 위젯이 부분적으로 투명해도 제스처를 감지합니다.HitTestBehavior.deferToChild
: 자식 위젯이 제스처를 처리할지 결정합니다.RawGestureDetector 사용
더 세밀한 제어가 필요한 경우 RawGestureDetector
를 사용할 수 있습니다:
RawGestureDetector(
gestures: {
AllowMultipleGestureRecognizer: GestureRecognizerFactoryWithHandlers<AllowMultipleGestureRecognizer>(
() => AllowMultipleGestureRecognizer(),
(AllowMultipleGestureRecognizer instance) {
instance.onStart = (details) => print('탭 시작');
instance.onEnd = (details) => print('탭 종료');
},
),
},
child: Container(/* ... */),
)
// 커스텀 제스처 인식기
class AllowMultipleGestureRecognizer extends OneSequenceGestureRecognizer {
@override
String get debugDescription => 'customGesture';
@override
void didStopTrackingLastPointer(int pointer) {}
@override
void handleEvent(PointerEvent event) {
if (event is PointerDownEvent) {
onStart?.call(event.position);
}
if (event is PointerUpEvent) {
onEnd?.call(event.position);
}
}
Function(Offset position)? onStart;
Function(Offset position)? onEnd;
}
GestureDetector vs InkWell
Flutter에는 사용자 입력을 처리하는 다른 위젯도 있습니다. 특히 InkWell
은 Material Design의 물결 효과(ripple effect)를 제공하는 위젯입니다:
InkWell(
onTap: () {
print('InkWell 탭');
},
splashColor: Colors.red,
highlightColor: Colors.blue.withOpacity(0.5),
child: Container(
width: 100,
height: 100,
color: Colors.transparent,
child: Center(
child: Text('InkWell 테스트'),
),
),
)
GestureDetector
와 InkWell
의 주요 차이점:
InkWell
은 Material Design 효과를 제공합니다 (물결 효과).GestureDetector
는 더 많은 제스처 타입을 지원합니다.InkWell
은Material
위젯 내부에서 사용해야 합니다.
성능 고려사항
제스처 처리 최적화
- 불필요한 제스처 콜백을 등록하지 마세요.
- 복잡한 로직은 제스처 콜백 내에서 직접 실행하지 말고 별도의 메서드로 분리하세요.
setState() 호출 최소화
onPanUpdate
와 같이 자주 호출되는 콜백에서는setState()
호출을 최소화하세요.- 애니메이션 프레임당 한 번만 상태를 업데이트하는 것이 좋습니다.
// 개선된 드래그 예제
class OptimizedDragWidget extends StatefulWidget {
@override
_OptimizedDragWidgetState createState() => _OptimizedDragWidgetState();
}
class _OptimizedDragWidgetState extends State<OptimizedDragWidget> {
double _x = 0.0;
double _y = 0.0;
bool _isDragging = false;
@override
Widget build(BuildContext context) {
return Scaffold(
body: GestureDetector(
onPanStart: (_) {
_isDragging = true;
},
onPanUpdate: (details) {
// setState() 없이 값만 업데이트
_x += details.delta.dx;
_y += details.delta.dy;
// 강제로 다시 그리기
if (_isDragging) {
setState(() {});
}
},
onPanEnd: (_) {
_isDragging = false;
setState(() {});
},
child: Stack(
children: [
Positioned(
left: _x,
top: _y,
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
),
],
),
),
);
}
}
결론
Flutter의 GestureDetector
는 앱에 다양한 터치 상호작용을 추가할 수 있는 강력한 위젯입니다. 단순한 탭부터 복잡한 드래그, 확대/축소, 회전까지 다양한 제스처를 쉽게 처리할 수 있습니다.
적절한 제스처를 구현하여 앱의 사용자 경험을 향상시키면서도, 성능과 제스처 인식 우선순위에 주의를 기울이는 것이 중요합니다. Flutter의 풍부한 제스처 시스템을 활용하면 사용자에게 직관적이고 자연스러운 인터페이스를 제공할 수 있습니다.