Flutter animated sliver header - flutter

I am trying to create a profile header sliver that can animate.
If you consider above image, Section 1 is what we see in the fully expanded sliver, and Section 2 is what we want to see in pinned mode.
Now I would like transition to move the image - purple circle - to the side, shrink it slightly, and also move the name and the links.
I can achieve all of that but one thing: How to center them in the expanded view.
As I have to use transform to move widgets around, I cannot simply use a centring widget like column or center. And I didn't find a way to calculate the exact position to center the widget, as it needs the size of the widget, that I don't have.

Firstly I am using SliverPersistentHeaderDelegate and it provides shrinkOffset that will be used on linear interpolation(lerp method).
Then CompositedTransformTarget widget to follow the center widget.
On this example play with targetAnchor and followerAnchor and use t/shrinkOffset to maintain other animation.
class SFeb223 extends StatelessWidget {
const SFeb223({super.key});
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
SliverPersistentHeader(
delegate: MySliverPersistentHeaderDelegate(),
pinned: true,
),
SliverToBoxAdapter(
child: SizedBox(
height: 1333,
),
)
],
),
);
}
}
class MySliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {
final LayerLink layerLink = LayerLink();
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
double t = shrinkOffset / maxExtent;
return Material(
color: Colors.cyanAccent.withOpacity(.2),
child: Stack(
children: [
Align(
alignment:
Alignment.lerp(Alignment.center, Alignment.centerLeft, t)!,
child: CompositedTransformTarget(
link: layerLink,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
height: lerpDouble(100, kToolbarHeight - 10, t),
width: lerpDouble(100, kToolbarHeight - 10, t),
decoration: const ShapeDecoration(
shape: CircleBorder(),
color: Colors.deepPurple,
),
),
),
),
),
CompositedTransformFollower(
link: layerLink,
targetAnchor: Alignment.lerp(
Alignment.bottomCenter, Alignment.centerRight, t)!,
followerAnchor:
Alignment.lerp(Alignment.topCenter, Alignment.centerLeft, t)!,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
child: Column(
children: [Text("Sheikh")],
),
),
),
),
],
),
);
}
#override
double get maxExtent => kToolbarHeight * 6;
#override
double get minExtent => kToolbarHeight;
#override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) =>
false;
}

Related

Flutter animated SliverAppBar with Animated Positioned Image

I am trying to replicate this animation with my appBar:
I know I can use SliverAppBar and simply animate the textSize. But how would I implement the logic for the image? It moves to the right and slightly shrinks.
This is what I have for the text:
SliverAppBar(
expandedHeight: 200,
flexibleSpace: FlexibleSpaceBar(
title: Text('Test', textScaleFactor: 1),
),
pinned: true,
),
Any idea how I could solve this?
You play with SliverPersistentHeaderDelegate
class AppSliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
final t = shrinkOffset / maxExtent;
return Stack(
children: [
Align(
alignment: Alignment(0, .7), //perhaps it should also use lerp
child: Text(
"Title",
style: TextStyle(fontSize: ui.lerpDouble(34, 14, t)),
),
),
Align(
alignment:
Alignment.lerp(Alignment.topCenter, Alignment.topRight, t)!,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(Icons.settings),
),
)
],
);
}
#override
double get maxExtent => 200;
#override
double get minExtent => kToolbarHeight;
#override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
return false;
}
}
And used on
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
body: CustomScrollView(
slivers: [
SliverPersistentHeader(
pinned: true,
delegate: AppSliverPersistentHeaderDelegate(),
),
SliverToBoxAdapter(
child: Container(
height: 12222,
color: Colors.red,
),
)
],
),
),
);
}
}
You can try using AnimatedPositioned class which flutter already provide .
Check this link
https://api.flutter.dev/flutter/widgets/AnimatedPositioned-class.html
You can use it and the change the position and size depending on a specific action .

Parallax-style header scrolling performance in flutter

