flutterdartflutter-pageview

Flutter make Perspective Page View infinite loop


I implemented a perspective_page_view like this. I updated the code to the new Flutter Version and everything is working fine.

The thing is that I would like that PageView to have an infinite loop.

I tried multiple different solutions I found on this question but non of them worked. They all messed up the perspective in my case.

I feel like this should be possible, but I can not come up with a solution. Happy for every help! Let me know if you need any more info!

Minimal Producable Example:

My Scaffold:

class ProjectsViewState extends State<ProjectsView>
    with TickerProviderStateMixin {
  late PageViewHolder holder;

  late PageController _controller;
  double fraction = 0.50;

  @override
  void initState() {
    super.initState();
    holder = PageViewHolder(value: 2.0);
    _controller = PageController(initialPage: 2, viewportFraction: fraction);
    _controller.addListener(() {
      holder.setValue(_controller.page);
    });
  }

  int currentPage = 2;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: ChangeNotifierProvider<PageViewHolder>.value(
          value: holder,
          child: Flexible(
            child: PageView.builder(
              controller: _controller,
              onPageChanged: (value) {
                setState(() {
                  currentPage = value;
                });
              },
              itemCount: projectsCounter,
              physics: const BouncingScrollPhysics(),
              itemBuilder: (context, index) {
                return ProjectsGalleryView(
                  number: index.toDouble(),
                  fraction: fraction,
                );
              },
            ),
          ),
        ),
      ),
    );
  }
}

and the pageView:

class ProjectsGalleryView extends StatefulWidget {
  final double number;
  final double fraction;

  const ProjectsGalleryView(
      {super.key, required this.number, required this.fraction});

  @override
  State<ProjectsGalleryView> createState() => _ProjectsGalleryViewState();
}

class _ProjectsGalleryViewState extends State<ProjectsGalleryView>
    with SingleTickerProviderStateMixin {
  @override
  Widget build(BuildContext context) {
    double value = Provider.of<PageViewHolder>(context).value;
    double diff = (widget.number - value);

    //Matrix for Elements
    final Matrix4 pvMatrix = Matrix4.identity()
      ..setEntry(3, 3, 1 / 0.8) // Increasing Scale by 80%
      // ..setEntry(1, 1, widget.fraction) // Changing Scale Along Y Axis
      ..setEntry(3, 0, 0.003 * -diff); // Changing Perspective Along X Axis

    return Transform(
      transform: pvMatrix,
      alignment: FractionalOffset.center,
      child: Column(
        children: <Widget>[
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 0),
            child: SizedBox(
              height: 200,
              width: 200,
              child: Container(
                decoration: BoxDecoration(
                  color: Colors.redAccent,
                  borderRadius: BorderRadius.circular(10),
                ),
              ),
            ),
          ),
          if (diff <= 1.0 && diff >= -1.0) ...[
            AnimatedOpacity(
              duration: const Duration(milliseconds: 100),
              opacity: 1 - diff.abs(),
              child: const Padding(
                padding: EdgeInsets.symmetric(
                  horizontal: 15,
                  vertical: 20,
                ),
                child: Text(
                  'A Text with animated opacity',
                ),
              ),
            ),
          ]
        ],
      ),
    );
  }

Solution

  • Well if you want the scroll to be smooth on the left side as well. You just have to set arbitrarily LARGE NUMBER as a starting point so that it feels INFINITE (large number like 9 trillion). However, it won't scroll indefinitely.

    1. set some large number (ex: const largeInt = 9223372036854;)
    2. modify your initState like below

    @override
    void initState() {
      super.initState();
      holder = PageViewHolder(value: largeInt.toDouble());
      _controller = PageController(
        initialPage: largeInt,
        viewportFraction: fraction,
      );
      _controller.addListener(() {
        holder.setValue(_controller.page);
      });
    }
    

    And the Full Code for the ProjectView

    const largeInt = 9223372036854;
    
    class ProjectsView extends StatefulWidget {
      const ProjectsView({super.key});
    
      @override
      ProjectsViewState createState() => ProjectsViewState();
    }
    
    class ProjectsViewState extends State<ProjectsView>
        with TickerProviderStateMixin {
      late PageViewHolder holder;
    
      late PageController _controller;
      double fraction = 0.50;
    
      @override
      void initState() {
        super.initState();
        holder = PageViewHolder(value: largeInt.toDouble());
        _controller = PageController(
          initialPage: largeInt,
          viewportFraction: fraction,
        );
        _controller.addListener(() {
          holder.setValue(_controller.page);
        });
      }
    
      int _currentPage = 2;
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: SafeArea(
            child: ChangeNotifierProvider<PageViewHolder>.value(
                value: holder,
                child: Column(
                  children: [
                    Flexible(
                      child: PageView.builder(
                        controller: _controller,
                        onPageChanged: (value) {
                          setState(() => _currentPage = value);
                        },
                        physics: const BouncingScrollPhysics(),
                        itemBuilder: (context, index) {
                          return ProjectsGalleryView(
                            number: index.toDouble(),
                            fraction: fraction,
                          );
                        },
                      ),
                    ),
                  ],
                )),
          ),
        );
      }
    }
    

    // Previous answer

    TL;DR It's not a perfect solution, but to scroll infinitely on the left side as well, add jumpToPage inside the _controller.addListener

    First, do not include itemCount in Pageview.builder or set it to null.

    However, Even though you set itemCount to null, it still ends when the index is 0.

    So, in the initState add following after initializing PageController.

    _controller.addListener(() {
      holder.setValue(_controller.page);
      final page = _controller.page;
      if (page == null) return;
      if (page < 2.01 && page > 1.99) {
        _controller.jumpToPage(10);
      }
    });
    

    by adding _controller.jumpToPage(10) when _controller.page is in betweem 1.99 and 2.01, _pageController will set the page to 10 again.

    The reason I added this in the _controller.addListener is that if you put that in onPageChanged, it will stutter real bad since the returned value is int.

    However, even though I used the double to minimize the impact, it will still kinda stutter a bit when scrolling really fast, and if its extremely fast, there is a chance that it will not work. However, this will always work for normal page by page scrolling.

    The full code for your ProjectView Widget.

    class ProjectsView extends StatefulWidget {
      const ProjectsView({super.key});
    
      @override
      ProjectsViewState createState() => ProjectsViewState();
    }
    
    class ProjectsViewState extends State<ProjectsView>
        with TickerProviderStateMixin {
      late PageViewHolder holder;
    
      late PageController _controller;
      double fraction = 0.50;
    
      @override
      void initState() {
        super.initState();
        holder = PageViewHolder(value: 2.0);
        _controller = PageController(initialPage: 2, viewportFraction: fraction);
        _controller.addListener(() {
          holder.setValue(_controller.page);
          final page = _controller.page;
          if (page == null) return;
          if (page < 2.01 && page > 1.99) {
            _controller.jumpToPage(10);
          }
        });
      }
    
      int currentPage = 2;
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: SafeArea(
            child: ChangeNotifierProvider<PageViewHolder>.value(
              value: holder,
              child: Flexible(
                child: PageView.builder(
                  controller: _controller,
                  onPageChanged: (value) {
                    setState(() => currentPage = value);
                  },
                  physics: const BouncingScrollPhysics(),
                  itemBuilder: (context, index) {
                    return ProjectsGalleryView(
                      number: index.toDouble(),
                      fraction: fraction,
                    );
                  },
                ),
              ),
            ),
          ),
        );
      }
    }