102

Negative margin is generally not needed but there are situations where it’s really useful. For example: why use negative margins?

For now, when I set margin for a container to a negative value, I got the following error:

I/flutter ( 3173): 'package:flutter/src/widgets/container.dart': Failed assertion: line 251: 'margin == null ||
I/flutter ( 3173): margin.isNonNegative': is not true.
Wei Song
  • 1,517
  • 3
  • 11
  • 9

10 Answers10

257

The container has a useful transform property.

enter image description here

child: Container(
  color: Theme.of(context).accentColor,
  transform: Matrix4.translationValues(0.0, -50.0, 0.0),
),
nagendra nag
  • 1,142
  • 2
  • 15
Praveen Vijayan
  • 6,521
  • 2
  • 29
  • 28
  • @ctrleffive and then you try to get it to scroll?! This is the ticket for the overlay and scrolling view. – jasonflaherty Nov 27 '19 at 22:28
  • This solution even works inside of a ListView or CustomScrollView with Padding around form elements. Part of me feels like it's an anti-pattern in Flutter, but it definitely works well. – RoboBear Sep 15 '20 at 19:19
  • 2
    Is there a way to place the following container's top margin fall behind the previous container? – U.Savas Dec 07 '20 at 19:42
  • 4
    Is there a way to remove the bottom margin that the transformed widget leaves? – U.Savas Dec 07 '20 at 21:18
43

I'm gonna give an answer for this, mostly because I had to find a way to do this.

I would like to say that it is not ideal and could likely be accomplished in a better way, but it does give the desired effect.

As you can see, the text can be pulled negatively outside its parent using a stack:enter image description here

Container(
  constraints: BoxConstraints.loose(Size.fromHeight(60.0)),
  decoration: BoxDecoration(color: Colors.black), 
  child: 
    Stack(
      alignment: Alignment.topCenter,
      overflow: Overflow.visible,
      children: [
        Positioned(
          top: 10.0,
          left: -15.0,
          right: -15.0,
          child: Text("OUTSIDE CONTAINER", style: TextStyle(color: Colors.red, fontSize: 24.0),)
        )
      ]
    )
)
AntonB
  • 2,724
  • 1
  • 31
  • 39
26

To answer this question you first have to define what "negative margins", or really "margins" in general, really are. In CSS, margins have various meanings in the various layout models, most commonly, they are one of several values that contribute to computing the offset that the block layout model uses to place subsequent children; a negative total margin in this case merely means the next child is placed above the bottom of the previous child instead of after it.

In Flutter, as in CSS, there are several layout models; however, there is currently no widget that is equivalent to the CSS block layout model (which supports margin collapsing, negative margins, skipping floats, etc). Such a layout model could certainly be implemented, it just hasn't been implemented yet, at least not in the framework itself.

To implement such a layout model, you would create a RenderBox descendant similar to RenderFlex or RenderListBody, probably providing a way to set the margins of each child using a ParentDataWidget in the same way that Flex children can have their flex configured using the Expanded widget.

Probably the most complicated part of designing a new layout model like this would be deciding how to handle overflow or underflow, when the children are too big or too small to fit the constraints passed to this new layout render object. The RenderFlex render object has a way to distribute the space if the children underflow, and considers it an error if they overflow (in debug mode, this is shown by a yellow-and-black striped warning area and a message logged to the console); the RenderListBody render object on the other hand takes the view that the constraints must be unbounded in the main axis, which means you can basically only use this layout model inside a list (hence the name).

If writing a new layout model is not attractive, you could use one of the existing layout widgets that allow overlapping children. Stack is the obvious choice, where you set the explicit positions of each child and they can overlap arbitrarily (this is vaguely similar to the CSS absolute position layout model). Another option is the CustomMultiChildLayout widget, which lets you layout and position each child in turn. With this, you could position each child one after the other, simulating negative margins by setting the position of the subsequent child to a value that's derived from the size and position of the previous child, but such that the subsequent child's top is above the previous child's bottom.

If there's interest in a block-like layout model, we could certainly implement it (please file a bug and describe the model you'd like implemented, or, implement it yourself and send a pull request for review). So far, though, we've not found that it has been that useful in practice, at least not useful enough to justify the complexity.

