import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; class SliverFloatingBar extends StatefulWidget { /// Creates a material design app bar that can be placed in a [CustomScrollView]. /// /// The arguments [forceElevated], [primary], [floating], [pinned], [snap] /// and [automaticallyImplyLeading] must not be null. const SliverFloatingBar({ Key key, this.leading, this.automaticallyImplyLeading = true, this.title, this.trailing, this.elevation = 5.0, this.backgroundColor, this.floating = false, this.pinned = false, this.snap = false, }) : assert(floating || !snap, 'The "snap" argument only makes sense for floating app bars.'), super(key: key); /// A widget to display before the [title]. /// /// If this is null and [automaticallyImplyLeading] is set to true, the [AppBar] will /// imply an appropriate widget. For example, if the [AppBar] is in a [Scaffold] /// that also has a [Drawer], the [Scaffold] will fill this widget with an /// [IconButton] that opens the drawer. If there's no [Drawer] and the parent /// [Navigator] can go back, the [AppBar] will use a [BackButton] that calls /// [Navigator.maybePop]. final Widget leading; /// Controls whether we should try to imply the leading widget if null. /// /// If true and [leading] is null, automatically try to deduce what the leading /// widget should be. If false and [leading] is null, leading space is given to [title]. /// If leading widget is not null, this parameter has no effect. final bool automaticallyImplyLeading; /// The primary widget displayed in the appbar. /// /// Typically a [Text] widget containing a description of the current contents /// of the app. final Widget title; final Widget trailing; /// The z-coordinate at which to place this app bar when it is above other /// content. This controls the size of the shadow below the app bar. /// /// Defaults to 4, the appropriate elevation for app bars. /// /// If [forceElevated] is false, the elevation is ignored when the app bar has /// no content underneath it. For example, if the app bar is [pinned] but no /// content is scrolled under it, or if it scrolls with the content, then no /// shadow is drawn, regardless of the value of [elevation]. final double elevation; /// The color to use for the app bar's material. Typically this should be set /// along with [brightness], [iconTheme], [textTheme]. /// /// Defaults to [ThemeData.primaryColor]. final Color backgroundColor; /// Whether the app bar should become visible as soon as the user scrolls /// towards the app bar. /// /// Otherwise, the user will need to scroll near the top of the scroll view to /// reveal the app bar. /// /// If [snap] is true then a scroll that exposes the app bar will trigger an /// animation that slides the entire app bar into view. Similarly if a scroll /// dismisses the app bar, the animation will slide it completely out of view. /// /// ## Animated Examples /// /// The following animations show how the app bar changes its scrolling /// behavior based on the value of this property. /// /// * App bar with [floating] set to false: /// {@animation 476 400 https://flutter.github.io/assets-for-api-docs/assets/material/app_bar.mp4} /// * App bar with [floating] set to true: /// {@animation 476 400 https://flutter.github.io/assets-for-api-docs/assets/material/app_bar_floating.mp4} /// /// See also: /// /// * [SliverAppBar] for more animated examples of how this property changes the /// behavior of the app bar in combination with [pinned] and [snap]. final bool floating; /// Whether the app bar should remain visible at the start of the scroll view. /// /// The app bar can still expand and contract as the user scrolls, but it will /// remain visible rather than being scrolled out of view. /// /// ## Animated Examples /// /// The following animations show how the app bar changes its scrolling /// behavior based on the value of this property. /// /// * App bar with [pinned] set to false: /// {@animation 476 400 https://flutter.github.io/assets-for-api-docs/assets/material/app_bar.mp4} /// * App bar with [pinned] set to true: /// {@animation 476 400 https://flutter.github.io/assets-for-api-docs/assets/material/app_bar_pinned.mp4} /// /// See also: /// /// * [SliverAppBar] for more animated examples of how this property changes the /// behavior of the app bar in combination with [floating]. final bool pinned; /// If [snap] and [floating] are true then the floating app bar will "snap" /// into view. /// /// If [snap] is true then a scroll that exposes the floating app bar will /// trigger an animation that slides the entire app bar into view. Similarly if /// a scroll dismisses the app bar, the animation will slide the app bar /// completely out of view. /// /// Snapping only applies when the app bar is floating, not when the appbar /// appears at the top of its scroll view. /// /// ## Animated Examples /// /// The following animations show how the app bar changes its scrolling /// behavior based on the value of this property. /// /// * App bar with [snap] set to false: /// {@animation 476 400 https://flutter.github.io/assets-for-api-docs/assets/material/app_bar_floating.mp4} /// * App bar with [snap] set to true: /// {@animation 476 400 https://flutter.github.io/assets-for-api-docs/assets/material/app_bar_floating_snap.mp4} /// /// See also: /// /// * [SliverAppBar] for more animated examples of how this property changes the /// behavior of the app bar in combination with [pinned] and [floating]. final bool snap; @override _SliverFloatingBarState createState() => _SliverFloatingBarState(); } class _SliverFloatingBarState extends State with TickerProviderStateMixin { FloatingHeaderSnapConfiguration _snapConfiguration; void _updateSnapConfiguration() { if (widget.snap && widget.floating) { _snapConfiguration = FloatingHeaderSnapConfiguration( vsync: this, curve: Curves.easeOut, duration: const Duration(milliseconds: 200), ); } else { _snapConfiguration = null; } } @override void initState() { super.initState(); _updateSnapConfiguration(); } @override void didUpdateWidget(SliverFloatingBar oldWidget) { super.didUpdateWidget(oldWidget); if (widget.snap != oldWidget.snap || widget.floating != oldWidget.floating) _updateSnapConfiguration(); } @override Widget build(BuildContext context) { final double topPadding = MediaQuery.of(context).padding.top; final double collapsedHeight = (widget.pinned && widget.floating) ? topPadding : null; return MediaQuery.removePadding( context: context, removeBottom: true, child: SliverPersistentHeader( floating: widget.floating, pinned: widget.pinned, delegate: _SliverAppBarDelegate( leading: widget.leading, automaticallyImplyLeading: widget.automaticallyImplyLeading, title: widget.title, trailing: widget.trailing, elevation: widget.elevation, backgroundColor: widget.backgroundColor, floating: widget.floating, pinned: widget.pinned, snapConfiguration: _snapConfiguration, collapsedHeight: collapsedHeight, topPadding: topPadding, ), ), ); } } class _FloatingAppBar extends StatefulWidget { const _FloatingAppBar({Key key, this.child}) : super(key: key); final Widget child; @override _FloatingAppBarState createState() => _FloatingAppBarState(); } // A wrapper for the widget created by _SliverAppBarDelegate that starts and /// stops the floating appbar's snap-into-view or snap-out-of-view animation. class _FloatingAppBarState extends State<_FloatingAppBar> { ScrollPosition _position; @override void didChangeDependencies() { super.didChangeDependencies(); _position.isScrollingNotifier.removeListener(_isScrollingListener); _position = Scrollable.of(context).position; _position.isScrollingNotifier.addListener(_isScrollingListener); } @override void dispose() { _position.isScrollingNotifier.removeListener(_isScrollingListener); super.dispose(); } RenderSliverFloatingPersistentHeader _headerRenderer() { return context.findAncestorRenderObjectOfType( ); } void _isScrollingListener() { final RenderSliverFloatingPersistentHeader header = _headerRenderer(); if (_position.isScrollingNotifier.value) header.maybeStopSnapAnimation(_position.userScrollDirection); else header.maybeStartSnapAnimation(_position.userScrollDirection); } @override Widget build(BuildContext context) => widget.child; } class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { _SliverAppBarDelegate({ @required this.leading, @required this.automaticallyImplyLeading, @required this.title, @required this.trailing, @required this.elevation, @required this.backgroundColor, @required this.floating, @required this.pinned, @required this.snapConfiguration, @required this.collapsedHeight, @required this.topPadding, }); final Widget trailing; final bool automaticallyImplyLeading; final Color backgroundColor; final double elevation; final bool floating; final Widget leading; final bool pinned; final Widget title; final double collapsedHeight; final double topPadding; @override double get minExtent => collapsedHeight ?? (topPadding + kToolbarHeight); @override final FloatingHeaderSnapConfiguration snapConfiguration; @override double get maxExtent => math.max(topPadding + (kToolbarHeight), minExtent); @override bool shouldRebuild(covariant _SliverAppBarDelegate oldDelegate) { return leading != oldDelegate.leading || automaticallyImplyLeading != oldDelegate.automaticallyImplyLeading || title != oldDelegate.title || trailing != oldDelegate.trailing || elevation != oldDelegate.elevation || topPadding != oldDelegate.topPadding || collapsedHeight != oldDelegate.collapsedHeight || backgroundColor != oldDelegate.backgroundColor || pinned != oldDelegate.pinned || floating != oldDelegate.floating || snapConfiguration != oldDelegate.snapConfiguration; } @override String toString() { return ''; } @override Widget build( BuildContext context, double shrinkOffset, bool overlapsContent) { final double visibleMainHeight = maxExtent - shrinkOffset; // // Truth table for `toolbarOpacity`: // // pinned | floating | bottom != null || opacity // // ---------------------------------------------- // // 0 | 0 | 0 || fade // // 0 | 0 | 1 || fade // // 0 | 1 | 0 || fade // // 0 | 1 | 1 || fade // // 1 | 0 | 0 || 1.0 // // 1 | 0 | 1 || 1.0 // // 1 | 1 | 0 || 1.0 // // 1 | 1 | 1 || fade final double toolbarOpacity = !pinned || (floating) ? ((visibleMainHeight) / kToolbarHeight).clamp(0.0, 1.0) : 1.0; final Widget appBar = FlexibleSpaceBar.createSettings( minExtent: minExtent, maxExtent: maxExtent, currentExtent: math.max(minExtent, maxExtent - shrinkOffset), toolbarOpacity: toolbarOpacity, child: Container( padding: EdgeInsets.symmetric(horizontal: 12.0), child: SafeArea( child: Material( color: backgroundColor, borderRadius: BorderRadius.circular(8.0), elevation: elevation, child: ListTile( leading: leading ?? (Scaffold.of(context).hasDrawer && automaticallyImplyLeading ? IconButton( icon: Icon(Icons.menu), onPressed: () { Scaffold.of(context).openDrawer(); }, ) : null), title: title, trailing: trailing ?? (Scaffold.of(context).hasEndDrawer && automaticallyImplyLeading ? IconButton( icon: Icon(Icons.menu), onPressed: () { Scaffold.of(context).openEndDrawer(); }, ) : null), ), ), ), ), ); return Container(child: floating ? _FloatingAppBar(child: appBar) : appBar); } }