How to get the SliverPersistentHeader to "overgrow" - flutter

I'm using a SliverPersistentHeader in my CustomScrollView to have a persistent header that shrinks and grows when the user scrolls, but when it reaches its maximum size it feels a bit stiff since it doesn't "overgrow".
Here is a video of the behaviour I want (from the Spotify app) and the behaviour I have:
.

While looking for a solution for this problem, I came across three different ways to solve it:
Create a Stack that contains the CustomScrollView and a header widget (overlaid on top of the scroll view), provide a ScrollController to the CustomScrollView and pass the controller to the header widget to adjust its size
Use a ScrollController, pass it to the CustomScrollView and use the value of the controller to adjust the maxExtent of the SliverPersistentHeader (this is what Eugene recommended).
Write my own Sliver to do exactly what I want.
I ran into problems with solution 1 & 2:
This solution seemed a bit "hackish" to me. I also had the problem, that "dragging" the header didn't scroll anymore, since the header was not inside the CustomScrollView anymore.
Adjusting the size of the sliver during scrolling results in strange side effects. Notably, the distance between the header and slivers below increases during the scroll.
That's why I opted for solution 3. I'm sure the way I implemented it, is not the best, but it works exactly as I want:
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'dart:math' as math;
/// The delegate that is provided to [ElSliverPersistentHeader].
abstract class ElSliverPersistentHeaderDelegate {
double get maxExtent;
double get minExtent;
/// This acts exactly like `SliverPersistentHeaderDelegate.build()` but with
/// the difference that `shrinkOffset` might be negative, in which case,
/// this widget exceeds `maxExtent`.
Widget build(BuildContext context, double shrinkOffset);
}
/// Pretty much the same as `SliverPersistentHeader` but when the user
/// continues to drag down, the header grows in size, exceeding `maxExtent`.
class ElSliverPersistentHeader extends SingleChildRenderObjectWidget {
final ElSliverPersistentHeaderDelegate delegate;
ElSliverPersistentHeader({
Key key,
ElSliverPersistentHeaderDelegate delegate,
}) : this.delegate = delegate,
super(
key: key,
child:
_ElSliverPersistentHeaderDelegateWrapper(delegate: delegate));
#override
_ElPersistentHeaderRenderSliver createRenderObject(BuildContext context) {
return _ElPersistentHeaderRenderSliver(
delegate.maxExtent, delegate.minExtent);
}
}
class _ElSliverPersistentHeaderDelegateWrapper extends StatelessWidget {
final ElSliverPersistentHeaderDelegate delegate;
_ElSliverPersistentHeaderDelegateWrapper({Key key, this.delegate})
: super(key: key);
#override
Widget build(BuildContext context) =>
LayoutBuilder(builder: (context, constraints) {
final height = constraints.maxHeight;
return delegate.build(context, delegate.maxExtent - height);
});
}
class _ElPersistentHeaderRenderSliver extends RenderSliver
with RenderObjectWithChildMixin<RenderBox> {
final double maxExtent;
final double minExtent;
_ElPersistentHeaderRenderSliver(this.maxExtent, this.minExtent);
#override
bool hitTestChildren(HitTestResult result,
{#required double mainAxisPosition, #required double crossAxisPosition}) {
if (child != null) {
return child.hitTest(result,
position: Offset(crossAxisPosition, mainAxisPosition));
}
return false;
}
#override
void performLayout() {
/// The amount of scroll that extends the theoretical limit.
/// I.e.: when the user drags down the list, although it already hit the
/// top.
///
/// This seems to be a bit of a hack, but I haven't found a way to get this
/// information in another way.
final overScroll =
constraints.viewportMainAxisExtent - constraints.remainingPaintExtent;
/// The actual Size of the widget is the [maxExtent] minus the amount the
/// user scrolled, but capped at the [minExtent] (we don't want the widget
/// to become smaller than that).
/// Additionally, we add the [overScroll] here, since if there *is*
/// "over scroll", we want the widget to grow in size and exceed
/// [maxExtent].
final actualSize =
math.max(maxExtent - constraints.scrollOffset + overScroll, minExtent);
/// Now layout the child with the [actualSize] as `maxExtent`.
child.layout(constraints.asBoxConstraints(maxExtent: actualSize));
/// We "clip" the `paintExtent` to the `maxExtent`, otherwise the list
/// below stops moving when reaching the border.
///
/// Tbh, I'm not entirely sure why that is.
final paintExtent = math.min(actualSize, maxExtent);
/// For the layout to work properly (i.e.: the following slivers to
/// scroll behind this sliver), the `layoutExtent` must not be capped
/// at [minExtent], otherwise the next sliver will "stop" scrolling when
/// [minExtent] is reached,
final layoutExtent = math.max(maxExtent - constraints.scrollOffset, 0.0);
geometry = SliverGeometry(
scrollExtent: maxExtent,
paintExtent: paintExtent,
layoutExtent: layoutExtent,
maxPaintExtent: maxExtent,
);
}
#override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
/// This sliver is always displayed at the top.
context.paintChild(child, Offset(0.0, 0.0));
}
}
}

Now you can create your own SliverPersistentHeaderDelegate and override this param"
#override
OverScrollHeaderStretchConfiguration get stretchConfiguration =>
OverScrollHeaderStretchConfiguration();
By default if null, but once you added it will allow you to stretch the view.
This is the class I use:
class CustomSliverDelegate extends SliverPersistentHeaderDelegate {
final Widget child;
final Widget title;
final Widget background;
final double topSafeArea;
final double maxExtent;
CustomSliverDelegate({
this.title,
this.child,
this.maxExtent = 350,
this.background,
this.topSafeArea = 0,
});
#override
Widget build(BuildContext context, double shrinkOffset,
bool overlapsContent) {
final appBarSize = maxExtent - shrinkOffset;
final proportion = 2 - (maxExtent / appBarSize);
final percent = proportion < 0 || proportion > 1 ? 0.0 : proportion;
return Theme(
data: ThemeData.dark(),
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: maxExtent),
child: Stack(
children: [
Positioned(
bottom: 0.0,
left: 0.0,
right: 0.0,
top: 0,
child: background,
),
Positioned(
bottom: 0.0,
left: 0.0,
right: 0.0,
child: Opacity(opacity: percent, child: child),
),
Positioned(
top: 0.0,
left: 0.0,
right: 0.0,
child: AppBar(
title: Opacity(opacity: 1 - percent, child: title),
backgroundColor: Colors.transparent,
elevation: 0,
),
),
],
),
),
);
}
#override
OverScrollHeaderStretchConfiguration get stretchConfiguration =>
OverScrollHeaderStretchConfiguration();
#override
double get minExtent => kToolbarHeight + topSafeArea;
#override
bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) {
return true;
}
}

EDIT: I found another way how to stretch an image in AppBar
here is minimal reproducible example:
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(
debugShowCheckedModeBanner: false,
home: Home(),
));
}
class Home extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: [
SliverAppBar(
pinned: true,
expandedHeight: 200,
title: Text('Title'),
stretch: true,
flexibleSpace: FlexibleSpaceBar(
background: Image.network('https://i.imgur.com/2pQ5qum.jpg', fit: BoxFit.cover),
),
),
SliverToBoxAdapter(
child: Column(
children: List.generate(50, (index) {
return Container(
height: 72,
color: Colors.blue[200],
alignment: Alignment.centerLeft,
margin: EdgeInsets.all(8),
child: Text('Item $index'),
);
}),
),
),
],
),
);
}
}
The magic is in - stretch: true and BouncingScrollPhysics() properties.
There is not complicated listeners, stageful widgets so on. Just FlexibleSpaceBar with an image on background.

I solved this problem by simply creating a custom SliverPersistentHeaderDelegate.
Just override the getter for stretchConfiguration. Here's my code in case this is useful.
class LargeCustomHeader extends SliverPersistentHeaderDelegate {
LargeCustomHeader(
{this.children,
this.title = '',
this.childrenHeight = 0,
this.backgroundImage,
this.titleHeight = 44,
this.titleMaxLines = 1,
this.titleTextStyle = const TextStyle(
fontSize: 30,
letterSpacing: 0.5,
fontWeight: FontWeight.bold,
height: 1.2,
color: ColorConfig.primaryContrastColor)}) {}
final List<Widget> children;
final String title;
final double childrenHeight;
final String backgroundImage;
final int _fadeDuration = 250;
final double titleHeight;
final int titleMaxLines;
final double _navBarHeight = 56;
final TextStyle titleTextStyle;
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return Container(
constraints: BoxConstraints.expand(),
decoration: BoxDecoration(
// borderRadius: BorderRadius.vertical(bottom: Radius.circular(35.0)),
color: Colors.black,
),
child: Stack(
fit: StackFit.loose,
children: <Widget>[
if (this.backgroundImage != null) ...[
Positioned(
top: 0,
left: 0,
right: 0,
bottom: 0,
child: FadeInImage.assetNetwork(
placeholder: "assets/images/image-placeholder.png",
image: backgroundImage,
placeholderScale: 1,
fit: BoxFit.cover,
alignment: Alignment.center,
imageScale: 0.1,
fadeInDuration: const Duration(milliseconds: 500),
fadeOutDuration: const Duration(milliseconds: 200),
),
),
Positioned(
top: 0,
left: 0,
right: 0,
bottom: 0,
child: Container(
color: Color.fromRGBO(0, 0, 0, 0.6),
),
),
],
Positioned(
bottom: 0,
left: 0,
right: 0,
top: _navBarHeight + titleHeight,
child: AnimatedOpacity(
opacity: (shrinkOffset >= childrenHeight / 3) ? 0 : 1,
duration: Duration(milliseconds: _fadeDuration),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[if (children != null) ...children],
))),
Positioned(
top: _navBarHeight,
left: 0,
right: 0,
height: titleHeight,
child: Padding(
padding: const EdgeInsets.only(
right: 30, bottom: 0, left: 30, top: 5),
child: AnimatedOpacity(
opacity: (shrinkOffset >= childrenHeight + (titleHeight / 3))
? 0
: 1,
duration: Duration(milliseconds: _fadeDuration),
child: Text(
title,
style: titleTextStyle,
maxLines: titleMaxLines,
overflow: TextOverflow.ellipsis,
),
),
),
),
Container(
color: Colors.transparent,
height: _navBarHeight,
child: AppBar(
elevation: 0.0,
backgroundColor: Colors.transparent,
title: AnimatedOpacity(
opacity:
(shrinkOffset >= childrenHeight + (titleHeight / 3))
? 1
: 0,
duration: Duration(milliseconds: _fadeDuration),
child: Text(
title,
),
)),
)
],
));
}
#override
double get maxExtent => _navBarHeight + titleHeight + childrenHeight;
#override
double get minExtent => _navBarHeight;
// #override
// FloatingHeaderSnapConfiguration get snapConfiguration => FloatingHeaderSnapConfiguration() ;
#override
OverScrollHeaderStretchConfiguration get stretchConfiguration =>
OverScrollHeaderStretchConfiguration(
stretchTriggerOffset: maxExtent,
onStretchTrigger: () {},
);
double get maxShrinkOffset => maxExtent - minExtent;
#override
bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) {
//TODO: implement specific rebuild checks
return true;
}
}

May be I have a easy way to code.
By use SliverAppBar and inside child widget leading, FlexibleSpaceBar and inside child widget title.
And by LayoutBuilder we can make some animation.
Full code link
SliverAppBar(
toolbarHeight: _appBarHeight,
collapsedHeight: _appBarHeight,
backgroundColor: Colors.white.withOpacity(1),
shadowColor: Colors.white.withOpacity(0),
expandedHeight: maxWidth,
/// ========================================
/// custom your app bar
/// ========================================
leading: Container(
width: 100,
height: _appBarHeight,
// color: Colors.blueAccent,
child: Center(
child: Icon(Icons.arrow_back_ios, color: Colors.black),
),
),
pinned: true,
stretch: true,
flexibleSpace: FlexibleSpaceBar(
stretchModes: [
StretchMode.fadeTitle,
StretchMode.blurBackground,
StretchMode.zoomBackground,
],
titlePadding: EdgeInsets.all(0),
title: LayoutBuilder(
builder: (_, __) {
var height = __.maxHeight;
/// ========================================
/// custom animate you want by height change
/// ========================================
// Logger.debug(__.maxHeight);
return Stack(
children: [
if (height > 100)
Container(
width: double.infinity,
height: double.infinity,
color: Colors.black.withOpacity(0.3),
),
],
);
},
),
background: Image.network(
'https://xx',
fit: BoxFit.cover,
),
),
),