I'm developing a parallax-style header/background block in my flutter application, which scrolls upwards at around 1/3 the speed of the foreground content. All parts in the foreground are within the same customScrollView and the background header is in a positioned container at the top of the stack.
I'm using a listener on the customscrollview to update a y-offset integer, and then using that integer to update the top position on the element inside my stack.
While this works as expected, the issue I'm facing is a large amount of repainting takes place on scroll, which in the future may impact performance. I'm sure there may be a more efficient way to achieve this - such as placing the entire background in a separate child widget and passing the controller down to it from the parent widget - however I am struggling to find any information on doing so, or if this is the correct approach.
Can someone point me in the right direction for refactoring this in such a way as to disconnect the scrolling background from the foreground, so that the foreground doesn't repaint constantly?
class ScrollingWidgetList extends StatefulWidget {
ScrollingWidgetList();
#override
State<StatefulWidget> createState() {
return _ScrollingWidgetList();
}
}
class _ScrollingWidgetList extends State<ScrollingWidgetList> {
ScrollController _controller;
double _offsetY = 0.0;
_scrollListener() {
setState(() {
_offsetY = _controller.offset;
});
}
#override
void initState() {
_controller = ScrollController();
_controller.addListener(_scrollListener);
super.initState();
}
#override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
Positioned(
top: -(_offsetY / 3),
child: ConstrainedBox(
constraints: new BoxConstraints(
maxHeight: 300.0,
minHeight: MediaQuery.of(context).size.width * 0.35),
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topRight,
end: Alignment.bottomLeft,
colors: [
Theme.of(context).primaryColorDark,
Colors.blueGrey[900].withOpacity(0.8)
],
)),
height: MediaQuery.of(context).size.width * 0.35)),
width: MediaQuery.of(context).size.width,
),
CustomScrollView(controller: _controller, slivers: [
SliverList(
delegate: SliverChildListDelegate([
Padding(
padding: const EdgeInsets.only(top: 16.0, bottom: 8.0),
child: ListTile(
title: Padding(
padding: const EdgeInsets.only(top: 6.0),
child: Text('Header text',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
color: Colors.white)),
),
subtitle: Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text('Subtitle text',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white)),
),
))
])),
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return FakeItem(
executing: false,
delay: index.isOdd,
complete: false,
cancelled: false);
},
childCount: 30,
)),
])
],
);
}
}
A great solution was added by #pskink in the comments, however they seemed to have removed it. For anyone searching for an elegant solution, this is the basics of what was settled on.
You can see in the below code there is two layouts that are being handled by CustomMultiChildLayout. Hopefully this helps anyone searching for a similar solution
class ScrollList extends StatelessWidget {
final ScrollController _controller = ScrollController();
#override
Widget build(BuildContext context) {
return CustomMultiChildLayout(
delegate: ScrollingChildComponentDelegate(_controller),
children: <Widget>[
// background element layout
LayoutId(
id: 'background',
child: DecoratedBox(
decoration: BoxDecoration(
// box decoration
),
),
),
// foreground element layout
LayoutId(
id: 'scrollview',
child: CustomScrollView(
controller: _controller,
physics: AlwaysScrollableScrollPhysics(),
slivers: [
SliverToBoxAdapter(
child: ListTile(
title: Text('TitleText'),
),
subtitle: Text('SubtitleText'),
)),
),
SliverList(
delegate: SliverChildBuilderDelegate(itemBuilder,
childCount: 100),
),
],
)),
],
);
}
}
// itembuilder for child components
Widget itemBuilder(BuildContext context, int index) {
return Card(
margin: EdgeInsets.all(6),
child: ClipPath(
clipper: ShapeBorderClipper(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10))),
child: Container(
// child element content
)));
}
// controller for the animation
class ScrollingChildComponentDelegate extends MultiChildLayoutDelegate {
final ScrollController _controller;
ScrollingChildComponentDelegate(this._controller) : super(relayout: _controller);
#override
void performLayout(Size size) {
positionChild('background', Offset(0, -_controller.offset / 3));
layoutChild('background',
BoxConstraints.tightFor(width: size.width, height: size.height * 0.2));
positionChild('scrollview', Offset.zero);
layoutChild('scrollview', BoxConstraints.tight(size));
}
#override
bool shouldRelayout(covariant MultiChildLayoutDelegate oldDelegate) => true;
}

