Search code examples
flutterstaggered-gridview

How to center a tile within a StaggeredGridView


I'm working with a StaggeredGridView within Flutter and I don't seem to see a way to center a tile if a tile is going to be the only tile in that "row".

For example, if I set the crossAxisCount of the StaggeredGridView to something like 6; and then sent the tile's crossAxisCellCount to 4, the tile occupies 4 "cells" starting from the left, leaving 2 "Cells" worth of empty space on the right if there isn't a tile that can occupy 2 cells.

Is it possible to force the tile to be centered? Essentially, making it occupy cells 2 - 5, leaving 1 empty cell on the left and 1 empty cell on the right?

I've tried wrapping the StaggeredGridView in a Center widget, but this didn't appear to make a difference.

Currently I have a stateful widget which has the following build method.

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<ProfileEditScreenViewModel>(
      create: (context) => model,
      child: Consumer<ProfileEditScreenViewModel>(
        builder: (context, model, child) => Scaffold(
          appBar: AppBar(
            centerTitle: true,
            title: Text(model.screenTitle),
          ),
          body: Center(
            child: StaggeredGridView.count(
              crossAxisCount: 6,
              shrinkWrap: true,
              mainAxisSpacing: 10.0,
              crossAxisSpacing: 10.0,
              staggeredTiles: model.displayCardTiles,
              children: model.displayCards,
            ),
          ),
        ),
      ),
    );
  }

In the View model for that widget there are two relevant functions that the StaggeredGridView uses: the displayCardTiles function that creates the StaggeredTiles, and the displayCards function which creates the widgets that go in the tiles. Those two functions are as follows:

List<StaggeredTile> _buildDisplayCardTiles(){
    List<StaggeredTile> myList = [];

    for(var bioCategory in userProfile.bioCategories!){
      myList.add(StaggeredTile.fit(bioCategory.crossAxisCellCount));
    }

    return myList;
  }

List<Widget> _buildDisplayCards(){
    List<Widget> myList = [];

    for(var bioCategory in userProfile.bioCategories!){
      myList.add(ProfileItemCard(bioCategory: bioCategory));
    }

    return myList;
  }

The "ProfileItemCard" is just a Card widget that displays a variety of Text widgets and checkboxes, but it's contents wouldn't impact the position of the Card within the StaggeredGridView.

I've also tried wrapping the ProfileItemCard in a Center widget, but it doesn't have any impact on how the Card is displayed.


