letsar / flutter_staggered_grid_view

A Flutter staggered grid view
MIT License
3.12k stars 508 forks source link

How to center a sliver when the crossAxisExtent is greater than a given threshold? #281

Closed bizz84 closed 1 year ago

bizz84 commented 1 year ago

I'm trying to implement a grid layout using SliverAlignedGrid (great package btw!), and I've hit a wall trying to make it responsive.

Here's what I'm trying to do.

When the window width is < 900, I use the full width (minus some padding) and render my items like this:

CleanShot 2023-01-06 at 15 31 09@2x

But if the width is > 900, I want to center my content like so:

CleanShot 2023-01-06 at 15 32 00@2x

I previously accomplished this using a separate package. But since that doesn't support slivers, I'm trying to use SliverAlignedGrid instead.

So far, I have a CustomScrollView that contains a custom ProductsSliverAlignedGrid widget inside it:

class ProductsSliverAlignedGrid extends StatelessWidget {
  const ProductsSliverAlignedGrid({
    super.key,
    required this.itemCount,
    required this.itemBuilder,
  });

  /// Total number of items to display.
  final int itemCount;

  /// Function used to build a widget for a given index in the grid.
  final Widget Function(BuildContext, int) itemBuilder;

  @override
  Widget build(BuildContext context) {
    if (itemCount == 0) {
      return SliverToBoxAdapter(
        child: Center(
          child: Text(
            'No products found'.hardcoded,
            style: Theme.of(context).textTheme.headline4,
          ),
        ),
      );
    }
    // use a LayoutBuilder to determine the crossAxisCount
    return SliverLayoutBuilder(builder: (context, constraints) {
      final width = constraints.crossAxisExtent;
      // 1 column for width < 500px
      // then add one more column for each 250px
      final crossAxisCount = max(1, width ~/ 250);

      return SliverPadding(
        padding: const EdgeInsets.all(Sizes.p16),
        sliver: SliverAlignedGrid.count(
          crossAxisCount: crossAxisCount,
          mainAxisSpacing: Sizes.p24,
          crossAxisSpacing: Sizes.p24,
          itemBuilder: itemBuilder,
          itemCount: itemCount,
        ),
      );
    });
  }
}

This layout works but always uses the full width available.

In other parts of my code, I use a responsive widget that applies the "centered after threshold" effect to any regular widget:

/// Reusable widget for showing a child with a maximum content width constraint.
/// If available width is larger than the maximum width, the child will be
/// centered.
/// If available width is smaller than the maximum width, the child use all the
/// available width.
class ResponsiveCenter extends StatelessWidget {
  const ResponsiveCenter({
    super.key,
    this.maxContentWidth = Breakpoint.desktop,
    this.padding = EdgeInsets.zero,
    required this.child,
  });
  final double maxContentWidth;
  final EdgeInsetsGeometry padding;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    // Use Center as it has *unconstrained* width (loose constraints)
    return Center(
      // together with SizedBox to specify the max width (tight constraints)
      // See this thread for more info:
      // https://twitter.com/biz84/status/1445400059894542337
      child: SizedBox(
        width: maxContentWidth,
        child: Padding(
          padding: padding,
          child: child,
        ),
      ),
    );
  }
}

But this does not work with slivers and gives this error:

FlutterError (A RenderPadding expected a child of type RenderBox but received a child of type _RenderSliverLayoutBuilder.
RenderObjects expect specific types of children because they coordinate with their children during layout and paint. For example, a RenderSliver cannot be the child of a RenderBox because a RenderSliver does not understand the RenderBox layout protocol.

The RenderPadding that expected a RenderBox child was created by:
  Padding ← SizedBox ← Center ← ResponsiveCenter ← SliverToBoxAdapter ← ResponsiveSliverCenter ← Viewport ← IgnorePointer-[GlobalKey#153ca] ← Semantics ← Listener ← _GestureSemantics ← RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#6122e] ← ⋯

So the last missing piece of the puzzle is: how do I make the content inside my sliver centered if the viewport exceed a given width? Any insights?

bizz84 commented 1 year ago

I figured it out. Turns out I can use a SliverPadding with a "responsive" padding value:

class ProductsSliverAlignedGrid extends StatelessWidget {
  const ProductsSliverAlignedGrid({
    super.key,
    required this.itemCount,
    required this.itemBuilder,
  });

  /// Total number of items to display.
  final int itemCount;

  /// Function used to build a widget for a given index in the grid.
  final Widget Function(BuildContext, int) itemBuilder;

  @override
  Widget build(BuildContext context) {
    if (itemCount == 0) {
      return SliverToBoxAdapter(
        child: Center(
          child: Text(
            'No products found'.hardcoded,
            style: Theme.of(context).textTheme.headline4,
          ),
        ),
      );
    }
    // use a LayoutBuilder to determine the crossAxisCount
    return SliverLayoutBuilder(builder: (context, constraints) {
      final width = constraints.crossAxisExtent;
      final maxWidth = min(width, Breakpoint.desktop);
      // 1 column for width < 500px
      // then add one more column for each 250px
      final crossAxisCount = max(1, maxWidth ~/ 250);
      // calculate a "responsive" padding that increases
      // when the width is greater than the desktop breakpoint
      final padding = width > Breakpoint.desktop + Sizes.p32
          ? (width - Breakpoint.desktop) / 2
          : Sizes.p16;
      return SliverPadding(
        padding: EdgeInsets.symmetric(horizontal: padding, vertical: Sizes.p16),
        sliver: SliverAlignedGrid.count(
          crossAxisCount: crossAxisCount,
          mainAxisSpacing: Sizes.p24,
          crossAxisSpacing: Sizes.p24,
          itemBuilder: itemBuilder,
          itemCount: itemCount,
        ),
      );
    });
  }
}