How to change pinned of Sliver Persistent Header - flutter

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.

Related

StreamBuilder not updating widget

i have a Widget that has an AppBar with a progress bar, and a PageView with 4 pages, when moving between pages i am increasing / decreasing the progress bar.
I'm trying to do all the logic in my ViewModel.
This is my ViewModel (omitted non relevant stuff):
class RegisterViewModel extends BaseViewModel with RegisterViewModelInputs, RegisterViewModelOutputs {
final StreamController _sexStreamController = StreamController<int>.broadcast();
final StreamController _progressBarController = StreamController<double>.broadcast();
final StreamController _currentIndexController = StreamController<int>.broadcast();
final StreamController _isBackEnabled = StreamController<bool>.broadcast();
double _progress = 0.25;
int _index = 0;
#override
setCurrentIndex(int index) {
currentIndex.add(index);
}
#override
increaseProgress() {
if (_progress <= 1.0) {
_progress += 0.25;
progress.add(_progress);
}
}
#override
decreaseProgress() {
if (_progress > 0) {
_progress -= 0.25;
progress.add(_progress);
}
}
#override
setIsBackEnabled(int index) {
_isBackEnabled.add(index > 0 ? true : false);
}
#override
nextPage() {
if (_index < 4) {
_index++;
increaseProgress();
setCurrentIndex(_index);
}
}
#override
previousPage() {
if (_index > 0) {
_index--;
decreaseProgress();
setCurrentIndex(_index);
}
}
#override
Sink get currentIndex => _currentIndexController.sink;
#override
Sink get progress => _progressBarController.sink;
#override
Sink get isBackEnabled => _isBackEnabled.sink;
#override
Stream<int> get outputCurrentIndex => _currentIndexController.stream.map((currentIndex) => currentIndex);
#override
Stream<double> get outputProgress => _progressBarController.stream.map((progress) => progress);
#override
Stream<bool> get outputIsBackEnabled => outputIsBackEnabled.map((isEnabled) => isEnabled);
}
And here is my View:
class RegisterView extends StatefulWidget {
const RegisterView({Key? key}) : super(key: key);
#override
_RegisterViewState createState() => _RegisterViewState();
}
class _RegisterViewState extends State<RegisterView> {
final RegisterViewModel _viewModel = getIt<RegisterViewModel>();
final PageController _pageController = PageController(initialPage: 0);
final FixedExtentScrollController _weightScrollController = FixedExtentScrollController(initialItem: 80);
final FixedExtentScrollController _ageScrollController = FixedExtentScrollController(initialItem: 13);
final FixedExtentScrollController _heightScrollController = FixedExtentScrollController(initialItem: 13);
#override
void initState() {
_bind();
super.initState();
}
#override
void dispose() {
_viewModel.dispose();
super.dispose();
}
_bind() {
_viewModel.start();
}
#override
Widget build(BuildContext context) {
_viewModel.outputCurrentIndex.listen((index) {
_pageController.animateToPage(index, duration: const Duration(milliseconds: 1000), curve: Curves.ease);
});
List<Widget> pagesList = [
SexPage(
onConfirm: (sex) {
_viewModel.setSex(sex);
_viewModel.nextPage();
},
),
AgePage(
scrollController: _ageScrollController,
),
WeightPage(scrollController: _weightScrollController),
HeightPage(scrollController: _heightScrollController),
];
return Scaffold(
backgroundColor: ColorManager.backgroundColor,
appBar: AppBar(
systemOverlayStyle: SystemUiOverlayStyle(
statusBarColor: ColorManager.backgroundColor,
statusBarBrightness: Brightness.dark,
statusBarIconBrightness: Brightness.dark,
),
centerTitle: true,
title: AppBarWidget(_pageController),
elevation: AppSize.s0,
),
body: PageView(
reverse: true,
controller: _pageController,
physics: NeverScrollableScrollPhysics(),
children: [...pagesList],
),
);
}
}
class AppBarWidget extends StatelessWidget {
final PageController pageController;
final RegisterViewModel _viewModel = getIt<RegisterViewModel>();
AppBarWidget(
this.pageController, {
Key? key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
_viewModel.outputCurrentIndex.listen((index) {
pageController.animateToPage(index, duration: const Duration(milliseconds: 1000), curve: Curves.ease);
});
return Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
flex: 1,
child: InkWell(
child: Text(
AppStrings.skip,
style: Theme.of(context).textTheme.labelMedium,
),
onTap: () => _viewModel.nextPage(),
),
),
Expanded(
flex: 4,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: AppPadding.p60),
child: Transform(
alignment: Alignment.center,
transform: Matrix4.rotationY(pi),
child: StreamBuilder<double>(
stream: _viewModel.outputProgress,
builder: (context, snapshot) {
return Progresso(
progress: snapshot.data ?? 0,
progressStrokeCap: StrokeCap.round,
backgroundStrokeCap: StrokeCap.round,
progressColor: ColorManager.primary,
backgroundColor: ColorManager.progressBarBackgroundGrey,
progressStrokeWidth: 10.0,
backgroundStrokeWidth: 10.0,
);
}),
),
),
),
StreamBuilder<int>(
stream: _viewModel.outputCurrentIndex,
builder: (context, snapshot) {
return Expanded(
flex: 1,
child: (snapshot.data ?? 0) > 0
? InkWell(
child: Row(
children: [
Text(
AppStrings.back,
style: Theme.of(context).textTheme.labelMedium,
),
Icon(
Icons.arrow_forward_ios,
color: ColorManager.subtitleGrey,
),
],
),
onTap: () => _viewModel.previousPage(),
)
: Container(),
);
}),
],
);
}
}
When i'm calling _viewModel.previousPage() & _viewModel.previousPage()` from the AppBarWidget, the progress bar view is updated, and there is a scroll animation to the next page.
But for some reason if the onConfirm callback:
onConfirm: (sex) {
_viewModel.setSex(sex);
_viewModel.nextPage();
}
is called from within SexPage, the scroll animation is working, but the progress bar view and the isBackEnabled is not updating.
I have checked and a new value is being added to the _progressBarController sink, but for some reason the StreamBuilder does not receive it? same for the isBackEnabled stream..
What am i doing wrong?
And another question i have is where should I listen to the outputCurrentIndex stream, and call _pageController.animateToPage()?
Apparently i had an issue with my Dependency Injection.
I'm using get_it and i used registerFactory, instead of registerLazySingleton.
Which probably made me have 2 separate ViewModels in each widget.

