개요
GestureDetector는 사용자의 제스처와 마우스 이벤트를 감지하는 위젯입니다.
GestureDetector는 자식 위젯을 감싸서 다양한 사용자 상호작용 이벤트를 감지하고 처리할 수 있게 해줍니다. 클릭, 마우스 이동, 드래그 등 다양한 이벤트 핸들러를 제공하여 인터랙티브한 UI를 구현할 수 있습니다. 이벤트는 자식 위젯의 영역 내에서만 감지됩니다.
참고: https://api.flutter.dev/flutter/widgets/GestureDetector-class.html
언제 사용하나요?
- 버튼이나 카드에 클릭 이벤트를 추가할 때
- 드래그 앤 드롭 기능을 구현할 때
- 호버 효과를 적용할 때
- 커스텀 인터랙티브 위젯을 만들 때
- 마우스 위치를 추적해야 할 때
기본 사용법
GestureDetector({
onClick: (e) => {
console.log('클릭됨!');
},
child: Container({
width: 100,
height: 100,
color: 'blue',
child: Center({
child: Text('클릭하세요')
})
})
})
Props
onClick
값: ((e: MouseEvent) => void) | undefined
마우스 클릭 시 호출되는 콜백 함수입니다.
GestureDetector({
onClick: (e) => {
console.log(`클릭 위치: ${e.clientX}, ${e.clientY}`);
},
child: Text('클릭 가능한 텍스트')
})
onMouseDown
값: ((e: MouseEvent) => void) | undefined
마우스 버튼을 누를 때 호출되는 콜백 함수입니다.
GestureDetector({
onMouseDown: (e) => {
console.log('마우스 버튼 눌림');
},
child: Container({ width: 100, height: 100 })
})
onMouseUp
값: ((e: MouseEvent) => void) | undefined
마우스 버튼을 뗄 때 호출되는 콜백 함수입니다.
GestureDetector({
onMouseUp: (e) => {
console.log('마우스 버튼 뗌');
},
child: Container({ width: 100, height: 100 })
})
onMouseMove
값: ((e: MouseEvent) => void) | undefined
마우스가 움직일 때 호출되는 콜백 함수입니다.
GestureDetector({
onMouseMove: (e) => {
console.log(`마우스 위치: ${e.clientX}, ${e.clientY}`);
},
child: Container({ width: 200, height: 200 })
})
onMouseEnter
값: ((e: MouseEvent) => void) | undefined
마우스가 위젯 영역에 진입할 때 호출되는 콜백 함수입니다.
GestureDetector({
onMouseEnter: (e) => {
console.log('마우스 진입');
},
child: Container({ color: 'gray' })
})
onMouseLeave
값: ((e: MouseEvent) => void) | undefined
마우스가 위젯 영역을 벗어날 때 호출되는 콜백 함수입니다.
GestureDetector({
onMouseLeave: (e) => {
console.log('마우스 떠남');
},
child: Container({ color: 'gray' })
})
onMouseOver
값: ((e: MouseEvent) => void) | undefined
마우스가 위젯 위에 있을 때 호출되는 콜백 함수입니다.
GestureDetector({
onMouseOver: (e) => {
console.log('마우스 오버');
},
child: Container({ width: 100, height: 100 })
})
onDragStart
값: ((e: MouseEvent) => void) | undefined
드래그를 시작할 때 호출되는 콜백 함수입니다. onMouseDown 이벤트와 함께 발생합니다.
GestureDetector({
onDragStart: (e) => {
console.log('드래그 시작');
},
child: Container({ width: 50, height: 50, color: 'red' })
})
onDragMove
값: ((e: MouseEvent) => void) | undefined
드래그 중일 때 호출되는 콜백 함수입니다.
let position = { x: 0, y: 0 };
GestureDetector({
onDragMove: (e) => {
position.x += e.movementX;
position.y += e.movementY;
console.log(`드래그 위치: ${position.x}, ${position.y}`);
},
child: Container({ width: 50, height: 50 })
})
onDragEnd
값: ((e: MouseEvent) => void) | undefined
드래그를 끝낼 때 호출되는 콜백 함수입니다.
GestureDetector({
onDragEnd: (e) => {
console.log('드래그 종료');
},
child: Container({ width: 50, height: 50 })
})
onWheel
값: ((e: WheelEvent) => void) | undefined
마우스 휠을 사용할 때 호출되는 콜백 함수입니다.
GestureDetector({
onWheel: (e) => {
console.log(`휠 델타: ${e.deltaY}`);
e.preventDefault(); // 기본 스크롤 방지
},
child: Container({ width: 200, height: 200 })
})
cursor
값: Cursor (기본값: “pointer”)
마우스 커서의 모양을 지정합니다.
사용 가능한 커서 타입:
"pointer"
: 손가락 모양 (기본값)"default"
: 기본 화살표"move"
: 이동 커서"text"
: 텍스트 선택 커서"wait"
: 대기 커서"help"
: 도움말 커서"crosshair"
: 십자선"grab"
: 잡기 커서"grabbing"
: 잡는 중 커서"not-allowed"
: 금지 커서- 리사이즈 커서:
"n-resize"
,"e-resize"
,"s-resize"
,"w-resize"
등
class DraggableCursor extends StatefulWidget {
createState() {
return new DraggableCursorState();
}
}
class DraggableCursorState extends State<DraggableCursor> {
cursor = 'grab';
build() {
return GestureDetector({
cursor: this.cursor,
onDragStart: () => this.setState(() => this.cursor = 'grabbing'),
onDragEnd: () => this.setState(() => this.cursor = 'grab'),
child: Container({ width: 100, height: 100 })
});
}
}
child
값: Widget | undefined
이벤트를 감지할 자식 위젯입니다.
GestureDetector({
onClick: () => console.log('클릭!'),
child: Container({
padding: EdgeInsets.all(20),
color: 'blue',
child: Text('클릭 가능한 영역')
})
})
실제 사용 예제
예제 1: 인터랙티브 버튼
class InteractiveButton extends StatefulWidget {
createState() {
return new InteractiveButtonState();
}
}
class InteractiveButtonState extends State<InteractiveButton> {
isPressed = false;
isHovered = false;
build() {
return GestureDetector({
onMouseDown: () => this.setState(() => this.isPressed = true),
onMouseUp: () => this.setState(() => this.isPressed = false),
onMouseEnter: () => this.setState(() => this.isHovered = true),
onMouseLeave: () => {
this.setState(() => {
this.isHovered = false;
this.isPressed = false;
});
},
onClick: () => console.log('버튼 클릭됨!'),
cursor: 'pointer',
child: Container({
padding: EdgeInsets.symmetric({ horizontal: 24, vertical: 12 }),
decoration: BoxDecoration({
color: this.isPressed ? '#1976D2' : (this.isHovered ? '#2196F3' : '#42A5F5'),
borderRadius: BorderRadius.circular(8),
boxShadow: this.isPressed ? [] : [
BoxShadow({
color: 'rgba(0, 0, 0, 0.2)',
blurRadius: 4,
offset: { x: 0, y: 2 }
})
]
}),
child: Text('커스텀 버튼', {
style: TextStyle({ color: 'white', fontWeight: 'bold' })
})
})
});
}
}
예제 2: 드래그 가능한 요소
class DraggableBox extends StatefulWidget {
createState() {
return new DraggableBoxState();
}
}
class DraggableBoxState extends State<DraggableBox> {
position = { x: 0, y: 0 };
isDragging = false;
build() {
return Transform({
transform: Matrix4.translation(this.position.x, this.position.y, 0),
child: GestureDetector({
onDragStart: () => this.setState(() => this.isDragging = true),
onDragMove: (e) => {
this.setState(() => {
this.position = {
x: this.position.x + e.movementX,
y: this.position.y + e.movementY
};
});
},
onDragEnd: () => this.setState(() => this.isDragging = false),
cursor: this.isDragging ? 'grabbing' : 'grab',
child: Container({
width: 100,
height: 100,
decoration: BoxDecoration({
color: this.isDragging ? 'orange' : 'blue',
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow({
color: 'rgba(0, 0, 0, 0.3)',
blurRadius: this.isDragging ? 8 : 4,
offset: { x: 0, y: this.isDragging ? 4 : 2 }
})
]
}),
child: Center({
child: Text('드래그하세요', {
style: TextStyle({ color: 'white' })
})
})
})
})
});
}
}
예제 3: 마우스 추적기
class MouseTracker extends StatefulWidget {
createState() {
return new MouseTrackerState();
}
}
class MouseTrackerState extends State<MouseTracker> {
mousePosition = { x: 0, y: 0 };
isInside = false;
build() {
return GestureDetector({
onMouseMove: (e) => {
const rect = e.currentTarget.getBoundingClientRect();
this.setState(() => {
this.mousePosition = {
x: e.clientX - rect.left,
y: e.clientY - rect.top
};
});
},
onMouseEnter: () => this.setState(() => this.isInside = true),
onMouseLeave: () => this.setState(() => this.isInside = false),
child: Container({
width: 300,
height: 200,
color: 'lightgray',
child: Stack({
children: [
if (this.isInside) Positioned({
left: this.mousePosition.x - 10,
top: this.mousePosition.y - 10,
child: Container({
width: 20,
height: 20,
decoration: BoxDecoration({
color: 'red',
borderRadius: BorderRadius.circular(10)
})
})
}),
Center({
child: Text(`마우스 위치: ${Math.round(this.mousePosition.x)}, ${Math.round(this.mousePosition.y)}`)
})
]
})
})
});
}
}
예제 4: 컨텍스트 메뉴
class ContextMenuExample extends StatefulWidget {
createState() {
return new ContextMenuExampleState();
}
}
class ContextMenuExampleState extends State<ContextMenuExample> {
showMenu = false;
menuPosition = { x: 0, y: 0 };
build() {
return Stack({
children: [
GestureDetector({
onClick: (e) => {
e.preventDefault();
if (e.button === 2) { // 오른쪽 클릭
this.setState(() => {
this.showMenu = true;
this.menuPosition = { x: e.clientX, y: e.clientY };
});
} else {
this.setState(() => this.showMenu = false);
}
},
child: Container({
width: 400,
height: 300,
color: 'white',
child: Center({
child: Text('오른쪽 클릭하여 메뉴 열기')
})
})
}),
if (this.showMenu) Positioned({
left: this.menuPosition.x,
top: this.menuPosition.y,
child: Container({
width: 150,
padding: EdgeInsets.symmetric({ vertical: 8 }),
decoration: BoxDecoration({
color: 'white',
borderRadius: BorderRadius.circular(4),
boxShadow: [
BoxShadow({
color: 'rgba(0, 0, 0, 0.2)',
blurRadius: 8,
offset: { x: 0, y: 2 }
})
]
}),
child: Column({
children: ['복사', '붙여넣기', '삭제'].map(item =>
GestureDetector({
onClick: () => {
console.log(`${item} 선택됨`);
this.setState(() => this.showMenu = false);
},
cursor: 'pointer',
child: Container({
padding: EdgeInsets.symmetric({ horizontal: 16, vertical: 8 }),
child: Text(item)
})
})
)
})
})
})
]
});
}
}
예제 5: 줌 가능한 이미지
class ZoomableImage extends StatefulWidget {
src: string;
constructor({ src }: { src: string }) {
super();
this.src = src;
}
createState() {
return new ZoomableImageState();
}
}
class ZoomableImageState extends State<ZoomableImage> {
scale = 1;
build() {
return GestureDetector({
onWheel: (e) => {
e.preventDefault();
const delta = e.deltaY > 0 ? 0.9 : 1.1;
this.setState(() => {
this.scale = Math.max(0.5, Math.min(3, this.scale * delta));
});
},
cursor: this.scale > 1 ? 'zoom-out' : 'zoom-in',
child: Container({
width: 400,
height: 300,
clipped: true,
child: Transform({
transform: Matrix4.identity().scaled(this.scale, this.scale, 1),
alignment: Alignment.center,
child: Image({ src: this.widget.src, fit: 'cover' })
})
})
});
}
}
MouseEvent 객체
모든 마우스 이벤트 핸들러는 표준 MouseEvent 객체를 받습니다:
- clientX, clientY: 뷰포트 기준 마우스 위치
- pageX, pageY: 페이지 기준 마우스 위치
- screenX, screenY: 화면 기준 마우스 위치
- movementX, movementY: 이전 이벤트 대비 이동 거리
- button: 누른 버튼 (0: 왼쪽, 1: 중간, 2: 오른쪽)
- buttons: 현재 눌린 버튼들의 비트마스크
- ctrlKey, shiftKey, altKey, metaKey: 보조 키 상태
- currentTarget: 이벤트가 발생한 요소
- preventDefault(): 기본 동작 방지
- stopPropagation(): 이벤트 버블링 중단
주의사항
- child가 없으면 이벤트가 감지되지 않습니다.
- 이벤트는 child의 실제 렌더링 영역에서만 감지됩니다.
- 드래그 이벤트는 전역적으로 추적되므로 마우스가 위젯을 벗어나도 계속됩니다.
- onDragStart는 onMouseDown과 함께 발생하므로 두 이벤트 모두 처리할 때 주의가 필요합니다.
- 브라우저의 기본 동작을 막으려면 e.preventDefault()를 사용하세요.
관련 위젯
- InkWell: Material Design의 리플 효과가 있는 터치 반응 위젯
- Draggable: 드래그 앤 드롭을 위한 특화된 위젯
- MouseRegion: 마우스 호버 상태를 추적하는 위젯
- Listener: 저수준 포인터 이벤트를 처리하는 위젯