Skip to content

Instantly share code, notes, and snippets.

@HansMuller
Created May 3, 2023 21:24
Show Gist options
  • Save HansMuller/514b849bdb8c861ebb52eb992f6569a2 to your computer and use it in GitHub Desktop.
Save HansMuller/514b849bdb8c861ebb52eb992f6569a2 to your computer and use it in GitHub Desktop.
// 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