Search code examples
flutterdartflutter-layoutflutter-sliver

Align a flutter PageView to the screen left


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?


Solution

  • 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