Solution

  • Intro: I came into this myself, so I want to thank you for opening the question.

    Baseline: calculate the remainder of the model count and the cross axis count (userProfile.bioCategories and 6 in the original question), then place the "remaining" tiles in a row (spaced evenly) and span the row all over entire grid width.

    Demo

    problemsolution

    Repro: Github.

    Code (a single-file solution, the only prerequsite is flutter_staggered_grid_view: 0.4.0):

    import 'package:flutter/material.dart';
    import 'dart:math';
    
    import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
    
    void main() {
      runApp(const TabBarDemo());
    }
    
    generateRandomColor() => Color((Random().nextDouble() * 0xFFFFFF).toInt()).withOpacity(1.0);
    
    class Model {
      final num;
    
      Model({this.num});
    }
    
    class ModelWidget extends StatelessWidget {
      final Model model;
    
      ModelWidget(this.model) : super();
    
      @override
      Widget build(BuildContext context) {
        return AspectRatio(
          aspectRatio: 1,
          child: Container(
              color: generateRandomColor(),
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text(
                    model.num.toString(),
                    textAlign: TextAlign.center,
                  ),
                ],
              )),
        );
      }
    }
    
    class TabBarDemo extends StatelessWidget {
      const TabBarDemo({Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        // ------------------------------- given ----------------------------------
        const CROSS_AXIS_COUNT = 6;
        const CROSS_AXIS_SPACING = 10.0;
        const MAIN_AXIS_SPACING = 10.0;
        final models = List.generate(20, (_) => Model(num: Random().nextInt(100)));
        // ------------------------------------------------------------------------
        // ------------------------------ solution --------------------------------
        final modelCount = models.length;
        final int fittingCount = modelCount - modelCount % CROSS_AXIS_COUNT;
        // ------------------------------------------------------------------------
    
        return MaterialApp(
          home: DefaultTabController(
            length: 3,
            child: Scaffold(
              appBar: AppBar(
                bottom: const TabBar(
                  tabs: [
                    Tab(
                      child: Text("problem"),
                    ),
                    Tab(
                      child: Text("solution (static)"),
                    ),
                    Tab(icon: Text("solution (builder)")),
                  ],
                ),
                title: const Text('staggered_grid - centering tiles'),
              ),
              body: Padding(
                padding: const EdgeInsets.all(8.0),
                child: TabBarView(
                  children: [
                    // ------------------------------ problem ---------------------------------
                    Scaffold(
                      body: StaggeredGridView.count(
                        crossAxisCount: CROSS_AXIS_COUNT,
                        shrinkWrap: true,
                        mainAxisSpacing: MAIN_AXIS_SPACING,
                        crossAxisSpacing: CROSS_AXIS_SPACING,
                        staggeredTiles: models.map((m) => StaggeredTile.fit(1)).toList(),
                        children: models.map((m) => ModelWidget(m)).toList(),
                      ),
                    ),
                    // ------------------------------------------------------------------------
                    // ------------------------- solution (static) ----------------------------
                    Scaffold(
                      body: LayoutBuilder(builder: (context, constraints) {
                        return StaggeredGridView.count(
                          crossAxisCount: CROSS_AXIS_COUNT,
                          shrinkWrap: true,
                          mainAxisSpacing: MAIN_AXIS_SPACING,
                          crossAxisSpacing: CROSS_AXIS_SPACING,
                          staggeredTiles: [
                            ...models.sublist(0, fittingCount).map((m) => StaggeredTile.fit(1)).toList(),
                            if (modelCount != fittingCount) StaggeredTile.fit(CROSS_AXIS_COUNT)
                          ],
                          children: [
                            ...models.sublist(0, fittingCount).map((m) => ModelWidget(m)).toList(),
                            if (modelCount != fittingCount)
                              Row(
                                  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                                  children: models.sublist(fittingCount, modelCount).map((m) {
                                    return Container(
                                        width: constraints.maxWidth / CROSS_AXIS_COUNT - CROSS_AXIS_SPACING,
                                        child: ModelWidget(m));
                                  }).toList())
                          ],
                        );
                      }),
                    ),
                    // ------------------------------------------------------------------------
                    // ------------------------- solution (builder) ---------------------------
                    Scaffold(
                      body: LayoutBuilder(builder: (context, constraints) {
                        return StaggeredGridView.countBuilder(
                            crossAxisCount: CROSS_AXIS_COUNT,
                            shrinkWrap: true,
                            mainAxisSpacing: MAIN_AXIS_SPACING,
                            crossAxisSpacing: CROSS_AXIS_SPACING,
                            itemCount: modelCount == fittingCount ? fittingCount : fittingCount + 1,
                            itemBuilder: (context, index) {
                              if (index < fittingCount) {
                                return ModelWidget(models.elementAt(index));
                              } else {
                                return Row(
                                    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                                    children: models.sublist(fittingCount, modelCount).map((m) {
                                      return Container(
                                          width: constraints.maxWidth / CROSS_AXIS_COUNT - CROSS_AXIS_SPACING,
                                          child: ModelWidget(m));
                                    }).toList());
                              }
                            },
                            staggeredTileBuilder: (int index) {
                              if (index < fittingCount) {
                                return StaggeredTile.count(1, 1);
                              } else {
                                return StaggeredTile.count(CROSS_AXIS_COUNT, 1);
                              }
                            });
                      }),
                    ),
                    // ------------------------------------------------------------------------
                  ],
                ),
              ),
            ),
          ),
        );
      }
    }