【Flutter】PageViewを使ってなるべく簡単に無限循環スクロールを模倣する

2024/02/04に公開

1. はじめに

無限に循環スクロールするWidgetが欲しくなりました。
indexed_list_viewを見つけたので、これで実現できるかもと思ったのですが、

しかし、以下のissueにもあるとおり、残念ながらFixedExtentScrollPhysicsPageScrollPhysicsなどのScrollPhysicsを使用できないようです。

https://github.com/marcglasberg/indexed_list_view/issues/36

スナップを効かせてスクロールするWidgetが中央に配置されるようにしたいため、ScrollPhysicsを拡張して試行錯誤してみたのですが、負のインデックスと正のインデックスの境界のスクロールを上手く制御できず諦めました...

そのため、もっと簡単に無限に循環するスクロールを模倣できないものかと考えました。

2. 挙動

循環の境界部分において、少し滑らかさを欠いているような気がします。ただまぁ模倣ですので許容範囲でしょうか。

3. コード

コードはこちら。

https://github.com/motucraft/infinite_page_view_scroll

これだけです(hooksをを使用しています)。

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';

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

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Infinite PageView Scroll',
      theme: ThemeData.dark(),
      home: const InfinitePageViewScroll(),
    );
  }
}

class InfinitePageViewScroll extends HookWidget {
  static final List<String> urls = [
    'https://p.npb.jp/img/common/logo/2024/logo_b_l.gif',
    'https://p.npb.jp/img/common/logo/2024/logo_m_l.gif',
    'https://p.npb.jp/img/common/logo/2024/logo_h_l.gif',
  ];

  const InfinitePageViewScroll({super.key});

  
  Widget build(BuildContext context) {
    final controller =
        usePageController(viewportFraction: 0.75, initialPage: 1);

    final itemCount = urls.length + 2;

    useEffect(() {
      controller.addListener(() {
        if (controller.page! > (urls.length + 0.999)) {
          controller.jumpToPage(1);
        } else if (controller.page! < 0.001) {
          controller.jumpToPage(itemCount - 2);
        }
      });
      return null;
    }, []);

    return Scaffold(
      appBar: AppBar(title: const Text('Infinite PageView Scroll')),
      body: PageView.builder(
        controller: controller,
        itemBuilder: (context, index) {
          if (index == 0) {
            return Image.network(urls.last);
          } else if (index == itemCount - 1) {
            return Image.network(urls.first);
          } else {
            return Image.network(urls[(index - 1) % urls.length]);
          }
        },
      ),
    );
  }
}

final itemCount = urls.length + 2; にて、前後にダミーの要素を配置するようにしました。
そして、ダミーの要素に近づいたらjumpToPageで実際のindexのページへジャンプさせています。

4. おわりに

当初はPageScrollPhysics_getTargetPixelsをカスタムすればいけそうだと思ったのですが、ScrollPhysicsをかなり深く理解しないと無理だなと思いました。
これに限らずですが、普段使っているSDKのWidgetやクラス群の中身をもっと理解しないとダメだなと思います。(でも難しいんだよなー)

GitHubで編集を提案

Discussion