[Flutter]PageView와 InteratcionViewer를 동시에 사용할때 Zoom에 따른 제스쳐 우선권 이슈

임효진·2025년 5월 13일
0

Flutter

목록 보기
26/28

기존 구현 개요

사용 구성요소

PageView.builder를 사용해 이미지/비디오 미디어 리스트를 스와이프

이미지 뷰어: InteractiveViewer로 핀치 줌, 팬 기능 구현
비디오 뷰어: VideoPlayerController + GestureDetector로 재생/탭 제어
확대 여부를 감지하기 위해 onInteractionUpdate를 통해 isZoomed 상태 추적
isZoomed일 경우 NeverScrollableScrollPhysics로 PageView 스크롤을 제한하는 방식 사용

예상 동작

핀치 줌 시 스와이프가 차단되어야 함
줌 상태가 아닌 경우에는 자유롭게 좌우 넘김 가능해야 함

문제 : 줌 중 스와이프가 동작해버림

이미지가 아직 줌되고 있는 도중에 좌우 드래그를 시도하면, PageView가 먼저 인식하고 페이지가 넘어가버림

isZoomed 상태는 줌이 확실히 끝난 뒤에만 반응하므로 초기 핀치 시도에 대한 제어가 불가능

원인: Flutter GestureArena 구조

Flutter는 모든 제스처 인식기(GestureRecognizer) 가 하나의 Arena에 모여 "경쟁"하며, 더 일찍, 더 강력한 제스처가 이벤트를 가져간다:

제스처 종류

좌우 스와이프 HorizontalDragGestureRecognizer (PageView)
핀치 줌 ScaleGestureRecognizer (InteractiveViewer)

  • 기본적으로 PageView의 드래그 인식기가 강력하고 빠르게 동작함
  • InteractiveViewer는 핀치가 일정 정도 이상 진행되어야 인식
  • 따라서 핀치 시도 중에도 PageView가 제스처를 먼저 잡아버리는 현상 발생

기존 코드에서 시도된 해결 방법

복사
편집
physics: ctl.isZoomed.value
    ? const NeverScrollableScrollPhysics()
    : const BouncingScrollPhysics(),

한계

isZoomed는 ScaleUpdateDetails에서 scale > 1.0일 때만 true로 변경됨

핀치를 시작하기 전에 이미 PageView가 제스처를 가져가 버리면 스크롤 차단이 무의미

해결 전략: extended_image 도입

선택한 도구: extended_image
extended_image는 이미지 뷰잉에 특화된 고성능 위젯이며, 내부적으로 제스처 간 우선순위, 충돌, 동시 제스처 등 GestureArena의 한계를 우회하는 로직이 포함되어 있음

핵심 해결 포인트

  1. ExtendedImageGesturePageView
    PageView + 이미지 줌 + 팬 + 더블탭 확대/축소 통합

이미지 확대 중에는 자동으로 스크롤 차단됨

  1. GestureConfig.inPageView = true
    이 옵션 하나로 핀치 상태일 때 PageView의 drag 인식을 막음

내부적으로 GestureDetector를 제어하여 PageView가 arena에서 이기지 못하게 처리

  1. ExtendedImageMode.gesture
    확대/축소, 팬, 더블탭 줌을 통합적으로 제공

  2. 비디오는 기존 방식 유지
    FutureBuilder + VideoPlayer 조합을 유지하며, 전체 구조에 통합

리팩토링 예제 코드

변경 전 (PageView + InteractiveViewer)

PageView.builder(
  controller: ctl.pageController,
  physics: ctl.isZoomed.value
      ? const NeverScrollableScrollPhysics()
      : const BouncingScrollPhysics(),
  ...
)

변경 후 (ExtendedImageGesturePageView)

ExtendedImageGesturePageView.builder(
  controller: ctl.pageController,
  itemCount: ctl.mediaItems.length,
  onPageChanged: ctl.updateIndex,
  scrollDirection: Axis.horizontal,
  physics: const BouncingScrollPhysics(),
  itemBuilder: (ctx, idx) {
    final item = ctl.mediaItems[idx];

    if (item.type == MediaType.IMAGE) {
      return ExtendedImage.network(
        item.url,
        fit: BoxFit.contain,
        mode: ExtendedImageMode.gesture,
        enableLoadState: true,
        initGestureConfigHandler: (_) => GestureConfig(
          minScale: 1.0,
          maxScale: 4.0,
          initialScale: 1.0,
          animationMinScale: 0.7,
          animationMaxScale: 4.0,
          inPageView: true, // 핵심
          cacheGesture: false,
        ),
      );
    }

    return FutureBuilder<VideoPlayerController>(
      future: ctl.futureAt(idx),
      builder: (_, snap) {
        if (!snap.hasData) return const OringLoadingIndicator().center();
        final vc = snap.data!;
        final screenWidth = MediaQuery.of(ctx).size.width;
        final aspectRatio = vc.value.size.width / vc.value.size.height;
        final height = screenWidth / aspectRatio;

        return Stack(
          children: [
            Center(
              child: VideoPlayer(vc)
                  .constrained(width: screenWidth, height: height),
            ),
            Positioned.fill(
              child: GestureDetector(
                onTap: ctl.toggleControls,
                behavior: HitTestBehavior.translucent,
                child: Container(color: Colors.transparent),
              ),
            ),
          ],
        );
      },
    );
  },
)

왜 이 방식이 근본적인 해결인가?

기준 기존 방식 (InteractiveViewer) extended_image 방식
제스처 충돌 GestureArena에서 스와이프가 승리 GestureArena를 우회 또는 내부 제어
줌 중 페이지 넘김 방지 수동으로 physics 조절 → 지연됨 자동 제어 (즉시 적용)
더블탭 줌 직접 구현해야 함 기본 포함
팬 제스처 제한적 고급 팬 + 스프링 복구 포함
유지보수 제스처 복잡성 ↑ 명확한 역할 분리로 ↓

결론

기존 Flutter 구조에서는 PageView와 InteractiveViewer의 제스처 충돌이 근본적으로 해결되지 않음
extended_image의 ExtendedImageGesturePageView는 이러한 제스처 충돌 문제를 설계 차원에서 해결
특히 GestureConfig.inPageView = true 옵션은 줌 상태일 때 스크롤 차단이라는 핵심 기능을 제공
전체 구조는 유지하면서도, 이미지 줌 시 UX를 훨씬 자연스럽고 직관적으로 개선

profile
기냥 저냥 개발함

0개의 댓글