Initial commit: Krow Worker App
This commit is contained in:
137
lib/widgets/collapsing_header.dart
Normal file
137
lib/widgets/collapsing_header.dart
Normal file
@@ -0,0 +1,137 @@
|
||||
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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user