Created
May 3, 2023 21:24
-
-
Save HansMuller/514b849bdb8c861ebb52eb992f6569a2 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This version of the SliverAppBar iOS large title snap demo is similar to | |
// snap.dart but uses a pair of SliverPersistentHeaders for the title | |
// and large title. This makes the layout simpler and obviates the ClipBox | |
// widget. | |
// | |
// Problems: | |
// - Same as snap.dart per the ScrollController and desktop. | |
// - To package this approach up as a single Sliver, SliverMainAxisGroup is needed. | |
// - It seems like the stretchConfiguration property should be on LargeTitleBar, since that's | |
// the one we want to stretch (plus scaling upon stretching...) | |
// | |
import 'package:flutter/material.dart'; | |
import 'package:flutter/rendering.dart'; | |
import 'package:flutter/scheduler.dart'; | |
// List content for the demo | |
class ItemListView extends StatelessWidget { | |
const ItemListView({ | |
super.key, | |
required this.startColor, | |
required this.endColor, | |
}); | |
final Color startColor; | |
final Color endColor; | |
static const int itemCount = 50; | |
@override | |
Widget build(BuildContext context) { | |
return SliverPadding( | |
padding: EdgeInsets.symmetric(horizontal: 8), | |
sliver: SliverList( | |
delegate: SliverChildBuilderDelegate( | |
(BuildContext context, int index) { | |
return Card( | |
color: Color.lerp(startColor, endColor, index / itemCount)!, | |
child: ListTile( | |
textColor: Colors.white, | |
title: Row( | |
children: <Widget>[ | |
Text('Page $index'), | |
Expanded(child: SizedBox()), | |
const Icon(Icons.navigate_next, color: Colors.white), | |
], | |
), | |
), | |
); | |
}, | |
childCount: itemCount, | |
), | |
), | |
); | |
} | |
} | |
class TitleBar extends SliverPersistentHeaderDelegate { | |
const TitleBar({ | |
required this.topPadding, | |
required this.height, | |
required this.titleOpacity, | |
}); | |
final double topPadding; | |
final double height; | |
final double titleOpacity; | |
@override | |
double get minExtent => height + topPadding; | |
@override | |
double get maxExtent => height + topPadding; | |
@override | |
OverScrollHeaderStretchConfiguration get stretchConfiguration => OverScrollHeaderStretchConfiguration(); | |
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { | |
return Container( | |
color: Colors.white, | |
padding: EdgeInsets.only(top: topPadding), | |
alignment: Alignment.center, | |
child: AnimatedOpacity( | |
opacity: titleOpacity, | |
duration: const Duration(milliseconds: 300), | |
child: Text('Settings', style: TextStyle(fontSize: 17)), | |
), | |
); | |
} | |
@override | |
bool shouldRebuild(TitleBar oldDelegate) { | |
return topPadding != oldDelegate.topPadding | |
|| height != oldDelegate.height | |
|| titleOpacity != oldDelegate.titleOpacity; | |
} | |
} | |
class LargeTitleBar extends SliverPersistentHeaderDelegate { | |
const LargeTitleBar({ | |
required this.height, | |
}); | |
final double height; | |
@override | |
double get minExtent => height; | |
@override | |
double get maxExtent => height; | |
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { | |
return Container( | |
color: Colors.white, | |
padding: const EdgeInsetsDirectional.only(start: 16, bottom: 8), | |
child: const Text('Settings', style: TextStyle(fontSize: 34)), | |
); | |
} | |
@override | |
bool shouldRebuild(LargeTitleBar oldDelegate) { | |
return height != oldDelegate.height; | |
} | |
} | |
class SnapDemo extends StatefulWidget { | |
const SnapDemo({ super.key }); | |
@override | |
State<SnapDemo> createState() => _SnapDemoState(); | |
} | |
class _SnapDemoState extends State<SnapDemo> with SingleTickerProviderStateMixin { | |
late final ScrollController scrollController; | |
final double collapsedHeight = 56; | |
final double expandedHeight = 128; | |
double titleOpacity = 0; | |
@override | |
void initState() { | |
super.initState(); | |
scrollController = ScrollController(); | |
} | |
@override | |
void dispose() { | |
scrollController.dispose(); | |
super.dispose(); | |
} | |
bool handleScrollNotification(ScrollNotification notification) { | |
final double offset = scrollController.position.pixels; | |
final double newTitleOpacity = offset >= 72 ? 1 : 0; | |
if (newTitleOpacity != titleOpacity) { | |
setState(() { | |
titleOpacity = newTitleOpacity; | |
}); | |
} | |
if (notification is ScrollEndNotification) { | |
final double expansionHeight = expandedHeight - collapsedHeight; | |
if (offset > 0 && offset < expansionHeight) { | |
SchedulerBinding.instance.addPostFrameCallback((Duration duration) { | |
scrollController.position.animateTo( | |
(offset > expansionHeight / 2) ? expansionHeight : 0, | |
duration: Duration(milliseconds: 500), | |
curve: Curves.easeInOut | |
); | |
}); | |
} | |
} | |
return true; | |
} | |
@override | |
Widget build(BuildContext context) { | |
final ColorScheme colorScheme = Theme.of(context).colorScheme; | |
final double topPadding = MediaQuery.paddingOf(context).top; | |
return Scaffold( | |
body: NotificationListener<ScrollNotification>( | |
onNotification: handleScrollNotification, | |
child: CustomScrollView( | |
controller: scrollController, | |
slivers: <Widget>[ | |
SliverPersistentHeader( | |
pinned: true, | |
delegate: TitleBar( | |
topPadding: topPadding, | |
height: collapsedHeight, | |
titleOpacity: titleOpacity, | |
), | |
), | |
SliverPersistentHeader( | |
floating: true, | |
delegate: LargeTitleBar( | |
height: expandedHeight - collapsedHeight, | |
), | |
), | |
ItemListView( | |
startColor: Colors.yellow, | |
endColor: Colors.green, | |
), | |
], | |
), | |
), | |
); | |
} | |
} | |
class SnapDemoApp extends StatelessWidget { | |
const SnapDemoApp({ super.key }); | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
theme: ThemeData( | |
useMaterial3: true, | |
), | |
debugShowCheckedModeBanner: false, | |
home: const SnapDemo(), | |
); | |
} | |
} | |
void main() { | |
runApp(const SnapDemoApp()); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment