Last active
August 2, 2023 16:39
-
-
Save HansMuller/6d272ec6712a045ab2121bc8fb324797 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
// First AppBar Parts example: iOS Settings app layout | |
// https://gist.github.com/HansMuller/6d272ec6712a045ab2121bc8fb324797 | |
import 'package:flutter/material.dart'; | |
// The pinned item at the top of the list. This is an implicitly | |
// animated widget: when the opacity changes the title and divider | |
// fade in or out. | |
class TitleBar extends StatelessWidget { | |
const TitleBar({ super.key, required this.opacity, required this.child }); | |
final double opacity; | |
final Widget child; | |
@override | |
Widget build(BuildContext context) { | |
final ThemeData theme = Theme.of(context); | |
final ColorScheme colorScheme = theme.colorScheme; | |
return AnimatedContainer( | |
duration: const Duration(milliseconds: 1000), | |
decoration: ShapeDecoration( | |
color: colorScheme.background, | |
shape: LinearBorder.bottom( | |
side: BorderSide( | |
color: opacity == 0 ? colorScheme.background : colorScheme.outline, | |
), | |
), | |
), | |
alignment: Alignment.center, | |
child: AnimatedOpacity( | |
opacity: opacity, | |
duration: const Duration(milliseconds: 1000), | |
child: child, | |
), | |
); | |
} | |
} | |
// The second item in the list. It scrolls normally. When it has scrolled | |
// out of view behind the first, pinned, TitleBar item, the TitleBar fades in. | |
class TitleItem extends StatelessWidget { | |
const TitleItem({ super.key, required this.child }); | |
final Widget child; | |
@override | |
Widget build(BuildContext context) { | |
return Container( | |
alignment: AlignmentDirectional.bottomStart, | |
padding: const EdgeInsets.symmetric(vertical: 8), | |
child: child, | |
); | |
} | |
} | |
// A placeholder list of 50 items. They'll appear after the TitleItem. | |
class ItemList extends StatelessWidget { | |
const ItemList({ | |
super.key, | |
required this.startColor, | |
required this.endColor, | |
this.itemCount = 50, | |
}); | |
final Color startColor; | |
final Color endColor; | |
final int itemCount; | |
@override | |
Widget build(BuildContext context) { | |
return SliverList( | |
delegate: SliverChildBuilderDelegate( | |
(BuildContext context, int index) { | |
return Card( | |
color: Color.lerp(startColor, endColor, index / itemCount)!, | |
child: ListTile( | |
textColor: Colors.white, | |
title: Text('Item $index'), | |
), | |
); | |
}, | |
childCount: itemCount, | |
), | |
); | |
} | |
} | |
// When the TitleItem scrolls underneath TitleBar, fade in the | |
// TitleBar. Ideally that would happen when the TitleItem had scrolled | |
// completely out of view. In this case we change our opacity from 0 | |
// to 1 when the TitleItem has scrolled as far as this TitleItem's height, | |
// because there's no convenient way to tell if the TitleItem has | |
// scrolled completely underneath the TitleBar. | |
class TitleBarSliverDelegate extends SliverPersistentHeaderDelegate { | |
const TitleBarSliverDelegate({ | |
required this.child, | |
required this.height, | |
}); | |
final Widget child; | |
final double height; | |
@override | |
double get minExtent => height; | |
@override | |
double get maxExtent => height; | |
@override | |
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { | |
return SizedBox( | |
height: height, | |
child: TitleBar( | |
opacity: shrinkOffset < height ? 0 : 1, | |
child: child, | |
), | |
); | |
} | |
@override | |
bool shouldRebuild(TitleBarSliverDelegate oldDelegate) { | |
return height != oldDelegate.height || child != oldDelegate.child; | |
} | |
} | |
class AppBarParts extends StatelessWidget { | |
const AppBarParts({ super.key }); | |
@override | |
Widget build(BuildContext context) { | |
const EdgeInsets horizontalPadding = EdgeInsets.symmetric(horizontal: 8); | |
final TextTheme textTheme = Theme.of(context).textTheme; | |
return Scaffold( | |
body: SafeArea( | |
child: CustomScrollView( | |
slivers: <Widget>[ | |
// The TitleBar item is not padded because it's divider must span | |
// the entire CustomScrollView. | |
SliverPersistentHeader( | |
pinned: true, | |
delegate: TitleBarSliverDelegate( | |
height: 56, | |
child: Text('Settings', style: textTheme.titleMedium), | |
), | |
), | |
SliverPadding( | |
padding: horizontalPadding, | |
sliver: SliverToBoxAdapter( | |
child: TitleItem( | |
child: Text('Settings', style: textTheme.headlineLarge), | |
), | |
), | |
), | |
const SliverPadding( | |
padding: horizontalPadding, | |
sliver: ItemList( | |
startColor: Colors.blue, | |
endColor: Colors.red, | |
), | |
), | |
], | |
), | |
), | |
); | |
} | |
} | |
class AppBarPartsApp extends StatelessWidget { | |
const AppBarPartsApp({ super.key }); | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
theme: ThemeData(useMaterial3: true), | |
home: const AppBarParts(), | |
); | |
} | |
} | |
void main() { | |
runApp(const AppBarPartsApp()); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment