I'm building a custom flexible app bar to use in a NestedScrollView and i'm running into issues with the animation.
What I want to achieve is something like this:
In the expanded state, the text is aligned with the top of the Profile picture (in orange), but when the bar collapse, it ends up aligned in the center. I also need all the elements (text + picture) to scale accordingly.
I have access to the current expand factor of the bar using a LayoutBuilder and a bit of math
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
double paddingTop = MediaQuery.of(context).padding.top;
double maxExtent = kExpandedHeight + paddingTop;
double minExtent = kToolbarHeight + paddingTop;
final double deltaExtent = maxExtent - minExtent;
// 0.0 -> Expanded
// 1.0 -> Collapsed to toolbar
final double t = (1.0 - (constraints.maxHeight - minExtent) / deltaExtent)
.clamp(0.0, 1.0);
// t can be used to animate here
});
I have managed to scale elements with the Transform widget and the value of t but what I can't figure out is how to animate the switch of alignment of the text part so that it end up perfectly aligned in the center with the picture.
Any ideas? :)
try this,
class Act_Demo extends StatefulWidget {
#override
_Act_DemoState createState() => _Act_DemoState();
}
class _Act_DemoState extends State<Act_Demo> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top),
child: CustomScrollView(
slivers: <Widget>[
TransitionAppBar(
backgroundColor: Colors.red,
extent: 150,
avatar: ListTile(
title: Text("Name", style: TextStyle(fontSize: 18.0, fontWeight: FontWeight.bold),),
subtitle: Text("abc#gmail.com"),
trailing: CircleAvatar(backgroundColor: Colors.orange,radius: 30.0,),
),
),
SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return Container(
child: ListTile(
title: Text("${index}a"),
));
}, childCount: 25))
],
),
),
);
}
}
.
class TransitionAppBar extends StatelessWidget {
final Widget avatar;
final double extent;
final Color backgroundColor;
TransitionAppBar({this.avatar, this.backgroundColor = Colors.transparent, this.extent = 200, Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return SliverPersistentHeader(
pinned: true,
delegate: _TransitionAppBarDelegate(
avatar: avatar,
backgroundColor: backgroundColor,
extent: extent > 150 ? extent : 150
),
);
}
}
class _TransitionAppBarDelegate extends SliverPersistentHeaderDelegate {
final _avatarAlignTween = AlignmentTween(begin: Alignment.center, end: Alignment.topCenter);
final Widget avatar;
final double extent;
final Color backgroundColor;
_TransitionAppBarDelegate({this.avatar, this.backgroundColor, this.extent = 200})
: assert(avatar != null),
assert(backgroundColor != null),
assert(extent == null || extent >= 150);
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
final progress = shrinkOffset / maxExtent;
final avatarAlign = _avatarAlignTween.lerp(progress);
return Container(
color: backgroundColor,
child: Align(
alignment: avatarAlign,
child: Container(
child: avatar,
),
),
);
}
#override
double get maxExtent => extent;
#override
double get minExtent => 70;
#override
bool shouldRebuild(_TransitionAppBarDelegate oldDelegate) {
return avatar != oldDelegate.avatar;
}
}
Related
I'm trying to do the following in my code:
the user can click on the plus button and increment the counter.
if the counter is more than or equal to 4, the user can no longer press the plus button, since the absorb pointer, turns it's absorb field to true.
after the point where the button cannot be pressed, on each tap, the canvas changes the offset and the color of the circle and then, the build widget rebuilds the entire page (there is a GestureDetector widget as the parent of the AbsorbPointer widget to check taps on the entire screen and set new offset and color to the circle.)
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
bool absorbPointer = false;
Color color = Colors.red;
Offset offset = const Offset(0,0);
void _incrementCounter() {
setState(() {
_counter++;
});
}
void _setRandomCircle(double maxHeight, double maxWidth) {
var rnd = Random();
double randWidth = rnd.nextInt(maxWidth.toInt() - 0).toDouble();
double randHeight = rnd.nextInt(maxHeight.toInt() - 0).toDouble();
offset = Offset(randWidth, randHeight);
}
void _setRandomColor() {
color = Color((math.Random().nextDouble() * 0xFFFFFF).toInt())
.withOpacity(1.0);
}
void _setAbsorbPointer(){
absorbPointer = true;
setState(() {});
}
#override
Widget build(BuildContext context) {
var size = MediaQuery.of(context).size;
double maxWidth = size.width;
double maxHeight = size.height;
return GestureDetector(
onTap: () {
if (_counter < 4) {
_incrementCounter();
} else {
_setRandomColor();
_setRandomCircle(maxHeight, maxWidth);
_setAbsorbPointer();
}
},
child: AbsorbPointer(
absorbing: absorbPointer,
child: Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text(widget.title),
),
body: Stack(
children: [
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
CustomPaint(
painter: MyPainter(
offsetOfCircle: offset,
colorOfCircle: color,
),
)
],
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
),
),
);
}
}
My problem:
Nothing seems to work properly and I get these errors:
"UnimplementedError"
"Each child must be laid out exactly once."
with these details:
The _ScaffoldLayout custom multichild layout delegate forgot to lay out the following child:
_ScaffoldSlot.body: RenderErrorBox#f9667 NEEDS-LAYOUT NEEDS-PAINT
parentData: offset=Offset(0.0, 0.0); id=_ScaffoldSlot.body
constraints: MISSING
size: MISSING
And this is Mypainter:
class MyPainter extends CustomPainter {
final Color colorOfCircle;
final Offset offsetOfCircle;
MyPainter({required this.colorOfCircle, required this.offsetOfCircle});
#override
void paint(Canvas canvas, Size size) {
var hexColor = "0x${colorOfCircle.value.toRadixString(16)}";
var myCustomPaint = Paint()..color = Color(int.parse(hexColor));
canvas.drawCircle(offsetOfCircle, 20, myCustomPaint);
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
// TODO: implement shouldRepaint
throw UnimplementedError();
}
}
The code structure you are following, it is little different. You can do it the way you described, you dont need to use extra AbsorbPointer widget tap event.
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
bool absorbPointer = false;
Color color = Colors.red;
Offset offset = const Offset(0, 0);
void _setRandomCircleColorAndPosition(double maxHeight, double maxWidth) {
double randWidth = math.Random().nextDouble() * (maxWidth * .9);
double randHeight = math.Random().nextDouble() * (maxHeight * .5);
setState(() {
offset = Offset(randWidth, randHeight);
color = Color((math.Random().nextDouble() * 0xFFFFFF).toInt())
.withOpacity(1.0);
});
}
#override
Widget build(BuildContext context) {
var size = MediaQuery.of(context).size;
double maxWidth = size.width;
double maxHeight = size.height;
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text(widget.title),
),
body: GestureDetector(
onTap: () {
_setRandomCircleColorAndPosition(maxHeight, maxWidth);
},
child: Container(
color: Colors.cyanAccent.withOpacity(.3),
child: Stack(
children: [
Align(
alignment: Alignment.center,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
CustomPaint(
painter: MyPainter(
offsetOfCircle: offset,
colorOfCircle: color,
),
),
],
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
if (_counter >= 4) {
absorbPointer = true;
_setRandomCircleColorAndPosition(maxHeight, maxWidth);
} else {
_counter++;
}
setState(() {});
},
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
class MyPainter extends CustomPainter {
Color colorOfCircle;
Offset offsetOfCircle;
MyPainter({required this.colorOfCircle, required this.offsetOfCircle});
#override
void paint(Canvas canvas, Size size) {
var myCustomPaint = Paint()..color = colorOfCircle;
canvas.drawCircle(offsetOfCircle, 20, myCustomPaint);
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
I'm trying to create draggable and resizable widgets (Matrix4 Gesture Detector Widget) wrapped inside a Stackwidget. I'm able to drag and resize the widgets in the way I want them to, but the problem is, when I programatically add another widget to the Stack's children, all the draggable widgets which were previously positioned revert and comes back to the center along with the newly added dragabble widget.
Let's say there are 2 draggable widgets in the Stack initially and we also positioned them,
When I add another widget programatically by tapping on the Add button, the positions are lost and every draggable widget appears to the center like this,
How can I fix this issue and prevent the widgets from not reverting to the center? Here is the code.
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider.value(value: DummyProvider())
],
child: MaterialApp(
home: MyApp(),
),
)
);
}
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
DummyProvider dummyProvider =
Provider.of<DummyProvider>(context, listen: true);
return Scaffold(
body: SafeArea(
child: Column(
children: [
Container(
height: Constants.deviceHeight! * .6,
color: Colors.red,
child: Stack(
children: List.generate(
dummyProvider.list.length,
(index) => Drag(
text: Text(dummyProvider.list[index].text,
style: TextStyle(fontSize: 40)),
)),
),
),
ElevatedButton(
onPressed: () {
dummyProvider.addToList(DraggableText(
id: DateTime.now().millisecondsSinceEpoch.toString(),
text: String.fromCharCodes(List.generate(
5, (index) => Random().nextInt(33) + 89))));
},
child: Text("Add"),
)
],
),
),
);
}
}
DraggableTextclass,
class DraggableText {
String id = '';
String text = '';
FontWeight fontWeight = FontWeight.normal;
FontStyle fontStyle = FontStyle.normal;
TextDecoration fontDecoration = TextDecoration.none;
Matrix4 position = Matrix4.identity();
DraggableText({required this.id, required this.text});
}
The provider that has all the draggable widget,
class DummyProvider extends ChangeNotifier {
List<DraggableText> list = [];
String currentText = '';
void addToList(DraggableText text) {
list.add(text);
notifyListeners();
}
}
The dragabble text widget using Matrix4 Gesture Detector Package,
class Drag extends StatelessWidget {
final Text text;
const Drag({required this.text});
#override
Widget build(BuildContext context) {
final ValueNotifier<Matrix4> notifier = ValueNotifier(Matrix4.identity());
return MatrixGestureDetector(
onMatrixUpdate: (m, tm, sm, rm) {
notifier.value = m;
print("$m $tm $sm $rm");
},
child: AnimatedBuilder(
animation: notifier,
builder: (ctx, child) {
return Transform(
transform: notifier.value,
child: Center(
child: Stack(
children: <Widget>[
Transform.scale(
scale: 1,
origin: Offset(0.0, 0.0),
child: GestureDetector(
child:
Container(color: Colors.blueAccent, child: text)),
),
],
),
),
);
},
),
);}}
The issue with generator, it is refreshing the list including the positions, every setState it is creating newList. I added extra property to model class to handle it and some extra-methods at notifier while testing the issue, this may help in the future.
void main() {
runApp(MultiProvider(
providers: [ChangeNotifierProvider.value(value: DummyProvider())],
child: const MaterialApp(
home: MyApp(),
),
));
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: LayoutBuilder(
builder: (context, constraints) => SafeArea(
child: Column(
children: [
Container(
height: constraints.maxHeight * .6,
color: Colors.red,
child: Consumer<DummyProvider>(
builder: (context, value, child) => Stack(
children: value.list
.map(
(d) => Drag(
key: ValueKey(d.id),
notifier: d.notifier!,
callBack: (
Matrix4 matrix,
) {
// print("${matrix.row0.a} ${matrix.row1.a}");
print(matrix);
},
text: Text(
d.text,
style: TextStyle(fontSize: 40),
),
),
)
.toList(),
),
),
),
ElevatedButton(
onPressed: () {
final provider = context.read<DummyProvider>();
provider.addToList(
DraggableText(
id: DateTime.now().millisecondsSinceEpoch.toString(),
text: String.fromCharCodes(
List.generate(5, (index) => Random().nextInt(33) + 89),
),
),
);
},
child: Text("Add"),
)
],
),
),
),
);
}
}
class Drag extends StatelessWidget {
final Text text;
final ValueNotifier<Matrix4> notifier;
final Function callBack;
const Drag({
Key? key,
required this.text,
required this.callBack,
required this.notifier,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return MatrixGestureDetector(
onMatrixUpdate: (m, tm, sm, rm) {
notifier.value = m;
// print("$m $tm $sm $rm");
callBack(notifier.value);
},
child: AnimatedBuilder(
animation: notifier,
builder: (ctx, child) {
print("${notifier.value.row0.a} ${notifier.value.row1.a}");
return Transform(
transform: notifier.value,
child: Center(
child: Stack(
children: <Widget>[
Transform.scale(
scale: 1,
origin: Offset(0.0, 0.0),
child: GestureDetector(
child: Container(
color: Colors.blueAccent,
child: text,
),
),
),
],
),
),
);
},
),
);
}
}
class DraggableText {
String id = '';
String text = '';
FontWeight fontWeight = FontWeight.normal;
FontStyle fontStyle = FontStyle.normal;
TextDecoration fontDecoration = TextDecoration.none;
Matrix4 position = Matrix4.identity();
ValueNotifier<Matrix4>? notifier;
DraggableText({
required this.id,
required this.text,
}) {
this.notifier = this.notifier ?? ValueNotifier(Matrix4.identity());
}
}
class DummyProvider extends ChangeNotifier {
List<DraggableText> list = [];
String currentText = '';
void addToList(DraggableText text) {
list.add(text);
notifyListeners();
}
void updatePosition(int index, Matrix4 position) {
list[index].position = position;
notifyListeners();
}
void updatePositionByItem(DraggableText text, Matrix4 position) {
list.firstWhere((element) => element == text).position = position;
notifyListeners();
}
}
try this in stateless:
class Drag extends StatelessWidget {
final Text text;
const Drag({Key? key, required this.text}) : super(key: key);
#override
Widget build(BuildContext context) {
or in stateful:
class Drag extends StatefulWidget {
final Text text;
Drag({
Key? key,
required this.text,
}) : super(key: key);
#override
State<StatefulWidget> createState() => DragState();
}
class DragState extends State<Drag> {
use it with:
Drag(
key: UniqueKey(),
text:
So, I'm pretty new to flutter, I'm trying to have a AppBar that looks like this :
And I'm actually able to do it using ClipPath like this :
class Header extends StatelessWidget {
final String page;
const Header({required this.page});
#override
Widget build(BuildContext context) {
final double topPadding = MediaQuery.of(context).padding.top;
return ClipPath(
child: HeaderContent(page: page, padding: topPadding),
clipper: Clipper(page: page));
}
}
class HeaderContent extends StatelessWidget {
final String page;
final double padding;
const HeaderContent({required this.page, required this.padding});
#override
Widget build(BuildContext context) {
final int headerHeight = page == "inscription" ? 250 : 150;
return Container(
height: headerHeight + padding,
padding: EdgeInsets.all(15),
width: double.infinity,
color: Theme.of(context).primaryColor,
child: AppBar(
title: Text("NOTIFICATIONS", style: Theme.of(context).textTheme.headline6),
centerTitle: true,
leading: Icon(
Icons.arrow_back,
size: 40,
),
),
);
}
}
class Clipper extends CustomClipper<Path> {
final String page;
const Clipper({required this.page});
#override
Path getClip(Size size) {
final Path path = Path();
selectHeaderClip(size, page, path);
return path;
}
#override
bool shouldReclip(CustomClipper<Path> oldClipper) => false;
}
Where the selectHeaderClip is a custom function that return the appropriate clip for a given page.
However, when I have a scrollable page (list of notifications for example), I see the notifications hiding too early as if there was a white rectangular container underneath my appBar. So my question basically is: how can I make that container transparent ?
Ps: sorry for the bad formulation of the question, don't hesitate to edit.
I am using 2 Sliver Headers above GridView and a ListView on a CustomScrollView . I want only 1 of the headers (the one I am scrolling over) to be pinned When I scroll down. I want to be able to scroll down and only one of the headers is pinned when I pass over Gridview.
EDIT:
Added _SliverAppBarDelegate
Scaffold(
body: SafeArea(
child: DefaultTabController(
length: 2,
child: CustomScrollView(
slivers: [
makeHeader('Categories', false),
SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 1.5,
),
delegate: SliverChildBuilderDelegate(
(context, index) => Container(
margin: EdgeInsets.all(5.0),
color: Colors.blue,
),
childCount: 10),
),
makeHeader('Watchlist', false),
SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 1.5,
),
delegate: SliverChildBuilderDelegate(
(context, index) => Container(
margin: EdgeInsets.all(5.0),
color: Colors.red,
),
childCount: 10),
),
],
),
),
),
)
SliverPersistentHeader makeHeader(String headerText, bool pinned) {
return SliverPersistentHeader(
pinned: pinned,
floating: true,
delegate: _SliverAppBarDelegate(
minHeight: 40.0,
maxHeight: 60.0,
child: Container(
child: Text(
headerText,
style: TextStyle(fontSize: 24, color: Colors.green,fontWeight: FontWeight.bold),
)),
),
);
}
///////////////////////////EDIT
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
_SliverAppBarDelegate({
#required this.minHeight,
#required this.maxHeight,
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;
}
}
This is an old question but I'm putting my solution here in case anyone else needs the sticky header effect without a plugin.
My solution is to have the sliver headers' minExtent value in a map where the key is the number of sliverList items above the header.
final _headersMinExtent = <int, double>{};
We can then reduce the minExtent when the header needs to be pushed out of the view. To accomplish that we listen to the scrollController. Note that we could also update the pinned status but we wouldn't get a transition.
We calculate the minExtent from :
The number of items above the header : key.
The number of headers already pushed out the view : n.
_scrollListener() {
var n = 0;
setState(() {
_headersMinExtent.forEach((key, value) {
_headersMinExtent[key] = (key * 30 + n * 40 + 190 - _scrollController.offset).clamp(0, 40);
n++;
});
});
}
When we construct our widget list we have to pass the minExtent parameter to the SliverPersistentHeaderDelegate :
List<Widget> _constructList() {
var widgetList = <Widget>[];
for (var i = 0; i < itemList.length; i++) {
// We want a header every 5th item
if (i % 5 == 0) {
// Don't forget to init the minExtent value.
_headersMinExtent[i] = _headersMinExtent[i] ?? 40;
// We pass the minExtent as a parameter.
widgetList.add(SliverPersistentHeader(pinned: true, delegate: HeaderDelegate(_headersMinExtent[i]!)));
}
widgetList.add(SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => Container(
decoration: BoxDecoration(color: Colors.yellow, border: Border.all(width: 0.5)),
height: 30,
),
childCount: 1,
)));
}
return widgetList;
}
This is the result :
Full application code :
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final _scrollController = ScrollController();
final _headersMinExtent = <int, double>{};
final itemList = List.filled(40, "item");
#override
void initState() {
super.initState();
_scrollController.addListener(() {
_scrollListener();
});
}
_scrollListener() {
var n = 0;
setState(() {
_headersMinExtent.forEach((key, value) {
_headersMinExtent[key] = (key * 30 + n * 40 + 190 - _scrollController.offset).clamp(0, 40);
n++;
});
});
}
List<Widget> _constructList() {
var widgetList = <Widget>[];
for (var i = 0; i < itemList.length; i++) {
// We want a header every 5th item
if (i % 5 == 0) {
// Don't forget to init the minExtent value.
_headersMinExtent[i] = _headersMinExtent[i] ?? 40;
// We pass the minExtent as a parameter.
widgetList.add(SliverPersistentHeader(pinned: true, delegate: HeaderDelegate(_headersMinExtent[i]!)));
}
widgetList.add(SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => Container(
decoration: BoxDecoration(color: Colors.yellow, border: Border.all(width: 0.5)),
height: 30,
),
childCount: 1,
)));
}
return widgetList;
}
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
appBar: AppBar(),
body: CustomScrollView(
controller: _scrollController,
slivers: _constructList(),
),
),
);
}
}
class HeaderDelegate extends SliverPersistentHeaderDelegate {
final double _minExtent;
HeaderDelegate(this._minExtent);
#override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
return Container(
decoration: BoxDecoration(color: Colors.green, border: Border.all(width: 0.5)),
);
}
#override
double get minExtent => _minExtent;
#override
double get maxExtent => 40;
#override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) => true;
}
class ListChildDelegate extends SliverChildDelegate {
#override
Widget? build(BuildContext context, int index) {
// TODO: implement build
throw UnimplementedError();
}
#override
bool shouldRebuild(covariant SliverChildDelegate oldDelegate) => true;
}
#diegoveloper I found this plugin. https://pub.dev/packages/flutter_sticky_header
I wish there is still an easier way to do it without a plugin. However, this plugin exactly solves the issue I am describing.
I've seen new flutter video and seen some interesting. (It's not typical sticky header or expandable list, so I don't know how to name it)
Video - watch from 0:20
Does anybody know how can I create such type of list with headers using SliverList?
One way is to create a CustomScrollView and pass a SliverAppBar pinned to true and a SliverFixedExtentList object with your Widgets.
Example:
List<Widget> _sliverList(int size, int sliverChildCount) {
var widgetList = <Widget>[];
for (int index = 0; index < size; index++)
widgetList
..add(SliverAppBar(
title: Text("Title $index"),
pinned: true,
))
..add(SliverFixedExtentList(
itemExtent: 50.0,
delegate:
SliverChildBuilderDelegate((BuildContext context, int index) {
return Container(
alignment: Alignment.center,
color: Colors.lightBlue[100 * (index % 9)],
child: Text('list item $index'),
);
}, childCount: sliverChildCount),
));
return widgetList;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Slivers"),
),
body: CustomScrollView(
slivers: _sliverList(50, 10),
),
);
}
SliverPersistentHeader is the more generic widget behind SliverAppBar that you can use.
SliverPersistentHeader(
delegate: SectionHeaderDelegate("Section B"),
pinned: true,
),
And the SectionHeaderDelegate can be implement with something like:
class SectionHeaderDelegate extends SliverPersistentHeaderDelegate {
final String title;
final double height;
SectionHeaderDelegate(this.title, [this.height = 50]);
#override
Widget build(context, double shrinkOffset, bool overlapsContent) {
return Container(
color: Theme.of(context).primaryColor,
alignment: Alignment.center,
child: Text(title),
);
}
#override
double get maxExtent => height;
#override
double get minExtent => height;
#override
bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) => false;
}