You can try using SliverAppBar with stretch:true and pass the widget you want to display in the appbar as flexibleSpace.
Here is an example
CustomScrollView(
physics: BouncingScrollPhysics(),
slivers: <Widget>[
SliverAppBar(
stretch: true,
floating: true,
backgroundColor: Colors.black,
expandedHeight: 300,
centerTitle: true,
title: Text("My Custom Bar"),
leading: IconButton(
onPressed: () {},
icon: Icon(Icons.menu),
),
actions: <Widget>[
IconButton(
onPressed: () {},
icon: Icon(Icons.search),
)
],
flexibleSpace: FlexibleSpaceBar(
collapseMode: CollapseMode.pin,
stretchModes:
[
StretchMode.zoomBackground,
StretchMode.blurBackground
],
background: YourCustomWidget(),
),
),
SliverList(
delegate: SliverChildListDelegate(
[
Container(color: Colors.red, height: 300.0),
Container(color: Colors.blue, height: 300.0),
],
),
),
],
);

Related

How to achieve an expandable bottom navigation bar in Flutter

I am trying to build an app that includes a bottom navigation bar for navigating between screens.
In the middle of the bottom navigation bar, I want to add a button which expands the bottom navigation bar with a semicircle, and revels more buttons.
I've read the documentation of the bottom navigation bar, and searched a lot in pub.dev if there is something similar I can use, but I couldn't find any.
Does anyone know if it's achievable, and if so, how?
Thank you very much
You can check this simple implementation with showDialog and CustomPainter. Basically it involved displaying a showDialog with bottom padding equals the height of BottomNavigationBar, then arrange the items within a Stack. The half circle is drawn using CustomPainter.
Full example app:
import 'dart:math' as math;
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('Test App'),
),
bottomNavigationBar: BottomNavigationBar(
unselectedItemColor: Colors.grey,
selectedItemColor: Colors.blue,
showUnselectedLabels: true,
selectedFontSize: 14,
unselectedFontSize: 14,
type: BottomNavigationBarType.fixed,
onTap: (index) {
if (index == 2) {
final diameter = 200.0;
final iconSize = 40;
showDialog(
context: context,
barrierDismissible: true,
barrierColor: Colors.grey.withOpacity(0.1),
builder: (context) => Material(
color: Colors.transparent,
child: Stack(
alignment: AlignmentDirectional.bottomCenter,
children: [
Container(
width: diameter + iconSize,
height: diameter / 1.5,
alignment: Alignment.bottomCenter,
margin:
EdgeInsets.only(bottom: kBottomNavigationBarHeight),
child: Stack(
children: [
Container(
alignment: Alignment.bottomCenter,
child: MyArc(diameter: diameter)),
Positioned(
left: 0,
bottom: 10,
child: _buildButton(),
),
Positioned(
left: diameter / 4,
top: 10,
child: _buildButton(),
),
Positioned(
right: diameter / 4,
top: 10,
child: _buildButton(),
),
Positioned(
right: 0,
bottom: 10,
child: _buildButton(),
)
],
),
),
],
),
),
);
}
},
items: List<BottomNavigationBarItem>.generate(
5,
(index) =>
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
),
),
);
}
_buildButton() {
return Container(
constraints: BoxConstraints.tightFor(width: 40, height: 60),
child: Column(
children: [
Text(
'Title',
style: TextStyle(fontSize: 12),
),
SizedBox(height: 3),
CircleAvatar(
backgroundColor: Colors.white,
child: Icon(Icons.home),
),
],
),
);
}
}
class MyArc extends StatelessWidget {
final double diameter;
const MyArc({Key key, this.diameter = 200}) : super(key: key);
#override
Widget build(BuildContext context) {
return CustomPaint(
painter: MyPainter(),
size: Size(diameter, diameter),
);
}
}
// This is the Painter class
class MyPainter extends CustomPainter {
#override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..shader = RadialGradient(
colors: [
Colors.blue,
Colors.purpleAccent.withOpacity(0.4),
],
).createShader(Rect.fromCircle(
center: Offset(size.width / 2, size.height),
radius: 200,
));
canvas.drawArc(
Rect.fromCenter(
center: Offset(size.width / 2, size.height),
height: size.height * 1.5,
width: size.width,
),
math.pi,
math.pi,
false,
paint,
);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
Result:
I think you need to learn about Flutter Animated Radial Menu and how to implement it in your code, you can go with this article and try to implement in your way.