Flutter Scroll view to focused widget on a column

I'm developing an app for Android TV, and use DPAD navigation.
I have multiple widgets inside a column. when i navigate to a widget which is outside the view, the widget/view is not moving to reflect the selected widget.
// ignore_for_file: avoid_print
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
static const String _title = 'Flutter Code Sample';
#override
Widget build(BuildContext context) {
return MaterialApp(
title: _title,
home: Scaffold(
appBar: AppBar(title: const Text(_title)),
body: const MyStatelessWidget(),
),
);
}
}
class MyStatelessWidget extends StatelessWidget {
const MyStatelessWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final TextTheme textTheme = Theme.of(context).textTheme;
return DefaultTextStyle(
style: textTheme.headline4!,
child: ChangeNotifierProvider<SampleNotifier>(
create: (context) => SampleNotifier(), child: const CardHolder()),
);
}
}
class CardHolder extends StatefulWidget {
const CardHolder({Key? key}) : super(key: key);
#override
_CardHolderState createState() => _CardHolderState();
}
class _CardHolderState extends State<CardHolder> {
late FocusNode _focusNode;
late FocusAttachment _focusAttachment;
#override
void initState() {
super.initState();
_focusNode = FocusNode(debugLabel: "traversal_node");
_focusAttachment = _focusNode.attach(context, onKey: _handleKeyPress);
_focusNode.requestFocus();
}
#override
Widget build(BuildContext context) {
_focusAttachment.reparent();
return Focus(
focusNode: _focusNode,
autofocus: true,
onKey: _handleKeyPress,
child: Consumer<SampleNotifier>(
builder: (context, models, child) {
int listSize = Provider.of<SampleNotifier>(context).listSize;
return SingleChildScrollView(
child: SampleRow(cat: "Test", models: models.modelList),
);
},
),
);
}
KeyEventResult _handleKeyPress(FocusNode node, RawKeyEvent event) {
if (event is RawKeyDownEvent) {
print("t:FocusNode: ${node.debugLabel} event: ${event.logicalKey}");
if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
Provider.of<SampleNotifier>(context, listen: false).moveRight();
return KeyEventResult.handled;
} else if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
Provider.of<SampleNotifier>(context, listen: false).moveLeft();
return KeyEventResult.handled;
}
}
// debugDumpFocusTree();
return KeyEventResult.ignored;
}
}
class SampleCard extends StatefulWidget {
final int number;
final SampleModel model;
final bool focused;
const SampleCard(
{required this.number,
required this.focused,
required this.model,
Key? key})
: super(key: key);
#override
_SampleCardState createState() => _SampleCardState();
}
class _SampleCardState extends State<SampleCard> {
late Color _color;
#override
void initState() {
super.initState();
_color = Colors.red.shade900;
}
#override
void dispose() {
super.dispose();
}
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: widget.focused
? Container(
width: 150,
height: 300,
color: Colors.white,
child: Center(
child: Text(
"${widget.model.text} ${widget.model.num}",
style: TextStyle(color: _color),
),
),
)
: Container(
width: 150,
height: 300,
color: Colors.black,
child: Center(
child: Text(
"${widget.model.text} ${widget.model.num}",
style: TextStyle(color: _color),
),
),
),
);
}
}
class SampleRow extends StatelessWidget {
final String cat;
final List<SampleModel> models;
SampleRow({Key? key, required this.cat, required this.models}) : super(key: key);
#override
Widget build(BuildContext context) {
final int selectedIndex =
Provider.of<SampleNotifier>(context).selectedIndex;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.only(left: 16, bottom: 8),
),
models.isNotEmpty
? SizedBox(
height: 200,
child: ListView.custom(
padding: const EdgeInsets.all(8),
scrollDirection: Axis.horizontal,
childrenDelegate: SliverChildBuilderDelegate(
(context, index) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: SampleCard(
focused: index == selectedIndex,
model: models[index],
number: index,
),
),
childCount: models.length,
findChildIndexCallback: _findChildIndex,
),
),
)
: SizedBox(
height: 200,
child: Container(
color: Colors.teal,
),
)
],
);
}
int _findChildIndex(Key key) => models.indexWhere((model) =>
"$cat-${model.text}_${model.num}" == (key as ValueKey<String>).value);
}
class SampleNotifier extends ChangeNotifier {
final List<SampleModel> _models = [
SampleModel(0, "zero"),
SampleModel(1, "one"),
SampleModel(2, "two"),
SampleModel(3, "three"),
SampleModel(4, "four"),
SampleModel(5, "five"),
SampleModel(6, "six"),
SampleModel(7, "seven"),
SampleModel(8, "eight"),
SampleModel(9, "nine"),
SampleModel(10, "ten")
];
int _selectedIndex = 0;
List<SampleModel> get modelList => _models;
int get selectedIndex => _selectedIndex;
int get listSize => _models.length;
void moveRight() {
if (_selectedIndex < _models.length - 1) {
_selectedIndex = _selectedIndex + 1;
}
notifyListeners();
}
void moveLeft() {
if (_selectedIndex > 0) {
_selectedIndex = _selectedIndex - 1;
}
notifyListeners();
}
}
class SampleModel {
int num;
String text;
SampleModel(this.num, this.text);
}
I need a way to move/scroll the widget into view. Is there any way to do this, using the DPAD navigation on android tv
Here is the gist
You could use the scrollable_positioned_list package.
Instead of a ListView.custom which scrolls based on pixels, this widgets its based on index:
final ItemScrollController itemScrollController = ItemScrollController();
ScrollablePositionedList.builder(
itemCount: 500,
itemBuilder: (context, index) => Text('Item $index'),
itemScrollController: itemScrollController,
itemPositionsListener: itemPositionsListener,
);
So you could maintain an index of the current scroll position and on DPAD press just :
itemScrollController.jumpTo(index: currentItem);
setState((){currentItem++;})

