3

How to update the PageView to trigger onPageChange only on specific conditions?

Here, I don't want to change the current page if the user is still touching the screen. Apart from that, everything should remain the same (ballistic scroll simulation, page limits)

It seems it has to deal with the ScrollPhysics object attached to PageView, but I don't know how to correctly extends it.

Let me know if you need some code, but the question is very general and can refer to any PageView, so you should not need any context.

Minimum Reproductible Example

Here is the translation in dart of the text above. Feel free to update this code to make it achieve the objective.

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

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  static const String _title = 'Flutter Code Sample';

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(title: _title, home: MyPageView());
  }
}

class MyPageView extends StatefulWidget {
  const MyPageView({Key? key}) : super(key: key);

  @override
  State<MyPageView> createState() => _MyPageViewState();
}

class _MyPageViewState extends State<MyPageView> {
  @override
  Widget build(BuildContext context) {
    final PageController controller = PageController();
    return Scaffold(
        body: SafeArea(
            child: PageView.builder(
      onPageChanged: (int index) {
        // TODO: Don't trigger this function if you still touch the screen
        print('onPageChanged index $index, ${controller.page}');
      },
      allowImplicitScrolling: false,
      controller: controller,
      itemBuilder: (BuildContext context, int index) {
        print('Build Sliver');
        return Center(
          child: Text('Page $index'),
        );
      },
    )));
  }
}

Example of a (bad) solution

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  static const String _title = 'Flutter Code Sample';

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(title: _title, home: MyPageView());
  }
}

class MyPageView extends StatefulWidget {
  const MyPageView({Key? key}) : super(key: key);

  @override
  State<MyPageView> createState() => _MyPageViewState();
}

class _MyPageViewState extends State<MyPageView> {
  @override
  Widget build(BuildContext context) {
    final PageController controller = PageController();
    return Scaffold(
        body: SafeArea(
      child: Listener(
        onPointerUp: (PointerUpEvent event) {
          if (controller.page == null) {
            return;
          }

          if (controller.page! > 0.5) {
            //TODO: update the time so it fits the end of the animation
            Future.delayed(const Duration(milliseconds: 700), () {
              print('Do your custom action onPageChange action here');
            });
          }
        },
        child: PageView.builder(
          controller: controller,
          itemBuilder: (BuildContext context, int index) {
            print('Build Sliver');
            return Center(
              child: Text('Page $index'),
            );
          },
        ),
      ),
    ));
  }
}

This solution triggers an action on the next page, 700ms after the user stops touching the screen.

It does work, but it is a lousy work.

  1. How to account for different screen sizes? 700ms is the maximum amount of time to animate between 2 pages on an iPhone SE.
  2. How to adjust this arbitrary number (700), so it varies according to controller.page (the closer to the next page, the smaller you have to wait).
  3. It doesn't use onHorizontalDragEnd or a similar drag detector, which can result in unwanted behaviour.
Adrien Kaczmarek
  • 530
  • 4
  • 13

3 Answers3

2

You should disable the scrolling entirely on PageView with physics: NeverScrollableScrollPhysics() and detect the scroll left and right on your own with GestureDetector. The GestureDetector.onHorizontalDragEnd will tell which direction the user dragged, to the left or to the right, checking the parameter's DragEndDetails property primaryVelocity. If the value is negative the user dragged to the right and is positive if the user dragged to the left.

To change the page manually just use the PageController methods nextPage and previousPage.

Take a look at the screenshot below and the live demo on DartPad.

Screenshot

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

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
      debugShowCheckedModeBanner: false,
      scrollBehavior: MyCustomScrollBehavior(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  late PageController _pageController;

  @override
  void initState() {
    super.initState();
    _pageController = PageController();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: GestureDetector(
        onHorizontalDragEnd: (details) => (details.primaryVelocity ?? 0) < 0
            ? _pageController.nextPage(
                duration: const Duration(seconds: 1), curve: Curves.easeInOut)
            : _pageController.previousPage(
                duration: const Duration(seconds: 1), curve: Curves.easeInOut),
        child: PageView(
          physics: const NeverScrollableScrollPhysics(),
          controller: _pageController,
          children: [
            Container(
              color: const Color.fromARGB(255, 0, 91, 187),
            ),
            Container(
              color: const Color.fromARGB(255, 255, 213, 0),
            ),
          ],
        ),
      ),
    );
  }
}

class MyCustomScrollBehavior extends MaterialScrollBehavior {
  @override
  Set<PointerDeviceKind> get dragDevices => {
        PointerDeviceKind.touch,
        PointerDeviceKind.mouse,
      };
}
lepsch
  • 8,927
  • 5
  • 24
  • 44
  • Thank you for this solution. Unfortunately, it doesn't fit the constraints (I already had a similar solution). With this solution, you break all the pre-existing behaviour (no more natural swipe when you touch the screen, nor any effects when you reach the end of the scrollable content). I need a solution that feels the same as `PageScrollPhysics`. However, the current page shouldn't change if you still touch the screen (so if you didn't release the *ballistic effect* yet) – Adrien Kaczmarek Jul 18 '22 at 13:39
0

You can simply use physics: NeverScrollableScrollPhysics() inside PageView() to achieve this kind of behaviour

0

I struggled with the same solution and built a complex custom gesture controller with drag listeners.

However, your so called bad example seems like the right direction.\

  • Why have this 700ms at all?\
  • You already have the onPointerUp event, where you can check the current page by using controller.page.round().\
  • You can also check that there is a dragging going on at this pointerUp by comparing controller.page==controller.page.floor()
ΞΫΛL
  • 184
  • 7