Sliver Appbar [Collapsing Toolbar] animate title from left to center in Flutter

Here is my Build method for collapsing toolbar:-
#override
Widget build(BuildContext context) {
return SafeArea(
child: CustomScrollView(
controller: controller,
slivers: <Widget>[
SliverAppBar(
pinned: true,
expandedHeight: appBarHeight,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
color: Colors.black,
),
onPressed: () => null,
),
floating: true,
flexibleSpace: FlexibleSpaceBar(
titlePadding: EdgeInsets.only(left:leftV , bottom:bottomV ),
title: Text(
"Title ",
style: TextStyle(
color: Colors.black,
fontSize: 16.0,
),
),
),
),
SliverList(delegate:
SliverChildBuilderDelegate((BuildContext context, int index) {
return ListTile(title: Text("Flutter / $index"));
}))
],
),
);
}
As per the doc I got solution to remove padding :-
/// By default the value of this property is
/// EdgeInsetsDirectional.only(start: 72, bottom: 16) if the title is
/// not centered, EdgeInsetsDirectional.only(start 0, bottom: 16) otherwise.
final EdgeInsetsGeometry titlePadding;
But I got the output as :-
I want to center the title when the app bar is totally collapsed.
Issue has been filed in github also check here.
Edit:
I ended up creating a better solution that utilizes the transformations already happening within the FlexibleSpaceBar. After copying the file from the gist into your project, replace FlexibleSpaceBar with MyFlexibleSpaceBar and provide a titlePaddingTween such as
titlePaddingTween: EdgeInsetsTween(begin: EdgeInsets.only(left: 16.0, bottom: 16), end: EdgeInsets.only(left: 72.0, bottom: 16))
instead of titlePadding. The tween will animate from the "begin" EdgeInsets when the appbar is fully expanded to the "end" EdgeInsets when the appbar is collapsed.
I also added a foreground parameter that displays above the title and background, but doesn't transform as they do.
Original Answer:
The other answers are good, but they rebuild more widgets than necessary. My solution builds on the other answers but will only rebuild what is within ValueListenableBuilder:
class SamplePage extends StatelessWidget {
static const _kBasePadding = 16.0;
static const kExpandedHeight = 250.0;
final ValueNotifier<double> _titlePaddingNotifier = ValueNotifier(_kBasePadding);
final _scrollController = ScrollController();
double get _horizontalTitlePadding {
const kCollapsedPadding = 60.0;
if (_scrollController.hasClients) {
return min(_kBasePadding + kCollapsedPadding,
_kBasePadding + (kCollapsedPadding * _scrollController.offset)/(kExpandedHeight - kToolbarHeight));
}
return _kBasePadding;
}
#override
Widget build(BuildContext context) {
_scrollController.addListener(() {
_titlePaddingNotifier.value = _horizontalTitlePadding;
});
return Scaffold(
body: NestedScrollView(
controller: _scrollController,
headerSliverBuilder: (context, innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
expandedHeight: kExpandedHeight,
floating: false,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
collapseMode: CollapseMode.pin,
centerTitle: false,
titlePadding: EdgeInsets.symmetric(vertical: 16, horizontal: 0),
title: ValueListenableBuilder(
valueListenable: _titlePaddingNotifier,
builder: (context, value, child) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: value),
child: Text(
"Title"),
);
},
),
background: Container(color: Colors.green)
)
),
];
},
body: Text("Body text")
),
);
}
}
Found the solution on my own!!!
Add below code to your Sliver App Bar .........
flexibleSpace: LayoutBuilder(
builder:
(BuildContext context, BoxConstraints constraints) {
double percent =
((constraints.maxHeight - kToolbarHeight) *
100 /
(appBarHeight - kToolbarHeight));
double dx = 0;
dx = 100 - percent;
if (constraints.maxHeight == 100) {
dx = 0;
}
return Stack(
children: <Widget>[
Padding(
padding: const EdgeInsets.only(
top: kToolbarHeight / 4, left: 0.0),
child: Transform.translate(
child: Text(
title,
style: MyTextStyle.getAppBarTextStyle(
screenUtil, appColors),
),
offset: Offset(
dx, constraints.maxHeight - kToolbarHeight),
),
),
],
);
},
),
Percentage is calculated based on the scroll and it animation has been placed accordingly.
I managed to get a solution with a ScrollController.
Example Result Gif
I used this next function:
double get _horizontalTitlePadding {
const kBasePadding = 15.0;
const kMultiplier = 0.5;
if (_scrollController.hasClients) {
if (_scrollController.offset < (kExpandedHeight / 2)) {
// In case 50%-100% of the expanded height is viewed
return kBasePadding;
}
if (_scrollController.offset > (kExpandedHeight - kToolbarHeight)) {
// In case 0% of the expanded height is viewed
return (kExpandedHeight / 2 - kToolbarHeight) * kMultiplier +
kBasePadding;
}
// In case 0%-50% of the expanded height is viewed
return (_scrollController.offset - (kExpandedHeight / 2)) * kMultiplier +
kBasePadding;
}
return kBasePadding;
}
And I used it inside of my SilverAppBar titlePadding:
child: Scaffold(
body: CustomScrollView(
controller: _scrollController,
slivers: <Widget>[
SliverAppBar(
pinned: true,
expandedHeight: kExpandedHeight,
flexibleSpace: FlexibleSpaceBar(
titlePadding: EdgeInsets.symmetric(
vertical: 16.0, horizontal: _horizontalTitlePadding),
Just make sure to initialize the controller in initState():
_scrollController = ScrollController()..addListener(() => setState(() {}));
I had the same issue, I resolved it using LayoutBuilder as the child for the flexibleSpace widget of the SliverAppBar. The purpose of the LayoutBuilder is to enable me to know the current position (height) of the appBar.
I'm using MediaQuery.of(context).size to automatically get the size of the screen.
var top = 0.0;
var appbarThreshold = 140.0;
class _MySliverAppBarState extends State<MySliverAppBar> {
#override
Widget build(BuildContext context) {
Size size = MediaQuery.of(context).size;
return SliverAppBar(
centerTitle: true,
pinned: true,
leading: TextButton(
child: CircleAvatar(
radius: size.width / 4,
backgroundColor: Colors.blue.withOpacity(0.3),
),
onPressed: () {
print("Hello");
},
),
leadingWidth: size.width / 4,
collapsedHeight: size.height / 11.5,
expandedHeight: size.height / 5,
backgroundColor: Colors.white,
foregroundColor: Colors.black,
flexibleSpace: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
top = constraints.biggest.height;
return FlexibleSpaceBar(
title: AnimatedOpacity(
duration: const Duration(milliseconds: 300),
opacity: 1.0,
child: Text(
top < appbarThreshold ? "Bloom" : "Welcome, Iremide",
style: TextStyle(
fontSize: top < appbarThreshold
? size.height / 30
: size.height / 40,
color: Colors.black87,
fontFamily: 'SourceSansSerif',
fontWeight: FontWeight.w700),
),
),
titlePadding: top < appbarThreshold
? EdgeInsets.fromLTRB(
size.width / 4.9, 0.0, 0.0, size.height / 18)
: EdgeInsets.fromLTRB(
size.width / 14, 0.0, 0.0, size.height / 30),
);
},
),
);
}
}
You can adjust the Position of the Title when appBar is collapsed by editing the left padding size here:
//
titlePadding: top < appbarThreshold
? EdgeInsets.fromLTRB(
size.width / 4.9, 0.0, 0.0, size.height / 18)
: EdgeInsets.fromLTRB(
size.width / 14, 0.0, 0.0, size.height / 30),
Regards.
late ScrollController _scrollController;
static const kExpandedHeight = 300.0;
#override
void initState() {
super.initState();
_scrollController = ScrollController()..addListener(() => setState(() {}));
}
double get _horizontalTitlePadding {
const kBasePadding = 15.0;
const kMultiplier = 0.5;
if (_scrollController.hasClients) {
if (_scrollController.offset < (kExpandedHeight / 2)) {
// In case 50%-100% of the expanded height is viewed
return kBasePadding;
}
if (_scrollController.offset > (kExpandedHeight - kToolbarHeight)) {
// In case 0% of the expanded height is viewed
return (kExpandedHeight / 2 - kToolbarHeight) * kMultiplier +
kBasePadding;
}
// In case 0%-50% of the expanded height is viewed
return (_scrollController.offset - (kExpandedHeight / 2)) * kMultiplier +
kBasePadding;
}
return kBasePadding;
}
CustomScrollView(
controller: _scrollController,
slivers: [
SliverAppBar(
expandedHeight: kExpandedHeight,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
title: Text(product.title),
titlePadding: EdgeInsets.symmetric(
vertical: 16.0, horizontal: _horizontalTitlePadding),
background: Hero(
tag: product.id,
child: Image.network(
product.imageUrl,
fit: BoxFit.cover,
),
),
),
),
SliverList(
delegate: SliverChildListDelegate([
//add your widgets here
])
]
) //end of CustomScrollView

In Flutter, how to create a SliverAppBar with frosted glass effect?

I want to create a top app bar similar to the one in the Apple News app.
It's a bit difficult to see the blur in the screenshot because the bar is so thin, but you get the idea.
I want the bar to expand and contract by scrolling and be pinned at the top when contracted, just like in the screenshot, the SliverAppBar does all that, except that I can't wrap it in ClipRect, BackdropFilter and Opacity to create the frosted glass effect because CustomScrollView only takes RenderSliver child classes.
My test code:
Widget build(BuildContext context) {
return CustomScrollView(
slivers: <Widget>[
SliverAppBar(
title: Text('SliverAppBar'),
elevation: 0,
floating: true,
pinned: true,
backgroundColor: Colors.grey[50],
expandedHeight: 200.0,
flexibleSpace: FlexibleSpaceBar(
background: Image.network("https://i.imgur.com/cFzxleh.jpg", fit: BoxFit.cover)
),
)
,
SliverFixedExtentList(
itemExtent: 150.0,
delegate: SliverChildListDelegate(
[
Container(color: Colors.red),
Container(color: Colors.purple),
Container(color: Colors.green),
Container(color: Colors.orange),
Container(color: Colors.yellow),
Container(color: Colors.pink,
child: Image.network("https://i.imgur.com/cFzxleh.jpg", fit: BoxFit.cover)
),
],
),
),
],
);
}
Is there a way to achieve what I want?
I managed to get it working by wrapping an AppBar inside of a SliverPersistentHeader (this is basically what SliverAppBar does).
Ignore the un-blurred edges it's an iOS simulator bug.
Here is a proof of concept code example:
class TranslucentSliverAppBar extends StatelessWidget {
#override
Widget build(BuildContext context) {
return SliverPersistentHeader(
floating: true,
pinned: true,
delegate: _TranslucentSliverAppBarDelegate(
MediaQuery.of(context).padding,
)
);
}
}
class _TranslucentSliverAppBarDelegate extends SliverPersistentHeaderDelegate {
/// This is required to calculate the height of the bar
final EdgeInsets safeAreaPadding;
_TranslucentSliverAppBarDelegate(this.safeAreaPadding);
#override
double get minExtent => safeAreaPadding.top;
#override
double get maxExtent => minExtent + kToolbarHeight;
#override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
return ClipRect(child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16),
child: Opacity(
opacity: 0.93,
child: Container(
// Don't wrap this in any SafeArea widgets, use padding instead
padding: EdgeInsets.only(top: safeAreaPadding.top),
height: maxExtent,
color: Colors.white,
// Use Stack and Positioned to create the toolbar slide up effect when scrolled up
child: Stack(
overflow: Overflow.clip,
children: <Widget>[
Positioned(
bottom: 0, left: 0, right: 0,
child: AppBar(
primary: false,
elevation: 0,
backgroundColor: Colors.transparent,
title: Text("Translucent App Bar"),
),
)
],
)
)
)
));
}
#override
bool shouldRebuild(_TranslucentSliverAppBarDelegate old) {
return maxExtent != old.maxExtent || minExtent != old.minExtent ||
safeAreaPadding != old.safeAreaPadding;
}
}

