138 lines
4.1 KiB
Dart
138 lines
4.1 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import 'common.dart';
|
|
|
|
/// Builds header content for a given collapse progress: 0 fully open, 1 fully
|
|
/// collapsed.
|
|
typedef CollapsingHeaderBuilder =
|
|
Widget Function(BuildContext context, double t);
|
|
|
|
/// The pinned brand header used by the main tabs.
|
|
///
|
|
/// It stays on screen while the content scrolls beneath it, and shrinks from
|
|
/// [expandedHeight] to [collapsedHeight] on the way. Pinning a tall header at
|
|
/// full size would cost a quarter of a phone screen for the whole session;
|
|
/// collapsing keeps the context and gives the space back.
|
|
class KrowSliverHeader extends StatelessWidget {
|
|
const KrowSliverHeader({
|
|
super.key,
|
|
required this.expandedHeight,
|
|
required this.collapsedHeight,
|
|
required this.builder,
|
|
});
|
|
|
|
/// Heights exclude the status bar, which is added for you.
|
|
final double expandedHeight;
|
|
final double collapsedHeight;
|
|
final CollapsingHeaderBuilder builder;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final top = MediaQuery.paddingOf(context).top;
|
|
return SliverPersistentHeader(
|
|
pinned: true,
|
|
delegate: _HeaderDelegate(
|
|
minHeight: collapsedHeight + top,
|
|
maxHeight: expandedHeight + top,
|
|
topInset: top,
|
|
builder: builder,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _HeaderDelegate extends SliverPersistentHeaderDelegate {
|
|
_HeaderDelegate({
|
|
required this.minHeight,
|
|
required this.maxHeight,
|
|
required this.topInset,
|
|
required this.builder,
|
|
});
|
|
|
|
final double minHeight;
|
|
final double maxHeight;
|
|
final double topInset;
|
|
final CollapsingHeaderBuilder builder;
|
|
|
|
@override
|
|
double get minExtent => minHeight;
|
|
|
|
@override
|
|
double get maxExtent => maxHeight;
|
|
|
|
@override
|
|
Widget build(BuildContext context, double shrinkOffset, bool overlaps) {
|
|
final range = maxHeight - minHeight;
|
|
final t = range <= 0 ? 1.0 : (shrinkOffset / range).clamp(0.0, 1.0);
|
|
return BrandSurface(
|
|
child: Padding(
|
|
padding: EdgeInsets.only(top: topInset),
|
|
child: builder(context, t),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
bool shouldRebuild(_HeaderDelegate old) =>
|
|
old.minHeight != minHeight ||
|
|
old.maxHeight != maxHeight ||
|
|
old.topInset != topInset ||
|
|
old.builder != builder;
|
|
}
|
|
|
|
/// Cross-fades a header between its open and collapsed states.
|
|
///
|
|
/// The open content keeps its natural size and is clipped as the header
|
|
/// shrinks — letting it compress instead would squash the type on the way
|
|
/// down, and would overflow before it finished fading.
|
|
class HeaderCrossFade extends StatelessWidget {
|
|
const HeaderCrossFade({
|
|
super.key,
|
|
required this.t,
|
|
required this.expandedHeight,
|
|
required this.expanded,
|
|
required this.collapsed,
|
|
});
|
|
|
|
final double t;
|
|
final double expandedHeight;
|
|
final Widget expanded;
|
|
final Widget collapsed;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// The open state clears out over the first two thirds; the collapsed bar
|
|
// arrives over the last half. They overlap briefly, which reads as one
|
|
// state becoming the other rather than a swap.
|
|
final openOpacity = (1 - t * 1.5).clamp(0.0, 1.0);
|
|
final shutOpacity = ((t - 0.5) * 2).clamp(0.0, 1.0);
|
|
|
|
return Stack(
|
|
fit: StackFit.expand,
|
|
children: [
|
|
if (openOpacity > 0)
|
|
ClipRect(
|
|
child: OverflowBox(
|
|
alignment: Alignment.topCenter,
|
|
minHeight: 0,
|
|
// Unbounded, so content taller than the header lays out at its
|
|
// natural size and is clipped. Capping it here instead would
|
|
// throw a layout overflow — and one only visible in a debug
|
|
// build, so it reaches a device before it reaches you.
|
|
maxHeight: double.infinity,
|
|
child: Opacity(
|
|
opacity: openOpacity,
|
|
child: IgnorePointer(ignoring: t > 0.5, child: expanded),
|
|
),
|
|
),
|
|
),
|
|
if (shutOpacity > 0)
|
|
Opacity(
|
|
opacity: shutOpacity,
|
|
child: IgnorePointer(ignoring: t < 0.5, child: collapsed),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|