import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/foundation.dart' show TargetPlatform;
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(automaticallyImplyLeading != null),
assert(floating != null),
assert(pinned != null),
assert(snap != null),
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;
// 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();
if (_position != null)
_position.isScrollingNotifier.removeListener(_isScrollingListener);
_position = Scrollable.of(context)?.position;
if (_position != null)
_position.isScrollingNotifier.addListener(_isScrollingListener);
}
void _isScrollingListener() {
if (_position == null) return;
// When a scroll stops, then maybe snap the appbar into view.
// Similarly, when a scroll starts, then maybe stop the snap animation.
final RenderSliverFloatingPersistentHeader header = _headerRenderer();
if (_position.isScrollingNotifier.value)
header?.maybeStopSnapAnimation(_position.userScrollDirection);
else
header?.maybeStartSnapAnimation(_position.userScrollDirection);
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);
import 'dart:math' as math;
import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/foundation.dart' show TargetPlatform;
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(automaticallyImplyLeading != null), assert(floating != null), assert(pinned != null), assert(snap != null), 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();
}
@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;
} }
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(); if (_position != null) _position.isScrollingNotifier.removeListener(_isScrollingListener); _position = Scrollable.of(context)?.position; if (_position != null) _position.isScrollingNotifier.addListener(_isScrollingListener); }
@override void dispose() { if (_position != null) _position.isScrollingNotifier.removeListener(_isScrollingListener); super.dispose(); }
RenderSliverFloatingPersistentHeader _headerRenderer() { return context.ancestorRenderObjectOfType( const TypeMatcher());
}
void _isScrollingListener() { if (_position == null) return;
}
@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; var platform = Theme.of(context).platform; // // 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;} }