🚀

[Flutter] swiperをPageViewの入れ子にした時、スクロールできない問題

2023/01/24に公開

swiperで何

https://pub.dev/packages/flutter_swiper
flutter_swiperはflutterに一番使われたswiper plugin,ただ作者が全然更新しないので、今回他の人がメンテしてくれるcard_swiperを使う、使い方は同じです。
https://pub.dev/packages/card_swiper

発生する問題

Flutterでは、PageViewがSwiperにネストされている場合、デフォルトでは問題なく動作し、内側のSwiperが先にスクロールイベントを消費する。
ただし、Swiperのレイアウトをカスタム:SwiperLayout.CUSTOMに設定した場合、Swiperはスクロールイベントを消費できない。

原因:

この問題は、PageViewとSwiperのスクロールイベントの競合により発生します。

解決策

解決策は二つある。

  • もしインタラクションが許容されるなら、最も簡単な方法は、外側のPageViewのスクロールを単に無効にすることです。PageViewのphysicsをNeverScrollableScrollPhysicsに変更するだけです。
    SwiperをListenerでラップして、onPointerDown、onPointerUpを監視して、タップされたタイミングでPageViewのphysicsをNeverScrollableScrollPhysicsに変更、指が離せたらPageViewのphysicsを戻す。

    ここではRiverpodつかている、他の状態管理でも同じことすれば、実現できるはずです。StreamBuilderもできる。
 Consumer(builder: (context, ref, _) {
          return Listener(
            onPointerDown: (_) {
              ref.watch(onTapProvider.notifier).update((state) => true);
            },
            onPointerUp: (_) {
              ref.watch(onTapProvider.notifier).update((state) => false);
            },
            child: Swiper(
              layout: SwiperLayout.CUSTOM,
              customLayoutOption:
                  CustomLayoutOption(startIndex: -1, stateCount: 3)
                    ..addRotate([-45.0 / 180, 0.0, 45.0 / 180])
                    ..addTranslate(const [
                      Offset(-370.0, -40.0),
                      Offset(0.0, 0.0),
                      Offset(370.0, -40.0)
                    ]),
              itemWidth: 300.0,
              itemHeight: 200.0,
              outer: false,
              itemBuilder: (context, index) {
                return Image.asset(
                  images[index],
                  fit: BoxFit.fill,
                );
              },
              itemCount: images.length,
            ),
          );
        }),
  • もう一つの方法は、Swiperの内部コードを変更することです。 flutterのpluginのコードを変更するのは非常に簡単で、コードをコピーアウトして修正するだけです。
    具体的な変更箇所は、custom_layout.dart内の_buildAnimationメソッドで、以下のように変更する。
    onPanStart、onPanEnd、onPanUpdateを
    onHorizontalDragStart, onHorizontalDragEnd, onHorizontalDragUpdateにする。
    return GestureDetector(
      behavior: HitTestBehavior.opaque,
      onPanStart: _onPanStart,
      onPanEnd: _onPanEnd,
      onPanUpdate: _onPanUpdate,
      child: ClipRect(
        child: Center(
          child: _buildContainer(list),
        ),
      ),
    );

return GestureDetector(
      behavior: HitTestBehavior.opaque,
      onHorizontalDragStart: _onPanStart,
      onHorizontalDragEnd: _onPanEnd,
      onHorizontalDragUpdate: _onPanUpdate,
      child: ClipRect(
        child: Center(
          child: _buildContainer(list),
        ),
      ),
    );

この問題が発生する原因はonHorizontalDragxxxの優先順位 > onPanxxx、これによりPageViewのonHorizontalDragxxxが先に呼ばれる、SwiperのonPanxxxが呼ばれなくなる。
タッチイベントの詳細はこちらに参考
https://zenn.dev/fastriver/articles/cb8f5a2a019715#3.闘技場の閉場

参考code:
https://github.com/paigupai/swiper_scroll_conflict

Discussion