5

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',
                ),
              ),
            ),
          ]
        ],
      ),
    );
  }
Chris
  • 1,828
  • 6
  • 40
  • 108

2 Answers2

1

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,
                );
              },
            ),
          ),
        ),
      ),
    );
  }
}
HW Kim
  • 444
  • 4
  • 13
  • appreciate the help, but like you said, not a perfect solution :D and its throwing errors for me in some cases as well.. For this project, the solution should be as smooth as possible – Chris Jan 16 '23 at 16:31
  • well, I modified my answer. If that is what you want, then there is no other way then starting the page with arbitrarily large number. – HW Kim Jan 17 '23 at 17:43
  • did you try the new answer? It is messing up the `transform` on init. But after scrolling the first time, it looks good... – Chris Jan 17 '23 at 18:24
  • Yes and I have added the full code for the `ProjectViewState` Widget. Your code had a problem, and it's not because of the `initState` with large number. It's because you have `Flexible` right inside the `SafeArea`. So I just wrapped `Flexible` with `Column` and everything is fine. – HW Kim Jan 17 '23 at 18:36
  • that was it. working as expected now :) – Chris Jan 18 '23 at 12:21
0

Isn't the accepted answer in the related question you linked also working?

simply not providing the itemCount: projectsCounter resulted in the following:

enter image description here

is this what you're after?

code
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import 'PageViewHolder.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Perspective PageView',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: ProjectsView(),
    );
  }
}

class ProjectsView extends StatefulWidget {
  const ProjectsView({super.key});

  @override
  State<ProjectsView> 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);
    });
  }

  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,
                );
              },
            ),
          ),
        ),
      ),
    );
  }
}

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',
                ),
              ),
            ),
          ]
        ],
      ),
    );
  }
}

Edit:

sorry, dont have enough karma to comment but: yes i can scroll infinitely (more then 3 times lol), i have rebuilt a new flutter app with newer versions and v2 embeddings, i can create and link you the repo if you want

HannesH
  • 932
  • 2
  • 12