개요

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: 저수준 포인터 이벤트를 처리하는 위젯