378 lines
13 KiB
Dart
378 lines
13 KiB
Dart
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(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<SliverFloatingBar>
|
|
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<RenderSliverFloatingPersistentHeader>());
|
|
}
|
|
|
|
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;
|
|
|
|
// // 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);
|
|
}
|
|
}
|