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();
_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();
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;
// 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);
}
@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;
final Widget appBar = FlexibleSpaceBar.createSettings(
minExtent: minExtent,
maxExtent: maxExtent,
currentExtent: math.max(minExtent, maxExtent - shrinkOffset),
toolbarOpacity: toolbarOpacity,
child: Container(
// margin: const EdgeInsets.only(top: 10.0),
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),
),
),
),
transform: platform == TargetPlatform.iOS
? Matrix4.translationValues(0.0, 0.0, 0.0)
: Matrix4.translationValues(0.0, 8.0, 0.0),
),
);
return Container(child: floating ? _FloatingAppBar(child: appBar) : appBar);
}
}