Ian Hickson
  • 8,174
  • 1
  • 29
  • 22
  • Thanks for your answer! I admitted that the need is mainly for rendering some server-provided declarative markup in a format similar to CSS block and flex layout model. It seems like a new layout model is more appropriate for this use case. However, it might not be ideal to re-implement the full layout model. I have been thinking of leveraging Facebook Yoga (https://facebook.github.io/yoga/) which has implemented all the layout models we want. I didn't dig deeper into RenderBox. Do you think that it is feasible or appropriate that we integrate Yoga with the custom RenderBox? – Wei Song Jan 04 '18 at 23:28
  • I don't know Yoga enough to really say one way or the other. – Ian Hickson Jan 05 '18 at 23:57
21

To extend the accepted answer, you can wrap any widget with Transform.translate. It takes a simple Offset as parameter.

I find it is easier to use than the translation Matrix.

Transform.translate(
   // e.g: vertical negative margin
   offset: const Offset(-10, 0),
   child: ...
),
legoffmael
  • 261
  • 2
  • 6
10

The short answer is "No, it doesn't".

To give few more details, Flutter has a sophisticated but effective algorithm for rendering its widgets. Margins and Paddings are analyzed at runtime, and the final size and position of the widget is determined. When you try to issue a negative margine you are purposefully creating a not valide layout where a widget is somehow dropping out of the space it is supposed to occupy.

Consider reading the doc here.

Anyhow I believe you should formulate better the question in another thread and really ask a solution for the behavior you are trying to achieve with those negative margins. I am sure you'll get much more that way.

Cheers

Fabio Veronese
  • 7,726
  • 2
  • 18
  • 27
5

No, Flutter does not allow negative margins but just in case you still want your widgets to overlap each other, you can use a Stack with Positioned which will allow you to generate the layout which you can do with negative margins.

Here is an example :

import 'package:flutter/material.dart';

class MyHomePage extends StatefulWidget {
  MyHomePageState createState() => new MyHomePageState();
}

class MyHomePageState extends State<MyHomePage>  {


  @override
  Widget build(BuildContext context) {
    return new Scaffold(
        body: new Center(
          child: new Container(
            padding: const EdgeInsets.all(8.0),
            height: 500.0,
            width: 500.0,
            child: new Stack(
              overflow: Overflow.visible,
              children: <Widget>[
                new Icon(Icons.pages, size: 36.0, color: Colors.red),
                new Positioned(
                  left: 20.0,
                  child: new Icon(Icons.pages, size: 36.0, color: Colors.green),
                ),

              ],
            ),
          ),
    )
    );
  }
}

void main() {
  runApp(new MaterialApp(
    title: 'Flutter Demo',
    theme: new ThemeData(
      primarySwatch: Colors.deepPurple,
    ),
    home: new MyHomePage(),
  ));
}

This will result in :

ScreenShot

NOTE: You can also give negative values in Positioned Widget.

Stephen Rauch
  • 47,830
  • 31
  • 106
  • 135
Sarthak Verma
  • 388
  • 3
  • 7
  • 18
5

You can use OverflowBox to disregard certain constraints.

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Container(
          color: Colors.blue.shade300,
          child: Padding(
            padding: const EdgeInsets.all(20),
            child: Column(
              children: [
                Expanded(
                  child: Container(
                    color: Colors.white,
                    child: Center(
                      child: Text('Padding on this one.'),
                    ),
                  ),
                ),
                SizedBox(height: 20),
                Expanded(
                  child: OverflowBox(
                    maxWidth: MediaQuery.of(context).size.width,
                    child: Container(
                      color: Colors.red.shade300,
                      child: Center(
                        child: Text('No padding on this one!'),
                      ),
                    ),
                  ),
                ),
                SizedBox(height: 20),
                Expanded(
                  child: Container(
                    color: Colors.yellow.shade300,
                    child: Center(
                      child: Text('Look, padding is back!'),
                    ),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }

Result:

enter image description here

Tom Roggero
  • 5,777
  • 1
  • 32
  • 39
  • 1
    You will have to wrap with Expanded every other children, doesn't help much in terms of less boilerplate – temirbek Dec 27 '21 at 12:13
4

To overcome some horizontal padding you can create such a Widget:

Usage (will take out 8pt from the padding left and right.

const ExpandWidth(
  child: MyWidget(),
  width: 8,
)

Implementation:

class ExpandWidth extends StatelessWidget {
  final double width;
  final Widget child;

  const ExpandWidth({
    super.key,
    required this.child,
    this.width = 0,
  });

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (BuildContext context, BoxConstraints constraints) {
        return IntrinsicHeight(
          child: OverflowBox(
            maxWidth: constraints.maxWidth + width * 2,
            child: child,
          ),
        );
      },
    );
  }
}

EDIT:

Margin Widget

I played a little around and wanted to share this here:

enter image description here

It's far from perfect, but at least anything to start with.

You can modify horizontal, vertical, left and top. The interesting part is the Margin widget.

In this example all the grey container have a padding of 16.

Result

Code example of the screenshot

Center(
  child: Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: [
      Container(
        width: 360,
        height: 300,
        color: Colors.black12,
        padding: const EdgeInsets.all(16),
        child: Container(
          color: Colors.black38,
          padding: const EdgeInsets.all(16),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              Column(
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: [
                  Margin(
                    horizontal: -24,
                    top: -8,
                    child: Container(
                      padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                      color: Colors.greenAccent.withOpacity(0.8),
                      child: const Center(child: Text('Horizontal: -24 & Top: -8')),
                    ),
                  ),
                  // const SizedBox(height: 8),
                  Container(
                    padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                    color: Colors.greenAccent.withOpacity(0.8),
                    child: const Center(child: Text('No modification')),
                  ),
                  const SizedBox(height: 8),
                  Margin(
                    vertical: -16,
                    top: -16,
                    child: Container(
                      padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                      color: Colors.greenAccent.withOpacity(0.8),
                      child: const Center(child: Text('Vertical: -24 & Top: -16')),
                    ),
                  ),
                ],
              ),
              Margin(
                vertical: -16,
                top: 32,
                child: Container(
                  padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                  color: Colors.greenAccent.withOpacity(0.8),
                  child: const Center(child: Text('Third')),
                ),
              ),
            ],
          ),
        ),
      ),
      const SizedBox(height: 16),
      Container(
        width: 360,
        height: 300,
        color: Colors.black12,
        padding: const EdgeInsets.all(16),
        child: Container(
          color: Colors.black38,
          padding: const EdgeInsets.all(16),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              Expanded(
                child: Row(
                  children: [
                    Flexible(
                      child: Margin(
                        vertical: -24,
                        // horizontal: 8,
                        child: Container(
                          padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
                          color: Colors.greenAccent.withOpacity(0.8),
                          child: const Center(child: Text('V -24')),
                        ),
                      ),
                    ),
                    const SizedBox(width: 16),
                    Flexible(
                      child: Margin(
                        vertical: 0,
                        child: Container(
                          padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
                          color: Colors.greenAccent.withOpacity(0.8),
                          child: const Center(child: Text('Nothing')),
                        ),
                      ),
                    ),
                  ],
                ),
              ),
              const SizedBox(width: 16),
              Margin(
                vertical: -16,
                child: Container(
                  padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
                  color: Colors.greenAccent.withOpacity(0.8),
                  child: const Center(
                      child: Text(
                    'V\n-16',
                    textAlign: TextAlign.center,
                  )),
                ),
              ),
            ],
          ),
        ),
      ),
    ],
  ),
);

margin.dart

import 'package:flutter/material.dart';

class SizeProviderWidget extends StatefulWidget {
  final Widget child;
  final Function(Size) onChildSize;

  const SizeProviderWidget({
    super.key,
    required this.onChildSize,
    required this.child,
  });
  @override
  _SizeProviderWidgetState createState() => _SizeProviderWidgetState();
}

class _SizeProviderWidgetState extends State<SizeProviderWidget> {
  @override
  void initState() {
    _onResize();
    super.initState();
  }

  void _onResize() {
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      if (context.size is Size) {
        widget.onChildSize(context.size!);
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    ///add size listener for every build uncomment the fallowing
    ///_onResize();
    return widget.child;
  }
}

class Margin extends StatefulWidget {
  const Margin({
    super.key,
    required this.child,
    this.horizontal = 0,
    this.vertical = 0,
    this.left = 0,
    this.top = 0,
  });

  final Widget child;
  final double horizontal;
  final double vertical;
  final double top;
  final double left;

  @override
  State<Margin> createState() => _MarginState();
}

class _MarginState extends State<Margin> {
  Size childSize = Size.zero;

  @override
  Widget build(BuildContext context) {
    final horizontalMargin = widget.horizontal * 2 * -1;
    final verticalMargin = widget.vertical * 2 * -1;

    final newWidth = childSize.width + horizontalMargin;
    final newHeight = childSize.height + verticalMargin;

    if (childSize != Size.zero) {
      return LimitedBox(
        maxWidth: newWidth,
        maxHeight: newHeight,
        child: OverflowBox(
          maxWidth: newWidth,
          maxHeight: newHeight,
          child: Transform.translate(
            offset: Offset(widget.left, widget.top),
            child: SizedBox(
              width: newWidth,
              height: newHeight,
              child: widget.child,
            ),
          ),
        ),
      );
    }

    return SizeProviderWidget(
      child: widget.child,
      onChildSize: (size) {
        setState(() => childSize = size);
      },
    );
  }
}
Stefan Rein
  • 8,084
  • 3
  • 37
  • 37
3

A hack if you really want this (for example, me) and need performance:

Disadvantage: The hit testing has problem on those edges. But if you only want to display the widget without wanting to click it, it is completely fine.

How to use it: As if you are using Padding widget, except that now your padding can be negative and no errors will happen.

import 'dart:math' as math;

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

class AllowNegativePadding extends SingleChildRenderObjectWidget {
  const AllowNegativePadding({
    Key key,
    @required this.padding,
    Widget child,
  })  : assert(padding != null),
        super(key: key, child: child);

  /// The amount of space by which to inset the child.
  final EdgeInsetsGeometry padding;

  @override
  RenderAllowNegativePadding createRenderObject(BuildContext context) {
    return RenderAllowNegativePadding(
      padding: padding,
      textDirection: Directionality.of(context),
    );
  }

  @override
  void updateRenderObject(BuildContext context, RenderAllowNegativePadding renderObject) {
    renderObject
      ..padding = padding
      ..textDirection = Directionality.of(context);
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding));
  }
}

class RenderAllowNegativePadding extends RenderShiftedBox {
  RenderAllowNegativePadding({
    EdgeInsetsGeometry padding,
    TextDirection textDirection,
    RenderBox child,
  })  : assert(padding != null),
        // assert(padding.isNonNegative),
        _textDirection = textDirection,
        _padding = padding,
        super(child);

  EdgeInsets _resolvedPadding;

  void _resolve() {
    if (_resolvedPadding != null) return;
    _resolvedPadding = padding.resolve(textDirection);
    // assert(_resolvedPadding.isNonNegative);
  }

  void _markNeedResolution() {
    _resolvedPadding = null;
    markNeedsLayout();
  }

  /// The amount to pad the child in each dimension.
  ///
  /// If this is set to an [EdgeInsetsDirectional] object, then [textDirection]
  /// must not be null.
  EdgeInsetsGeometry get padding => _padding;
  EdgeInsetsGeometry _padding;

  set padding(EdgeInsetsGeometry value) {
    assert(value != null);
    // assert(value.isNonNegative);
    if (_padding == value) return;
    _padding = value;
    _markNeedResolution();
  }

  /// The text direction with which to resolve [padding].
  ///
  /// This may be changed to null, but only after the [padding] has been changed
  /// to a value that does not depend on the direction.
  TextDirection get textDirection => _textDirection;
  TextDirection _textDirection;

  set textDirection(TextDirection value) {
    if (_textDirection == value) return;
    _textDirection = value;
    _markNeedResolution();
  }

  @override
  double computeMinIntrinsicWidth(double height) {
    _resolve();
    final double totalHorizontalPadding = _resolvedPadding.left + _resolvedPadding.right;
    final double totalVerticalPadding = _resolvedPadding.top + _resolvedPadding.bottom;
    if (child != null) // next line relies on double.infinity absorption
      return child.getMinIntrinsicWidth(math.max(0.0, height - totalVerticalPadding)) + totalHorizontalPadding;
    return totalHorizontalPadding;
  }

  @override
  double computeMaxIntrinsicWidth(double height) {
    _resolve();
    final double totalHorizontalPadding = _resolvedPadding.left + _resolvedPadding.right;
    final double totalVerticalPadding = _resolvedPadding.top + _resolvedPadding.bottom;
    if (child != null) // next line relies on double.infinity absorption
      return child.getMaxIntrinsicWidth(math.max(0.0, height - totalVerticalPadding)) + totalHorizontalPadding;
    return totalHorizontalPadding;
  }

  @override
  double computeMinIntrinsicHeight(double width) {
    _resolve();
    final double totalHorizontalPadding = _resolvedPadding.left + _resolvedPadding.right;
    final double totalVerticalPadding = _resolvedPadding.top + _resolvedPadding.bottom;
    if (child != null) // next line relies on double.infinity absorption
      return child.getMinIntrinsicHeight(math.max(0.0, width - totalHorizontalPadding)) + totalVerticalPadding;
    return totalVerticalPadding;
  }

  @override
  double computeMaxIntrinsicHeight(double width) {
    _resolve();
    final double totalHorizontalPadding = _resolvedPadding.left + _resolvedPadding.right;
    final double totalVerticalPadding = _resolvedPadding.top + _resolvedPadding.bottom;
    if (child != null) // next line relies on double.infinity absorption
      return child.getMaxIntrinsicHeight(math.max(0.0, width - totalHorizontalPadding)) + totalVerticalPadding;
    return totalVerticalPadding;
  }

  @override
  void performLayout() {
    final BoxConstraints constraints = this.constraints;
    _resolve();
    assert(_resolvedPadding != null);
    if (child == null) {
      size = constraints.constrain(Size(
        _resolvedPadding.left + _resolvedPadding.right,
        _resolvedPadding.top + _resolvedPadding.bottom,
      ));
      return;
    }
    final BoxConstraints innerConstraints = constraints.deflate(_resolvedPadding);
    child.layout(innerConstraints, parentUsesSize: true);
    final BoxParentData childParentData = child.parentData as BoxParentData;
    childParentData.offset = Offset(_resolvedPadding.left, _resolvedPadding.top);
    size = constraints.constrain(Size(
      _resolvedPadding.left + child.size.width + _resolvedPadding.right,
      _resolvedPadding.top + child.size.height + _resolvedPadding.bottom,
    ));
  }

  @override
  void debugPaintSize(PaintingContext context, Offset offset) {
    super.debugPaintSize(context, offset);
    assert(() {
      final Rect outerRect = offset & size;
      debugPaintPadding(context.canvas, outerRect, child != null ? _resolvedPadding.deflateRect(outerRect) : null);
      return true;
    }());
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding));
    properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
  }
}
ch271828n
  • 15,854
  • 5
  • 53
  • 88
1

You can try something like this:

import 'package:flutter/material.dart';

void main() => runApp(MaterialApp(
      home: MyApp(),
    ));

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('text'),
      ),
      body: Container(
        child: Center(
          child: Column(
            children: <Widget>[
              Container(
                  height: 300.0,
                  width: MediaQuery.of(context).size.width,
                  decoration: BoxDecoration(
                      image: DecorationImage(
                          image: NetworkImage(
                              "https://images.unsplash.com/photo-1539450780284-0f39d744d390?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=d30c5801b9fff3d4a5b7f1522901db9f&auto=format&fit=crop&w=1051&q=80"),
                          fit: BoxFit.cover)),
                  child: Stack(
                      alignment: Alignment.topCenter,
                      overflow: Overflow.visible,
                      children: [
                        Positioned(
                            top: 200.0,
                            child: Card(
                              child: Text("Why not?"),
                            ))
                      ]))
            ],
          ),
        ),
      ),
    );
  }
}
Jack Sun
  • 2,012
  • 1
  • 16
  • 20