Original address: medium.com/flutter-com…

ScrollPhysics is very powerful and you can customize sliding effects by setting damping coefficients and so on.

KISS (keep it simple…. Don’t think too much, haha)

Cycling through a multi-page collection or slide collection is a common scenario.

The code for this effect is very simple, we just need to use the default PageView property.

import 'dart:math';

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  final List<int> pages = List.generate(4, (index) => index);
@override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: PageView.builder(
          itemCount: pages.length,
          itemBuilder: (context, index) {
            returnContainer(color: randomColor, margin: const EdgeInsets. All (20.0),); },),),); } Color get randomColor => Color((Random().nextDouble() * 0xFFFFFF).toint () << 0).withopacity (1.0); }Copy the code

Source code to GitHub

It’s really cool, but.

Sometimes we want to give the user a hint; Or the elements in our scrolling list aren’t really full pages. In this case, it would be nice if the current page filled only part of the view so that we could see the next element (or the last one).

Don’t worry, just use PageController in the Flutter.

class MyHomePage extends StatelessWidget {
  final List<int> pages = List.generate(4, (index) => index);
  final _pageController = PageController(viewportFraction: 0.8);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: PageView.builder(
          controller: _pageController,
          itemCount: pages.length,
          itemBuilder: (context, index) {
            returnContainer(color: randomColor, margin: const EdgeInsets. All (20.0),); },),),); }Copy the code

It’s really cool, but.

What if that’s not what we want? I want the elements to be like a whole list, not centered; But you want to scroll one element at a time.

To do this, we need to dig into a property we haven’t used yet: ScrollPhysics

Row vs PageView

PageView is designed more for users to slide through a set of pages, sort of like a slideshow. Our case is a little different because we want the effect of a list, but at the same time we want to scroll one element at a time. Ditching PageView in favor of ListView is more in line with the requirements.

@override
  Widget build(BuildContext context) {
    returnScaffold( body: SafeArea( child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: pages.length, itemBuilder: (context, index) => Container( height: double.infinity, width: 300, color: Margin: const EdgeInsets. All (20.0),),),); }Copy the code

Simple. But if you swipe to the right, you can’t swipe one element at a time. We’re now dealing with elements in a List, not pages. So we need to create our own concept of the page, and we can do that using the ListView physics property.

ScrollPhysics

There are different ScrollPhysics and things like that that you can use to control sliding effects; One of them looks really interesting, PageScrollPhysics. PageView uses PageScrollPhysics inside, but unfortunately, it doesn’t work in ListView. We can design one of our own, and let’s look at the implementation of PageScrollPhysics.

class PageScrollPhysics extends ScrollPhysics {
  /// Creates physics for a [PageView].
  const PageScrollPhysics({ ScrollPhysics parent }) : super(parent: parent);

  @override
  PageScrollPhysics applyTo(ScrollPhysics ancestor) {
    return PageScrollPhysics(parent: buildParent(ancestor));
  }

  double _getPage(ScrollPosition position) {
    if (position is _PagePosition)
      return position.page;
    return position.pixels / position.viewportDimension;
  }

  double _getPixels(ScrollPosition position, double page) {
    if (position is _PagePosition)
      return position.getPixelsFromPage(page);
    return page * position.viewportDimension;
  }

  double _getTargetPixels(ScrollPosition position, Tolerance tolerance, double velocity) {
    double page = _getPage(position);
    if(velocity < -tolerance. Velocity) Page -= 0.5;else if(Velocity > tolerance. Velocity) Page += 0.5;return _getPixels(position, page.roundToDouble());
  }

