Created
August 23, 2023 01:33
-
-
Save HansMuller/cdc44523ce64b09b5c8b0be2cbccd988 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
| // Settings app bar demo with auto-scroll and new coordinator API. | |
| // This demo must be run in a simulator or on a mobile device | |
| import 'dart:math' as math; | |
| import 'package:flutter/foundation.dart'; | |
| import 'package:flutter/material.dart'; | |
| import 'package:flutter/rendering.dart'; | |
| import 'package:flutter/scheduler.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), | |
| padding: const EdgeInsets.symmetric(vertical: 12), | |
| 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 SliverList of 50 items. | |
| 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, | |
| ), | |
| ); | |
| } | |
| } | |
| class SliverLayoutInfo { | |
| const SliverLayoutInfo({ | |
| required this.constraints, | |
| required this.geometry | |
| }); | |
| final SliverConstraints constraints; | |
| final SliverGeometry geometry; | |
| } | |
| class SliverCoordinatorData { | |
| final Map<Object, SliverLayoutInfo> _idToInfo = <Object, SliverLayoutInfo>{}; | |
| void put<T extends SliverLayoutInfo>(Object id, T info) { | |
| _idToInfo[id] = info; | |
| } | |
| T? get<T extends SliverLayoutInfo>(Object id) { | |
| return _idToInfo[id] as T?; | |
| } | |
| void clear() => _idToInfo.clear(); | |
| } | |
| class _SliverCoordinatorScope extends InheritedWidget { | |
| const _SliverCoordinatorScope({ required this.data, required super.child }); | |
| final SliverCoordinatorData data; | |
| @override | |
| bool updateShouldNotify(_SliverCoordinatorScope oldWidget) => false; | |
| } | |
| typedef SliverCoordinatorCallback = void Function(ScrollNotification notification, SliverCoordinatorData data); | |
| class SliverCoordinator extends StatefulWidget { | |
| const SliverCoordinator({ super.key, required this.callback, required this.child }); | |
| final Widget child; | |
| final SliverCoordinatorCallback callback; | |
| @override | |
| State<SliverCoordinator> createState() => _SliverCoordinatorState(); | |
| static SliverCoordinatorData of(BuildContext context) { | |
| final _SliverCoordinatorScope? scope = context.dependOnInheritedWidgetOfExactType<_SliverCoordinatorScope>(); | |
| return scope!.data; | |
| } | |
| } | |
| class _SliverCoordinatorState extends State<SliverCoordinator> { | |
| SliverCoordinatorData data = SliverCoordinatorData(); | |
| bool handleScrollNotification(ScrollNotification notification) { | |
| if (notification is ScrollUpdateNotification) { | |
| data.clear(); | |
| } | |
| // The callback runs after the descendant CustomScollView's viewport | |
| // has been laid out and the SliverLayoutInfo object has been updated. | |
| SchedulerBinding.instance.addPostFrameCallback((Duration duration) { | |
| widget.callback(notification, data); | |
| }); | |
| return true; | |
| } | |
| @override | |
| Widget build(BuildContext context) { | |
| return NotificationListener<ScrollNotification>( | |
| onNotification: handleScrollNotification, | |
| child: _SliverCoordinatorScope( | |
| data: data, | |
| child: widget.child, | |
| ), | |
| ); | |
| } | |
| } | |
| class CoordinatedSliver extends SingleChildRenderObjectWidget { | |
| const CoordinatedSliver({ | |
| super.key, | |
| super.child, | |
| }); | |
| @override | |
| RenderObject createRenderObject(BuildContext context) { | |
| return RenderCoordinatedSliver( | |
| id: this, | |
| data: SliverCoordinator.of(context), | |
| ); | |
| } | |
| @override | |
| void updateRenderObject(BuildContext context, RenderCoordinatedSliver renderObject) { | |
| renderObject.id = this; | |
| renderObject.data = SliverCoordinator.of(context); | |
| } | |
| SliverLayoutInfo? getLayoutInfo(SliverCoordinatorData data) { | |
| return data.get<SliverLayoutInfo>(this); | |
| } | |
| } | |
| class RenderCoordinatedSliver extends RenderProxySliver { | |
| RenderCoordinatedSliver({ | |
| required this.id, | |
| required this.data, | |
| RenderSliver? child, | |
| }) : super(child); | |
| Object id; | |
| SliverCoordinatorData data; | |
| @override | |
| void performLayout() { | |
| super.performLayout(); | |
| data.put<SliverLayoutInfo>(id, SliverLayoutInfo( | |
| constraints: constraints, | |
| geometry: geometry!, | |
| )); | |
| } | |
| } | |
| class PinnedHeaderSliver extends SingleChildRenderObjectWidget { | |
| const PinnedHeaderSliver({ | |
| super.key, | |
| super.child, | |
| }); | |
| @override | |
| RenderPinnedHeaderSliver createRenderObject(BuildContext context) { | |
| return RenderPinnedHeaderSliver(); | |
| } | |
| } | |
| class RenderPinnedHeaderSliver extends RenderSliverSingleBoxAdapter { | |
| RenderPinnedHeaderSliver({ super.child }); | |
| double get childExtent { | |
| if (child == null) { | |
| return 0.0; | |
| } | |
| assert(child!.hasSize); | |
| return switch (constraints.axis) { | |
| Axis.vertical => child!.size.height, | |
| Axis.horizontal => child!.size.width, | |
| }; | |
| } | |
| @override | |
| double childMainAxisPosition(covariant RenderObject child) => 0; | |
| @override | |
| void performLayout() { | |
| final SliverConstraints constraints = this.constraints; | |
| child?.layout(constraints.asBoxConstraints(), parentUsesSize: true); | |
| final double layoutExtent = clampDouble(childExtent - constraints.scrollOffset, 0, constraints.remainingPaintExtent); | |
| final double paintExtent = math.min(childExtent, constraints.remainingPaintExtent - constraints.overlap); | |
| geometry = SliverGeometry( | |
| scrollExtent: childExtent, | |
| paintOrigin: constraints.overlap, | |
| paintExtent: paintExtent, | |
| layoutExtent: layoutExtent, | |
| maxPaintExtent: childExtent, | |
| maxScrollObstructionExtent: childExtent, | |
| cacheExtent: calculateCacheOffset(constraints, from: 0.0, to: childExtent), | |
| hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity. | |
| ); | |
| } | |
| } | |
| class AppBarParts extends StatefulWidget { | |
| const AppBarParts({ super.key }); | |
| @override | |
| State<AppBarParts> createState() => _AppBarPartsState(); | |
| } | |
| class _AppBarPartsState extends State<AppBarParts> { | |
| late final ScrollController scrollController; | |
| late CoordinatedSliver titleBar; | |
| late CoordinatedSliver titleItem; | |
| double titleBarOpacity = 0; | |
| @override | |
| void initState() { | |
| super.initState(); | |
| scrollController = ScrollController(); | |
| } | |
| @override | |
| void dispose() { | |
| scrollController.dispose(); | |
| super.dispose(); | |
| } | |
| @override | |
| Widget build(BuildContext context) { | |
| final TextTheme textTheme = Theme.of(context).textTheme; | |
| const EdgeInsets horizontalPadding = EdgeInsets.symmetric(horizontal: 8); | |
| return Scaffold( | |
| body: SafeArea( | |
| child: Padding( | |
| padding: const EdgeInsets.symmetric(horizontal: 4), | |
| child: SliverCoordinator( | |
| callback: (ScrollNotification notification, SliverCoordinatorData data) { | |
| SliverLayoutInfo? titleBarInfo = titleBar.getLayoutInfo(data); | |
| SliverLayoutInfo? titleItemInfo = titleItem.getLayoutInfo(data); | |
| if (titleBarInfo == null || titleItemInfo == null) { | |
| return; | |
| } | |
| final double scrollOffset = titleBarInfo.constraints.scrollOffset; | |
| final double titleItemExtent = titleItemInfo.geometry.scrollExtent; | |
| if (notification is ScrollEndNotification) { | |
| if (scrollOffset > 0 && scrollOffset < titleItemExtent) { | |
| scrollController.position.animateTo( | |
| scrollOffset >= titleItemExtent / 2 ? titleItemExtent : 0, | |
| duration: const Duration(milliseconds: 300), | |
| curve: Curves.easeInOut | |
| ); | |
| } | |
| } | |
| final double opacity = scrollOffset >= titleItemExtent ? 1 : 0; | |
| if (opacity != titleBarOpacity) { | |
| setState(() { | |
| titleBarOpacity = opacity; | |
| }); | |
| } | |
| }, | |
| child: CustomScrollView( | |
| controller: scrollController, | |
| slivers: <Widget>[ | |
| titleBar = CoordinatedSliver( | |
| child: PinnedHeaderSliver( | |
| child: TitleBar( | |
| opacity: titleBarOpacity, | |
| child: Text('Settings', style: textTheme.titleMedium), | |
| ), | |
| ), | |
| ), | |
| SliverPadding( | |
| padding: horizontalPadding, | |
| sliver: titleItem = CoordinatedSliver( | |
| child: SliverToBoxAdapter( | |
| child: Container( | |
| color: Colors.yellow, | |
| child: TitleItem( | |
| child: Text('Settings', style: textTheme.displayLarge!.copyWith(fontSize: 72)), | |
| ), | |
| ), | |
| ), | |
| ), | |
| ), | |
| 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