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에서는 여러 제스처 인식기가 동시에 작동할 수 있습니다. 이로 인해 제스처 인식에 경쟁이 발생할 수 있습니다.

경쟁 해결 방법

  1. 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 테스트'),
    ),
  ),
)

GestureDetectorInkWell의 주요 차이점:

  • InkWell은 Material Design 효과를 제공합니다 (물결 효과).
  • GestureDetector는 더 많은 제스처 타입을 지원합니다.
  • InkWellMaterial 위젯 내부에서 사용해야 합니다.

성능 고려사항

  1. 제스처 처리 최적화

    • 불필요한 제스처 콜백을 등록하지 마세요.
    • 복잡한 로직은 제스처 콜백 내에서 직접 실행하지 말고 별도의 메서드로 분리하세요.
  2. 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의 풍부한 제스처 시스템을 활용하면 사용자에게 직관적이고 자연스러운 인터페이스를 제공할 수 있습니다.

results matching ""

    No results matching ""