Centering the last row of items in a flutter gridview

2,835

Solution 1

There are two answers considering flutter_staggered_grid_view. Although it is a nice package, I think it does much more than you want.

I am providing a more manual, custom implementation.

import 'dart:math';

import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';

/// A [SliverGridLayout] that provide a way to customize the children geometry.
class SliverGridWithCustomGeometryLayout extends SliverGridRegularTileLayout {
  /// The builder for each child geometry.
  final SliverGridGeometry Function(
    int index,
    SliverGridRegularTileLayout layout,
  ) geometryBuilder;

  SliverGridWithCustomGeometryLayout({
    @required this.geometryBuilder,
    @required int crossAxisCount,
    @required double mainAxisStride,
    @required double crossAxisStride,
    @required double childMainAxisExtent,
    @required double childCrossAxisExtent,
    @required bool reverseCrossAxis,
  })  : assert(geometryBuilder != null),
        assert(crossAxisCount != null && crossAxisCount > 0),
        assert(mainAxisStride != null && mainAxisStride >= 0),
        assert(crossAxisStride != null && crossAxisStride >= 0),
        assert(childMainAxisExtent != null && childMainAxisExtent >= 0),
        assert(childCrossAxisExtent != null && childCrossAxisExtent >= 0),
        assert(reverseCrossAxis != null),
        super(
          crossAxisCount: crossAxisCount,
          mainAxisStride: mainAxisStride,
          crossAxisStride: crossAxisStride,
          childMainAxisExtent: childMainAxisExtent,
          childCrossAxisExtent: childCrossAxisExtent,
          reverseCrossAxis: reverseCrossAxis,
        );

  @override
  SliverGridGeometry getGeometryForChildIndex(int index) {
    return geometryBuilder(index, this);
  }
}

/// Creates grid layouts with a fixed number of tiles in the cross axis, such
/// that fhe last element, if the grid item count is odd, is centralized.
class SliverGridDelegateWithFixedCrossAxisCountAndCentralizedLastElement
    extends SliverGridDelegateWithFixedCrossAxisCount {
  /// The total number of itens in the layout.
  final int itemCount;

  SliverGridDelegateWithFixedCrossAxisCountAndCentralizedLastElement({
    @required this.itemCount,
    @required int crossAxisCount,
    double mainAxisSpacing = 0.0,
    double crossAxisSpacing = 0.0,
    double childAspectRatio = 1.0,
  })  : assert(itemCount != null && itemCount > 0),
        assert(crossAxisCount != null && crossAxisCount > 0),
        assert(mainAxisSpacing != null && mainAxisSpacing >= 0),
        assert(crossAxisSpacing != null && crossAxisSpacing >= 0),
        assert(childAspectRatio != null && childAspectRatio > 0),
        super(
          crossAxisCount: crossAxisCount,
          mainAxisSpacing: mainAxisSpacing,
          crossAxisSpacing: crossAxisSpacing,
          childAspectRatio: childAspectRatio,
        );

  bool _debugAssertIsValid() {
    assert(crossAxisCount > 0);
    assert(mainAxisSpacing >= 0.0);
    assert(crossAxisSpacing >= 0.0);
    assert(childAspectRatio > 0.0);
    return true;
  }

  @override
  SliverGridLayout getLayout(SliverConstraints constraints) {
    assert(_debugAssertIsValid());
    final usableCrossAxisExtent = max(
      0.0,
      constraints.crossAxisExtent - crossAxisSpacing * (crossAxisCount - 1),
    );
    final childCrossAxisExtent = usableCrossAxisExtent / crossAxisCount;
    final childMainAxisExtent = childCrossAxisExtent / childAspectRatio;
    return SliverGridWithCustomGeometryLayout(
      geometryBuilder: (index, layout) {
        return SliverGridGeometry(
          scrollOffset: (index ~/ crossAxisCount) * layout.mainAxisStride,
          crossAxisOffset: itemCount.isOdd && index == itemCount - 1
              ? layout.crossAxisStride / 2
              : _getOffsetFromStartInCrossAxis(index, layout),
          mainAxisExtent: childMainAxisExtent,
          crossAxisExtent: childCrossAxisExtent,
        );
      },
      crossAxisCount: crossAxisCount,
      mainAxisStride: childMainAxisExtent + mainAxisSpacing,
      crossAxisStride: childCrossAxisExtent + crossAxisSpacing,
      childMainAxisExtent: childMainAxisExtent,
      childCrossAxisExtent: childCrossAxisExtent,
      reverseCrossAxis: axisDirectionIsReversed(constraints.crossAxisDirection),
    );
  }

  double _getOffsetFromStartInCrossAxis(
    int index,
    SliverGridRegularTileLayout layout,
  ) {
    final crossAxisStart = (index % crossAxisCount) * layout.crossAxisStride;

    if (layout.reverseCrossAxis) {
      return crossAxisCount * layout.crossAxisStride -
          crossAxisStart -
          layout.childCrossAxisExtent -
          (layout.crossAxisStride - layout.childCrossAxisExtent);
    }
    return crossAxisStart;
  }
}

Explanation

Explaining a little, what this code is doing is that it is a custom implementation of SliverGridLayout, which is the type of class that tells Flutter how to position children on a grid layout.

The more important method here is getGeometryForChildIndex. This is the method that says where each child should be positioned, based on its index. Note, however, that we are exposing this method through a parameter to the next class I'll be talking about.

The next class we implement is SliverGridDelegate. We have to make a custom implementation to use SliverGridWithCustomGeometryLayout, as there's no other way to make a delegate use a specific SliverGridLayout. Here we used our parameter geometryBuilder to delegate to it the role of returning a SliverGridGeometry.

This implementation of geometryBuilder is where all the magic happens. It is basically a copy of the original SliverGridRegularTileLayout method, but with one change. We check if the index of the element is even and if it is also the last one. If both checks pass, we return a centered position. Else, we return the position it would have anyway.

To use our solution, just pass it to the gridDelegate parameter on a GridView. Example:

GridView.builder(
    itemCount: 9,
    itemBuilder: (_, __) =>
        Container(width: 100, height: 100, color: Colors.red),
    gridDelegate:
        SliverGridDelegateWithFixedCrossAxisCountAndCentralizedLastElement(
            itemCount: checklist.sections.length(),
            crossAxisCount: 2,
            childAspectRatio: 0.825,
        ),
)

Disclaimer

This solution was not exhaustively tested, and I posted it here only to one have the idea about how to make it. However, I am using it in production code. If I eventually find any problem with it, I'll edit here.

Solution 2

How about you try below things?

  1. Generate GridView with 2n item.
  2. If there is a remain item, add a widget for last one item.
Share:
2,835
mindfullsilence
Author by

mindfullsilence

Updated on December 18, 2022

Comments

  • mindfullsilence
    mindfullsilence over 1 year

    I have a dynamic list of items I am outputting into a GridView.count constructor with a mainAxisCount of 2 (2 column grid). If the list length is odd, the last row will only contain a single item. I want this single item to be centered on the screen, rather than being aligned with the first column. Can this be done?

  • Phan Sinh
    Phan Sinh about 3 years
    Did you implement the collapse the column in the specific index? @Mateus Felipe I did implement flutter_staggered_grid_view, but the gridview did not rebuild the column width when the container has resize. :((
  • Mateus Felipe
    Mateus Felipe about 3 years
    Yes, this example is very fixed, but it shouldn't be hard to generalize.