  @override
  Simulation createBallisticSimulation(ScrollMetrics position, double velocity) {
    // If we're out of range and not headed back in range, defer to the parent // ballistics, Pixels <= which should put us back in range at a page boundary Position. MinScrollExtent) | | (velocity > = 0.0 && position. The pixels > = position. MaxScrollExtent)) return super.createBallisticSimulation(position, velocity); final Tolerance tolerance = this.tolerance; final double target = _getTargetPixels(position, tolerance, velocity); if (target ! = position.pixels) return ScrollSpringSimulation(spring, position.pixels, target, velocity, tolerance: tolerance); return null; } @override bool get allowImplicitScrolling => false; }Copy the code

Source code to GitHub

Methods createBallisticSimulation is the entrance of a class, position and velocity as the input parameters of the scroll bar. First, this checks whether the user is scrolling right or left, and then calculates the new position in the scroll bar, which is the range to add or subtract the current view, because the scrolling in the page view is one after another.

What we’re going to do is very similar, but instead of using a viewport, we’re going to use a custom size, because each view has multiple elements.

This is a custom size we can calculate ourselves, which is the total size of the scroll bar divided by the number of elements in the list minus 1. Why do WE subtract 1? 1 element in the list can’t slide, 2 elements can slide 1 element… So N elements can slide N minus 1

CustomScrollPhysics

class CustomScrollPhysics extends ScrollPhysics {
  final double itemDimension;

  CustomScrollPhysics({this.itemDimension, ScrollPhysics parent})
      : super(parent: parent);

  @override
  CustomScrollPhysics applyTo(ScrollPhysics ancestor) {
    return CustomScrollPhysics(
        itemDimension: itemDimension, parent: buildParent(ancestor));
  }

  double _getPage(ScrollPosition position) {
    return position.pixels / itemDimension;
  }

  double _getPixels(double page) {
    return page * itemDimension;
  }

  double _getTargetPixels(
      ScrollPosition position, Tolerance tolerance, double velocity) {
    double page = _getPage(position);
    if (velocity < -tolerance.velocity) {
      page -= 0.5;
    } else if (velocity > tolerance.velocity) {
      page += 0.5;
    }
    return _getPixels(page.roundToDouble());
  }
@override
  Simulation createBallisticSimulation(
      ScrollMetrics position, double velocity) {
    // If we're out of range and not headed back in range, defer to the parent // ballistics, Pixels <= which should put us back in range at a page boundary Position. MinScrollExtent) | | (velocity > = 0.0 && position. The pixels > = position. MaxScrollExtent)) return super.createBallisticSimulation(position, velocity); final Tolerance tolerance = this.tolerance; final double target = _getTargetPixels(position, tolerance, velocity); if (target ! = position.pixels) return ScrollSpringSimulation(spring, position.pixels, target, velocity, tolerance: tolerance); return null; } @override bool get allowImplicitScrolling => false; }Copy the code

Source code to GitHub

We override getPixels() to return the location based on the page number, override getPage() to return the location based page number, and finally pass the itemDimension into the constructor.

Using CustomScrollPhysics

Luckily, the ScrollController can get the length of the scroll bar; However, you need to wait until the widget is created. We need to change our Page to StatefulWidget, listen for dimensions’ validity notification, and initialize CustomScrollPhysics

final _controller = ScrollController();

final List<int> pages = List.generate(4, (index) => index);

ScrollPhysics _physics;

@override
void initState() {
  super.initState();

  _controller.addListener(() {
    if (_controller.position.haveDimensions && _physics == null) {
      setState(() { var dimension = _controller.position.maxScrollExtent / (pages.length - 1); _physics = CustomScrollPhysics(itemDimension: dimension); }); }}); }Copy the code

At this point, we can slide the List one element at a time.

conclusion

Here’s a simple example of customizing ScrollPhysics to customize the sliding effect; In the example we let the ListView slide one element at a time. The complete code is as follows:

import 'dart:math';

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final _controller = ScrollController();

  final List<int> pages = List.generate(4, (index) => index);

  ScrollPhysics _physics;

  @override
  void initState() {
    super.initState();

    _controller.addListener(() {
      if (_controller.position.haveDimensions && _physics == null) {
        setState(() { var dimension = _controller.position.maxScrollExtent / (pages.length - 1); _physics = CustomScrollPhysics(itemDimension: dimension); }); }}); } @override Widget build(BuildContext context) {returnScaffold( body: SafeArea( child: ListView.builder( scrollDirection: Axis.horizontal, controller: _controller, physics: _physics, itemCount: pages.length, itemBuilder: (context, index) => Container( height: double.infinity, width: 300, color: randomColor, margin: const EdgeInsets. All (20.0),),),); } Color get randomColor => Color((Random().nextDouble() * 0xFFFFFF).toint () << 0).withopacity (1.0); } class CustomScrollPhysics extends ScrollPhysics { final double itemDimension; CustomScrollPhysics({this.itemDimension, ScrollPhysics parent}) : super(parent: parent); @override CustomScrollPhysics applyTo(ScrollPhysics ancestor) {return CustomScrollPhysics(
        itemDimension: itemDimension, parent: buildParent(ancestor));
  }

  double _getPage(ScrollPosition position) {
    return position.pixels / itemDimension;
  }
  
  double _getPixels(double page) {
    return page * itemDimension;
  }

  double _getTargetPixels(
      ScrollPosition position, Tolerance tolerance, double velocity) {
    double page = _getPage(position);
    if (velocity < -tolerance.velocity) {
      page -= 0.5;
    } else if (velocity > tolerance.velocity) {
      page += 0.5;
    }
    return _getPixels(page.roundToDouble());
  }

  @override
  Simulation createBallisticSimulation(
      ScrollMetrics position, double velocity) {
    // If we're out of range and not headed back in range, defer to the parent // ballistics, Pixels <= which should put us back in range at a page boundary Position. MinScrollExtent) | | (velocity > = 0.0 && position. The pixels > = position. MaxScrollExtent)) return super.createBallisticSimulation(position, velocity); final Tolerance tolerance = this.tolerance; final double target = _getTargetPixels(position, tolerance, velocity); if (target ! = position.pixels) return ScrollSpringSimulation(spring, position.pixels, target, velocity, tolerance: tolerance); return null; } @override bool get allowImplicitScrolling => false; }Copy the code

Source code to GitHub

That’s all for now, and I’m open for questions. Thank you for reading!