Let wrap take full width

How can I make the red box to use full width and put the button to the very right of the screen when wrapping?
Also I want spaceBetween if the button is not wrapping, but it does not work:
This is what I have sofar:
Column(
children: [
Align(
alignment: Alignment.topLeft,
child: Container(
color: Colors.red,
child: Wrap(
alignment: WrapAlignment.end,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Container(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
"Please wrap on small screens",
style: Theme.of(context).textTheme.bodyText1,
),
),
),
TextButton(
onPressed: () {},
child: Text("Bearbeiten"))
],
),
),
),
// ...
]
)
Updated
Finally, I manage to solve your problem. Probably is not a canonic way, but it's working.
You need to compute the total width of your content in order to compare it with the width of the screen (the width is the width of the widgets, plus space you want between widgets, plus padding from both sides).
With that, you are able to know when you need to change the layout, and you can apply the layout you want to each case.
Here I leave the code:
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final GlobalKey textKey = GlobalKey();
final GlobalKey buttonKey = GlobalKey();
double widthLimit = double.infinity;
void initState() {
super.initState();
SchedulerBinding.instance.addPostFrameCallback((timeStamp) {
final textBox = textKey.currentContext.findRenderObject() as RenderBox;
final buttonBox = buttonKey.currentContext.findRenderObject() as RenderBox;
widthLimit = textBox.size.width + buttonBox.size.width + 8 + 16 * 2;
});
}
Widget _wrap(bool spaceBetween, Widget child) => spaceBetween
? child
: Align(
alignment: Alignment.centerRight,
child: child,
);
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Container(
width: double.infinity,
padding: EdgeInsets.all(16.0),
color: Colors.red,
child: Builder(builder: (context) {
final screenWidth = MediaQuery.of(context).size.width;
final spaceBetween = screenWidth >= widthLimit;
return Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
alignment: WrapAlignment.spaceBetween,
children: [
Text("Please wrap on small screen small screen", key: textKey),
_wrap(
spaceBetween,
TextButton(
key: buttonKey,
onPressed: () {},
child: Text("Bearbeiten"),
)),
],
);
}),
),
),
);
}
}
Old Answer
I am going to take a couple of premises from your screenshots:
The Wrap doesn't have to take the full width by default
You only want to go to two lines when there is not enough space in the device screen.
With that, you can make what you want with this code:
Container(
padding: EdgeInsets.all(16.0),
color: Colors.red,
child: Wrap(
spacing: 8,
crossAxisAlignment: WrapCrossAlignment.center,
alignment: WrapAlignment.end,
children: [
Text("Please wrap on small screen"),
TextButton(onPressed: () {}, child: Text("Bearbeiten")),
]),
),
I added the spacing property to the Wrap to provide space as you wanted when both widgets are at the same line. Also, I set the alignment of the Wrap to end to align as you want.
When there is not enough space on the screen, the widget is going to move to a new line and align to the end. As the first widget is longer than the wrapped one and, as I said in the premises, we don't need to take the full width of the screen, Wrap will adapt to keep things aligned properly.

How do I go about removing the elevation from SliverPersistentHeader?

