Un header collassabile con SliverPersistentHeaderDelegate
SliverAppBar è già un SliverPersistentHeader preconfezionato. Quando serve un comportamento diverso (progress bar che si riempie, titolo che migra, contenuto che cambia layout) conviene scrivere il delegate.
API essenziali del delegate:
minExtent / maxExtent: estensione minima e massima in pixel;
build(context, shrinkOffset, overlapsContent): shrinkOffset va da 0 a maxExtent - minExtent;
shouldRebuild(old): torna true solo se i parametri cambiano.
Attenzione: il widget restituito da build deve poter occupare l'altezza corrente; usa SizedBox.expand o Stack con fit: StackFit.expand. Con pinned: true l'header resta agganciato, con floating: true ricompare allo scroll verso l'alto.
class CollapsingHeader extends SliverPersistentHeaderDelegate {
CollapsingHeader({required this.title, required this.progress});
final String title;
final double progress; // 0..1
@override
double get minExtent => 72;
@override
double get maxExtent => 220;
@override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
final t = (shrinkOffset / (maxExtent - minExtent)).clamp(0.0, 1.0);
final scheme = Theme.of(context).colorScheme;
return Material(
color: Color.lerp(scheme.primaryContainer, scheme.primary, t),
elevation: 4 * t,
child: SizedBox.expand(
child: Stack(
children: [
Positioned(
left: 16 + 8 * t,
bottom: lerpDouble(24, 20, t)!,
child: Text(
title,
style: TextStyle(
fontSize: lerpDouble(30, 18, t)!,
fontWeight: FontWeight.bold,
color: Color.lerp(scheme.onPrimaryContainer, scheme.onPrimary, t),
),
),
),
Align(
alignment: Alignment.bottomCenter,
child: LinearProgressIndicator(value: progress, minHeight: 3),
),
],
),
),
);
}
@override
bool shouldRebuild(CollapsingHeader oldDelegate) =>
oldDelegate.title != title || oldDelegate.progress != progress;
}
Risultato atteso
Scrollando, l'header si comprime da 220 a 72 px interpolando colore, elevazione e dimensione del titolo.