17

I want to render cards with a horizontal paged scroll and be able to see the borders of the previous and next card every time one is visible. The flutter PageView widget produces almost the result I want, but it doesn't show the pages aligned the way I want, this is my code

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'PageView Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'PageView Alignment'),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(title)),
      body: PageView.builder(
        itemCount: 5,
        itemBuilder: (context, i) => Container(
              color: Colors.blue,
              margin: const EdgeInsets.only(right: 10),
              child: Center(child: Text("Page $i")),
            ),
        controller: PageController(viewportFraction: .7),
      ),
    );
  }
}

this is the result the above code produces enter image description here

I want the PageView to be aligned to the left of the screen, or at least that first page, i.e to remove that blank space at the left of Page 0. I s there any PageView parameter I'm missing? Or does some other component exists that produces the result I'm looking for?

Rolando Urquiza
  • 1,404
  • 3
  • 17
  • 29

3 Answers3

33

Setting the PageView's padEnds property to false should do the trick .

...the property probably didn't exist in the past.

Zamorite
  • 686
  • 9
  • 13
30

After making a deeper analysis on my own needs and checking the source code for the PageView widget, I realized that that I needed a scrolling widget that works in a item by item basis, but at the same time I needed that the space given to every item was the same as a normal scroll, so I needed to change the ScrollPhysics of a normal scroller. In found this post which describes scroll physics in flutter at some extent and was close to my needs, the difference was I needed to add space at bith sides of the current visible widget, not only to the right.

So I took the CustomScrollPhysics in the post and modified it in this way (the changed parts from the post code are sourrounded withh <-- and --> comments:

class CustomScrollPhysics extends ScrollPhysics {
  final double itemDimension;

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

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

  double _getPage(ScrollMetrics position, double portion) {
    // <--
    return (position.pixels + portion) / itemDimension;
    // -->
  }

  double _getPixels(double page, double portion) {
    // <--
    return (page * itemDimension) - portion;
    // -->
  }

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

  @override
  Simulation? createBallisticSimulation(
      ScrollMetrics position, double velocity) {
    // If we're out of range and not headed back in range, defer to the parent
    // ballistics, which should put us back in range at a page boundary.
    if ((velocity <= 0.0 && position.pixels <= position.minScrollExtent) ||
        (velocity >= 0.0 && position.pixels >= position.maxScrollExtent)) {
      return super.createBallisticSimulation(position, velocity);
    }

    final Tolerance tolerance = this.tolerance;
    // <--
    final portion = (position.extentInside - itemDimension) / 2;
    final double target =
        _getTargetPixels(position, tolerance, velocity, portion);
    // -->
    if (target != position.pixels) {
      return ScrollSpringSimulation(spring, position.pixels, target, velocity,
          tolerance: tolerance);
    }
    return null;
  }

  @override
  bool get allowImplicitScrolling => false;
}

In summary, what I did is to take half of the extra space left by the current visible widget (i.e (position.extentInside - itemDimension) / 2) and add it to the page calculation based on the scroll position, allowing the widget to be smaller that the visible scroll size but considering the whole extent as a single page, and subtract it to the scroll pixels calculation based on the page, preventing a "page" to be placed past or before the half visible part of the widgets at their sides.

The other change is that itemDimension is not the scroll extent divided by the element amount, I needed this value to be the size of each widget in the scroll direction.

This is what I end up with:

final result

Of course, this implementation has some limitations:

  • The size of each element in the scroll direction must be fixed, if a single element has a different size, then the whole scroll behaves erratically
  • The size must include the padding in case there is some, otherwise it will have the same effect that having widgets of different sizes

I didn't focus on solving this limitations and having a more complete widget because this limitations are ensured in the case I need this widget for. Here is the complete code of the above example.

https://gist.github.com/rolurq/5db4c0cb7db66cf8f5a59396faeec7fa

Andrey Gordeev
  • 30,606
  • 13
  • 135
  • 162
Rolando Urquiza
  • 1,404
  • 3
  • 17
  • 29
  • 2
    UPVOTE THIS DUDE. Verry nice sollution. I would like to see this be implemented in the PageView – vanlooverenkoen Oct 03 '19 at 12:11
  • Thanks @KoenVanLooveren !! I agree with you, this should be implemented in PageView, I don't know why it isn't, I have seen this behavior in many apps – Rolando Urquiza Oct 03 '19 at 14:22
  • Indeed maybe you could try to implement this in the pageview itself and merge it to the main branch? – vanlooverenkoen Oct 03 '19 at 14:27
  • You're right, and I will, I should clean it just a bit and work in the limitations. Also, I think that changing the sliver used when `viewportFraction` is set can work without having to deal with the `ScrollPhysics` – Rolando Urquiza Oct 03 '19 at 19:17
  • That is what I tried before. I could not figure it out. The problem with the scrollphysics for me was navigating to a specific page. This is possible with the PageViewController – vanlooverenkoen Oct 04 '19 at 06:44
  • The snippet code is not valid: `itemExtent` is not defined. Had to use code from the gist. – Andrey Gordeev Dec 06 '19 at 13:30
  • @AndreyGordeev sorry, it was a naming error, `itemExtent` should be `itemDimension` I edited the code snippet, thanks for noticing. – Rolando Urquiza Dec 06 '19 at 16:18
  • 1
    I would rename CustomScrollPhysics to SnapScrollPhysics or something like that. – Juliano Jan 07 '20 at 21:39
  • 1
    On iOS devices, where the default physics is BouncingScrollPhysics, the condition of returning `super.createBallisticSimulation(position, velocity)` should contain `position.outOfRange`, or the method `applyBoundaryConditions` should be overridden to avoid unexpected bug caused by the iOS bouncing effect. – ZhangLei Jul 03 '20 at 02:45
0

Flutter Added New Property padEnds Set it to False

  • Plagiarism of above answer https://stackoverflow.com/a/69151759/4575350 – STA Sep 21 '21 at 05:26
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-ask). – Community Sep 21 '21 at 06:35