Hopefully a simple question, I have SliverPersistentHeader inside a CustomScrollView, and it looks like this SliverPersistentHeader has some sort of shadow/elevation. Is there a way to remove it (I've outlined the shadow in the red box)?
See picture below:
This basic body is a scaffold with the SliverPersistentHeader coming from the _makeHeader call:
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Consts.coMainBackground,
body: CustomScrollView(
slivers: <Widget>[
_sliverAppBar(),
_makeHeader(),
BlocBuilder<AllPersonsBloc, AllPersonsState>(
builder: (context, state) {
if (state is AllPersonsLoading) {
return _buildLoading();
} else if (state is AllPersonsLoaded) {
return _sliverList(context, state.persons);
} else if (state is AllPersonsError) {
return _buildErrorMessage(state.message);
} else {
return _buildErrorMessage('Unknown error!');
}
},
),
],
),
);
}
the Make Header function:
Widget _makeHeader() {
return SliverPersistentHeader(
pinned: true,
delegate: _SliverAppBarDelegate(
minHeight: 130.0,
maxHeight: 130.0,
child: Container(
color: Consts.coForestGreenBackground,
child: Container(
decoration: BoxDecoration(
color: Consts.coMainBackground,
borderRadius: BorderRadius.only(topLeft: Radius.circular(15), topRight: Radius.circular(15))
),
child: _cardHeader('People', Icons.search, 'Name')),
)
),
);
}
And finally the Delegate function:
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
_SliverAppBarDelegate({
#required this.minHeight,
#required this.maxHeight,
#required this.child,
});
final double minHeight;
final double maxHeight;
final Widget child;
#override
double get minExtent => minHeight;
#override
double get maxExtent => math.max(maxHeight, minHeight);
#override
Widget build(
BuildContext context,
double shrinkOffset,
bool overlapsContent)
{
return new SizedBox.expand(child: child);
}
#override
bool shouldRebuild(_SliverAppBarDelegate oldDelegate) {
return maxHeight != oldDelegate.maxHeight ||
minHeight != oldDelegate.minHeight ||
child != oldDelegate.child;
}
}
And card header (not actually a card)
Widget _cardHeader(String titleText, IconData inputIcon, String hint) {
return Container(
padding: EdgeInsets.symmetric(horizontal: standardEdgePadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(titleText, style: Consts.cardHeaderStyle,),
Container(
margin: EdgeInsets.only(top: 20),
height: 40,
decoration: BoxDecoration(
color: Consts.inputGreen,
borderRadius: BorderRadius.all(Radius.circular(10))
),
child: Row(
children: <Widget>[
Padding(
padding: EdgeInsets.symmetric(horizontal: 10),
child: Icon(
inputIcon,
color: Consts.inputGreenText,
size: Consts.cardInputIconSize,
),
),
Flexible(child: TextField(
decoration: new InputDecoration.collapsed(
hintText: hint,
hintStyle: Consts.cardInputStyle,
),
),)
],
),
)
],
),
);
}
SliverPersistentHeader alone does not have a shadow, therefore that elevation effect is not coming from your SliverPersistentHeader. It is not explicit from your code snippets, but I can see you have a _cardHeader('People', Icons.search, 'Name') method in your widget tree. I suspect it contains a Card widget inside the widget tree this method returns.
As seen in the Card widget Flutter documentation, Cards have a default non-zero elevation value, which might be casting the shadow in your case. See if there is any Card widget in your widget tree, and set its elevation parameter to zero.
So it turns out because, i have a container inside a container:
Widget _makeHeader() {
return SliverPersistentHeader(
pinned: true,
delegate: _SliverAppBarDelegate(
minHeight: 130.0,
maxHeight: 130.0,
child: Container(
color: Consts.coForestGreenBackground,
child: Container(
//There seemed to be some spacing between the search header and the rest of the list, this takes care of it
//transform: Matrix4.translationValues(0.0, 1.0, 0.0),
decoration: BoxDecoration(
color: Consts.coMainBackground,
borderRadius: BorderRadius.only(topLeft: Radius.circular(15), topRight: Radius.circular(15))
),
child: _cardHeader('People', Icons.search, 'Name')),
)
),
);
}
The way that the Header "meets" the SliverList there is an very minor gap (might be related to the emulator only). So the "coForestGreenBackground" colour in the parent container is showing through in that gap. There is no actual elevation/shadow.
If I uncomment the code, then the gap (aka elevation in my original question) vanishes.
I slept very poorly last night as I couldn't stop thinking about where I made a mistake. Thanks to #drogel for confirming there is no actual shadow/elevation on the SliverPersistentHeader.

Does Flutter support negative margin?

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.
The container has a useful transform property.
child: Container(
color: Theme.of(context).accentColor,
transform: Matrix4.translationValues(0.0, -50.0, 0.0),
),
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:
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),)
)
]
)
)
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.
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: ...
),
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
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 :
NOTE: You can also give negative values in Positioned Widget.
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:
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));
}
}
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:
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);
},
);
}
}
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?"),
))
]))
],
),
),
),
);
}
}