How do I add widgets to the Sliver App Bar area?

I would like to add widgets in the Sliver AppBar area and make it feel more natural. The AppBar gives the liberty to add images but does it have any functionalities to add up more widgets into it?
I am focusing on how to use those CircleAvatar and Text widgets on the sliver appbar.
You won't be able to do that with the SliverAppBar :/.
What you should do is using SliverPersistentHeader with SliverPersistentHeaderDelegate.
You'll have to write a little bit more code but no too much :).
Example:
slivers = [
SliverPersistentHeader(
pinned: True,
delegate: _SliverAppBarDelegate(
minHeight: 60.0,
maxHeight: 250.
),
),
SliverList(
delegate: SliverChildListDelegate([
Padding(
padding: const EdgeInsets.symmetric(horizontal: 25.0),
child: Column(
children: ...
),
)
]),
),
];
...
class _SliverAppBarDelegate extends x {
_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: LayoutBuilder(
builder: (_, constraints) {
DO WHAT YOU WANT HERE !
}
)
);
}
#override
bool shouldRebuild(_SliverAppBarDelegate oldDelegate) {
return maxHeight != oldDelegate.maxHeight ||
minHeight != oldDelegate.minHeight ||
child != oldDelegate.child;
}
}
There is a sample using SliverPersistentHeader with SliverPersistentHeaderDelegate.
In this sample when you scroll up the header will resized and button dissapear
slivers: <Widget>[
makeHeader(),
// You can put more slivers here
],
This is the method makeHeader:
SliverPersistentHeader makeHeader() {
return SliverPersistentHeader(
pinned: pinned,
floating: true,
delegate: _SliverAppBarDelegate(
minHeight: 60.0,
maxHeight: 200.0,
),
);
}
And the class _SliverAppBarDelegate:
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
final double minHeight;
final double maxHeight;
_SliverAppBarDelegate(
{#required this.minHeight,
#required this.maxHeight,
this.hideButtonWhenExpanded = true});
#override
double get minExtent => minHeight;
#override
double get maxExtent => math.max(maxHeight, minHeight);
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
final appBarSize = maxHeight - shrinkOffset;
final proportion = 2 - (maxHeight / appBarSize);
final photoToButton = 160 * proportion;
final percent = proportion < 0 || proportion > 1 ? 0.0 : proportion;
return new SizedBox.expand(
child: Container(
color: Colors.white,
child: Stack(
alignment: Alignment.topCenter,
children: <Widget>[
Positioned(
top: 10.0,
child: CircleAvatar(
minRadius: 20.0,
maxRadius: 75.0 * proportion > 20 ? 75.0 * proportion : 20.0,
backgroundImage: NetworkImage(
'https://randomuser.me/api/portraits/men/75.jpg'),
),
),
Positioned(
left: 0.0,
right: 0.0,
top: photoToButton,
child: Opacity(
opacity: percent,
child: FlatButton(
onPressed: () {},
child: Text(
'Add Contact',
style: TextStyle(
color: Colors.blue, fontSize: 14.0 * proportion),
),
),
),
),
Positioned(
left: 0.0,
right: 0.0,
top: appBarSize - 1.0 > 59.0 ? appBarSize - 1 : 59.0,
child: const Divider(
// color: Colors.grey,
height: 1,
thickness: 0.5,
),
)
],
),
),
);
}
#override
bool shouldRebuild(_SliverAppBarDelegate oldDelegate) {
return maxHeight != oldDelegate.maxHeight ||
minHeight !=
oldDelegate
.minHeight;
}
}

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?"),
))
]))
],
),
),
),
);
}
}