GestureDetector와 InteractiveViewer를 중첩하여 제어하기

2025. 7. 7. 17:28Programming/Flutter

반응형

이전에 GestureDetector()을 사용해서 제스쳐를 통해 Widget을 제어하는 기능을 구현했었다. 간단하게 PseudoCode를 작성하면 다음과 같은 형식이다.

GestureDetector(
    onHorizontalDragStart: (DragStartDetails details) {
        viewModel
                .updateDragStartPoint(details.globalPosition.dx);
    },
    onHorizontalDragEnd: (DragEndDetails details) {
        viewModel.initializeDragStartPoint();
    },
    onHorizontalDragUpdate: (DragUpdateDetails details) {
        viewModel.flipByGesture(
            details.globalPosition.dx,
        );
    },
    child: AspectRatio(
        aspectRatio: 16 / 9,
        child: ImageWidget( // 이미지를 표시하는 위젯
            color: Colors.black,
        ),
    ),
);

코드를 살펴보면 AspectRatio() 위젯 위에 GestureDetector() 위젯을 중첩하여 사용자가 수평방향으로 드래그 제스쳐를 취할 때 콜백을 등록하고 있다. 수평방향으로 드래그를 시작할 때 좌표를 저장하고, 드래그하는 동안 좌표를 업데이트한다. 그리고 드래그가 끝나면 시작 좌표를 초기화하는 식이다. 이런 구현을 통해서 ImageWidget()의 이미지를 수평방향 드래그로 변경하는 캐로셀같은 위젯을 구현할 수 있다.

문제는 그 다음이었는데, 다른게 아니라 ImageWidget()에 표시되는 이미지를 확대하고 축소하는 기능을 추가하고싶었던 것이다. 다행히 Flutter에서는 InteractiveViewer() 위젯을 제공하는데, 이 위젯을 사용하면 간단하게 child 요소를 확대/축소하는 기능을 구현할 수 있다. PseudoCode로 작성한 코드를 업데이트해보자.

GestureDetector(
    onHorizontalDragStart: (DragStartDetails details) {
        viewModel
                .updateDragStartPoint(details.globalPosition.dx);
    },
    onHorizontalDragEnd: (DragEndDetails details) {
        viewModel.initializeDragStartPoint();
    },
    onHorizontalDragUpdate: (DragUpdateDetails details) {
        viewModel.flipByGesture(
            details.globalPosition.dx,
        );
    },
    child: InteractiveViewer(
        transformationController: viewModel.transformationController,
        maxScale: 2.0,
        minScale: 1.0,
        child: AspectRatio(
            aspectRatio: 16 / 9,
            child: ImageWidget( // 이미지를 표시하는 위젯
                color: Colors.black,
            ),
        ),
    ),
);

이것으로 간단하게 ImageWidget()을 줌인/줌아웃 할 수 있는 기능을 구현할 수 있다. 하지만 문제는 이렇게 구현하면 두 위젯을 중첩하여 사용하는 것이기 때문에 두 위젯의 제스쳐가 충돌하는 문제가 발생한다. 예를 들어, 사용자가 ImageWidget()을 확대하고 있는 상태에서 수평방향으로 드래그를 시작하면 GestureDetector() 위젯이 드래그 제스쳐를 가로채서 확대댄 이미지를 이동하는게 아니라, 이미지를 변경하는 onHorizontalDrag 콜백을 실행한다. 이미지가 확대된 상태에서 onHorizontalDrag에 의해 다른 이미지로 변경되는 것이다.

기본적으로 InteractiveViewer()는 자식 위젯이 확대된 상태에서 드래그를 하게되면 확대된 위젯이 표시되는 좌표를 변경하게된다. 그렇다고 모든 제스쳐를 GestureDetector()가 가져가는 것도 아니라서, 두 손가락으로 드래그하면 이미지가 변경되는 게 아니라 확대된 위젯이 표시되는 좌표를 변경해버린다. 이도저도 아닌 상황이다. 확대된 상태에서 드래그를 했을 때 InteractiveViewer()가 제스쳐 동작을 가져가게 하려면, 즉 Javascript의 캡쳐링처럼 이벤트를 전파시키려면 어떻게 해야할까?

Claude에게 물어보니 AbsorbPointer() 위젯을 사용하는 방안이나 GestureDetector()에서 조건부로 return을 하라는 답변이 와서 적용해봤지만, AbsorbPointer()는 자식 위젯이 제스쳐를 가져가는 것을 막는 위젯이라서 사용할 수 없었고, GestureDetector()에서 조건부로 return을 할 경우 자식 위젯으로 이벤트가 전파되지 않아서 딱히 쓸모가 없었다.

최종적으로 코드를 업데이트하면 다음과 같다.

GestureDetector(
    onHorizontalDragStart: viewModel.currentScale == 1.0 ? (DragStartDetails details) {
        viewModel
                .updateDragStartPoint(details.globalPosition.dx);
    } : null,
    onHorizontalDragEnd: viewModel.currentScale == 1.0 ? (DragEndDetails details) {
        viewModel.initializeDragStartPoint();
    } : null,
    onHorizontalDragUpdate: viewModel.currentScale == 1.0 ? (DragUpdateDetails details) {
        viewModel.flipByGesture(
            details.globalPosition.dx,
        );
    } : null,
    child: InteractiveViewer(
        transformationController: viewModel.transformationController,
        maxScale: 2.0,
        minScale: 1.0,
        child: AspectRatio(
            aspectRatio: 16 / 9,
            child: ImageWidget( // 이미지를 표시하는 위젯
                color: Colors.black,
            ),
        ),
    ),
);

viewModel 내에서 transformationControlleraddListener()로 등록해서 currentScale에 배율을 업데이트하고, 이 배율이 1.0일 때만 GestureDetector()에 등록한 콜백을 null로 할당해주는 것이다. 이러면 GestureDetector()에서 제스쳐 동작을 감지할 콜백 함수가 null이므로 이벤트를 자식 위젯으로 전달하게되고, 결과적으로는 InteractiveViewer()가 제스쳐 동작을 처리하게 된다.

코드는 삼항연산자를 사용해서 조금 지저분해진 감이 있지만, 아무튼 간단하게 원하는 기능을 구현할 수 있었다. 와ㅡ이! ' ㅇ')/

반응형