How to mask-out the overlaped section, visible through the "translucent header sliver" in the NestedScrollView?

The following code yields a scrollable list together with a "translucent pinned sliver header".
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return [
SliverPersistentHeader(
delegate: _SliverPersistentHeaderDelegate(),
pinned: true,
),
];
},
body: ListView.builder(
itemBuilder: (context, index) {
return ListTile(
title: Container(
color: Colors.amber.withOpacity(0.3),
child: Text('Item $index'),
),
);
},
),
),
),
);
}
}
class _SliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {
#override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
return Container(
color: Colors.blue.withOpacity(0.75),
child: Placeholder(),
);
}
#override double get maxExtent => 300;
#override double get minExtent => 200;
#override bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) => true;
}
It's all good; except, I need the "header" to be transparent, but having it translucent, causes the underneathed list-items to get revealed (as per the screenshot below).
So, how to "mask-out" the "list items" that are visible through the "translucent header"?
How about using CustomClipper for List itself? Because the list height is dynamic during scrolling, the clip height must be calculated dynamically. So I pass the clipHeight into the custom clipper.
To get the clipHeight, I use MediaQuery.of(context).size.height - header height. So I create another class to get this value.
...
body: CustomWidget (
child: ListView.builder(
...
class CustomWidget extends StatelessWidget {
final Widget child;
CustomWidget({this.child,Key key}):super(key:key);
#override
Widget build(BuildContext context) {
return ClipRect(
clipper: MyCustomClipper(clipHeight: MediaQuery.of(context).size.height-200),
child: child,
);
}
}
class MyCustomClipper extends CustomClipper<Rect>{
final double clipHeight;
MyCustomClipper({this.clipHeight});
#override
getClip(Size size) {
double top = math.max(size.height - clipHeight,0) ;
Rect rect = Rect.fromLTRB(0.0, top, size.width, size.height);
return rect;
}
#override
bool shouldReclip(CustomClipper oldClipper) {
return false;
}
}
Pinned SliverPersistentHeader works like "CSS position: absolute".
So your body widget doesn't know that something is upon it.
One of the option is to not to use the SliverPersistentHeader.
import 'package:flutter/material.dart';
import 'dart:math' as math;
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
ScrollController controller;
#override
void initState() {
currentHeight = _maxExtent;
controller = ScrollController();
controller.addListener(() {
_updateHeaderHeight();
});
super.initState();
}
_updateHeaderHeight() {
double offset = controller.offset;
if (offset <= _maxExtent - _minExtent) {
setState(() {
currentHeight = math.max(_maxExtent - offset, _minExtent);
});
}
}
double currentHeight;
final double _maxExtent = 300;
final double _minExtent = 200;
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: DecoratedBox(
// only to prove transparency
decoration: BoxDecoration(
image: DecorationImage(
colorFilter: ColorFilter.mode(Colors.white, BlendMode.color),
image: NetworkImage(
'https://picsum.photos/720/1280',
),
fit: BoxFit.cover,
),
),
child: Stack(
children: [
Header(currentHeight: currentHeight),
Padding(
padding: EdgeInsets.only(top: currentHeight),
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.blueAccent),
),
child: ListView.builder(
controller: controller,
itemBuilder: (context, index) {
return ListTile(
title: Container(
color: Colors.amber.withOpacity(0.3),
child: Text('Item $index'),
),
);
},
),
),
),
],
),
),
),
);
}
}
class Header extends StatelessWidget {
const Header({Key key, this.currentHeight}) : super(key: key);
final double currentHeight;
#override
Widget build(BuildContext context) {
return Container(
height: currentHeight,
color: Colors.blue.withOpacity(0.75),
child: Placeholder(),
);
}
}

Animate widget alignment

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;
}
}

Sticky headers on SliverList

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;
}