본문 바로가기
Flutter

[Flutter] Widget(4) - GestureDetector

by 아마도개발자 2023. 12. 9.
GestureDetector Widget
  • GestureDetector위젯은 사용자의 동작을 감지하는 위젯이다. (마우스 좌클릭, 롱 프레스, 우클릭 등)
  • non-Null 콜백의 제스쳐에 대한 인식을 수행한다.
  • 자식요소가 있으면 위젯에 크기는 자식요소에 따라 결정되고, 자식이 없으면 대신 부모에 맞춰서 결정된다.

 

Align Widget 예제

 

다음 예제를 통해 GestureDetector위젯으로 클릭버튼을 감지하여 전구 상태의 on / off를 표현해보자 

 

  • 예제
import 'package:flutter/material.dart';

enum Department {
  treasury,
  state,
}

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(body: Sample()),
    );
  }
}

class Sample extends StatefulWidget {
  const Sample({Key? key}) : super(key: key);

  @override
  State<Sample> createState() => _SampleState();
}

class _SampleState extends State<Sample> {
  @override
  Widget build(BuildContext context) {
    bool _lightIsOn = false;
    return Container(
      alignment: FractionalOffset.center,
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: Icon(
              Icons.lightbulb_outline,
              color: _lightIsOn ? Colors.yellow.shade600 : Colors.black,
              size: 60,
            ),
          ),
          GestureDetector(
            onTap: () {
              setState(() {
                // Toggle light when tapped.
                _lightIsOn = !_lightIsOn;
              });
            },
            child: Container(
              color: Colors.yellow.shade600,
              padding: const EdgeInsets.all(8),
              // Change button text when light changes state.
              child: Text(_lightIsOn ? 'TURN LIGHT OFF' : 'TURN LIGHT ON'),
            ),
          ),
        ],
      ),
    );
  }
}

 

  • 실행화면

 

실행화면만 봤을 때, TURN LIGHT ON이 버튼으로 보일 수도 있다. 하지만 예제 코드를 확인해보면 button이 전혀 쓰이지 않았는데, 버튼의 클릭이벤트를 이용하는 대신, 컨테이너를 GestureDetector로 감싸 onTap 이벤트를 사용해서 클릭이벤트와 같은 기능을 만들었음을 알 수 있다. 

 

그렇다면 만약 GestureDetector의 위젯이 겹쳐있는 상황이면 어떻게 될까?

 

  • 예제
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';

/// Flutter code sample for [GestureDetector].

void main() {
  debugPrintGestureArenaDiagnostics = true;
  runApp(const NestedGestureDetectorsApp());
}

enum _OnTapWinner { none, yellow, green }

class NestedGestureDetectorsApp extends StatelessWidget {
  const NestedGestureDetectorsApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Nested GestureDetectors')),
        body: const NestedGestureDetectorsExample(),
      ),
    );
  }
}

class NestedGestureDetectorsExample extends StatefulWidget {
  const NestedGestureDetectorsExample({super.key});

  @override
  State<NestedGestureDetectorsExample> createState() =>
      _NestedGestureDetectorsExampleState();
}

class _NestedGestureDetectorsExampleState
    extends State<NestedGestureDetectorsExample> {
  bool _isYellowTranslucent = false;
  _OnTapWinner _winner = _OnTapWinner.none;
  final Border highlightBorder = Border.all(color: Colors.red, width: 5);

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        Expanded(
          child: GestureDetector(
            onTap: () {
              debugPrint('Green onTap');
              setState(() {
                _winner = _OnTapWinner.green;
              });
            },
            onTapDown: (_) => debugPrint('Green onTapDown'),
            onTapCancel: () => debugPrint('Green onTapCancel'),
            child: Container(
              alignment: Alignment.center,
              decoration: BoxDecoration(
                border: _winner == _OnTapWinner.green ? highlightBorder : null,
                color: Colors.green,
              ),
              child: GestureDetector(
                // Setting behavior to transparent or opaque as no impact on
                // parent-child hit testing. A tap on 'Yellow' is also in
                // 'Green' bounds. Both enter the gesture arena, 'Yellow' wins
                // because it is in front.
                behavior: _isYellowTranslucent
                    ? HitTestBehavior.translucent
                    : HitTestBehavior.opaque,
                onTap: () {
                  debugPrint('Yellow onTap');
                  setState(() {
                    _winner = _OnTapWinner.yellow;
                  });
                },
                child: Container(
                  alignment: Alignment.center,
                  decoration: BoxDecoration(
                    border:
                        _winner == _OnTapWinner.yellow ? highlightBorder : null,
                    color: Colors.amber,
                  ),
                  width: 200,
                  height: 200,
                  child: SizedBox(),
                ),
              ),
            ),
          ),
        ),
        Padding(
          padding: const EdgeInsets.all(8.0),
          child: Row(
            children: <Widget>[
              ElevatedButton(
                child: const Text('Reset'),
                onPressed: () {
                  setState(() {
                    _isYellowTranslucent = false;
                    _winner = _OnTapWinner.none;
                  });
                },
              ),
              const SizedBox(width: 8),
              ElevatedButton(
                child: Text(
                  'Set Yellow behavior to ${_isYellowTranslucent ? 'opaque' : 'translucent'}',
                ),
                onPressed: () {
                  setState(() => _isYellowTranslucent = !_isYellowTranslucent);
                },
              ),
            ],
          ),
        ),
      ],
    );
  }

  @override
  void dispose() {
    debugPrintGestureArenaDiagnostics = false;
    super.dispose();
  }
}

 

  • 실행화면

 

GestureDetector하위의 초록색 컨테이너의 자식요소로 다시 GestureDetector로 감싸진 노란색 컨테이너가 놓여져 있다. 

두 위젯 모두 GestureDetector의 onTap이벤트가 적용되어 있는 상황에서 초록색 영역을 먼저 클릭해보자.

 

상위 GestureDetector가 적용되어 있는 초록색영역을 클릭했기 때문에, 상위 GestureDetector의 onTap 이벤트만 실행되었다. 그렇다면 GestureDetector의 영역이 중첩되어 있는 노란색 영역을 클릭하면 어떻게 될까?

 

 

클릭한 영역인 하위 GestureDetector의 onTap 이벤트만 실행되는 것을 확인할 수 있다.

영역이 겹치는 부분의 GestureDetector위젯의 이벤트를 발생시키면, 두 영역의 onTap이벤트가 모두 같은 arena로 들어가게 된다. 하지만 선택영역(클릭한 영역)에 가까운 하위 위젯부터 arena에 들어가서 활성화되기 때문에 하위 GestureDetector의 onTap이벤트만 실행되게 되는 것이다.

 

 

InkWell Widget 과의 차이

 

  • InkWell의 경우 onTap, onDoubleTap 등 제스쳐 기능을 제공하나 GestureDetector에 비해 종류가 빈약하다.
  • GestureDetector의 경우 InkWell과 비교하여 기본 애니메이션 효과를 제공하지 않는다.
  • 즉, InkWell에 비해 Gesture에 대해 복잡하거나 다양한 요구사항이 있을 때 GestureDetector를 사용하는 것이 좋다.