Issue
I have these requirements for an Appbar and I don’t find a way to solve them.
- When stretched, AppBar has to show the two images one above the other and the title has to be hidden.
- When closed, AppBar has to show the title and two images have to be scaled down when scrolling and moved to both sides of the title. The title becomes visible when scrolling.
I created a couple of mock-ups to help with the result needed.
This is the Appbar when stretched:
This is the Appbar when closed:
Solution
You can create your own SliverAppBar
by extending SliverPersistentHeaderDelegate
.
The translate, scaling, and opacity changes will be done in the build(...)
method because this will be called during extent changes (via scrolling), minExtent <-> maxExtent
.
Here’s a sample code.
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primaryColor: Colors.blue,
),
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverPersistentHeader(
delegate: MySliverAppBar(
title: 'Sample',
minWidth: 50,
minHeight: 25,
leftMaxWidth: 200,
leftMaxHeight: 100,
rightMaxWidth: 100,
rightMaxHeight: 50,
shrinkedTopPos: 10,
),
pinned: true,
),
SliverList(
delegate: SliverChildBuilderDelegate(
(_, int i) => Container(
height: 50,
color: Color.fromARGB(
255,
Random().nextInt(255),
Random().nextInt(255),
Random().nextInt(255),
),
),
childCount: 50,
),
),
],
),
);
}
}
class MySliverAppBar extends SliverPersistentHeaderDelegate {
MySliverAppBar({
required this.title,
required this.minWidth,
required this.minHeight,
required this.leftMaxWidth,
required this.leftMaxHeight,
required this.rightMaxWidth,
required this.rightMaxHeight,
this.titleStyle = const TextStyle(fontSize: 26),
this.shrinkedTopPos = 0,
});
final String title;
final TextStyle titleStyle;
final double minWidth;
final double minHeight;
final double leftMaxWidth;
final double leftMaxHeight;
final double rightMaxWidth;
final double rightMaxHeight;
final double shrinkedTopPos;
final GlobalKey _titleKey = GlobalKey();
double? _topPadding;
double? _centerX;
Size? _titleSize;
double get _shrinkedTopPos => _topPadding! + shrinkedTopPos;
@override
Widget build(
BuildContext context,
double shrinkOffset,
bool overlapsContent,
) {
if (_topPadding == null) {
_topPadding = MediaQuery.of(context).padding.top;
}
if (_centerX == null) {
_centerX = MediaQuery.of(context).size.width / 2;
}
if (_titleSize == null) {
_titleSize = _calculateTitleSize(title, titleStyle);
}
double percent = shrinkOffset / (maxExtent - minExtent);
percent = percent > 1 ? 1 : percent;
return Container(
color: Colors.red,
child: Stack(
children: <Widget>[
_buildTitle(shrinkOffset),
_buildLeftImage(percent),
_buildRightImage(percent),
],
),
);
}
Size _calculateTitleSize(String text, TextStyle style) {
final TextPainter textPainter = TextPainter(
text: TextSpan(text: text, style: style),
maxLines: 1,
textDirection: TextDirection.ltr)
..layout(minWidth: 0, maxWidth: double.infinity);
return textPainter.size;
}
Widget _buildTitle(double shrinkOffset) => Align(
alignment: Alignment.topCenter,
child: Padding(
padding: EdgeInsets.only(top: _topPadding!),
child: Opacity(
opacity: shrinkOffset / maxExtent,
child: Text(title, key: _titleKey, style: titleStyle),
),
),
);
double getScaledWidth(double width, double percent) =>
width - ((width - minWidth) * percent);
double getScaledHeight(double height, double percent) =>
height - ((height - minHeight) * percent);
/// 20 is the padding between the image and the title
double get shrinkedHorizontalPos =>
(_centerX! - (_titleSize!.width / 2)) - minWidth - 20;
Widget _buildLeftImage(double percent) {
final double topMargin = minExtent;
final double rangeLeft =
(_centerX! - (leftMaxWidth / 2)) - shrinkedHorizontalPos;
final double rangeTop = topMargin - _shrinkedTopPos;
final double top = topMargin - (rangeTop * percent);
final double left =
(_centerX! - (leftMaxWidth / 2)) - (rangeLeft * percent);
return Positioned(
left: left,
top: top,
child: Container(
width: getScaledWidth(leftMaxWidth, percent),
height: getScaledHeight(leftMaxHeight, percent),
color: Colors.black,
),
);
}
Widget _buildRightImage(double percent) {
final double topMargin = minExtent + (rightMaxHeight / 2);
final double rangeRight =
(_centerX! - (rightMaxWidth / 2)) - shrinkedHorizontalPos;
final double rangeTop = topMargin - _shrinkedTopPos;
final double top = topMargin - (rangeTop * percent);
final double right =
(_centerX! - (rightMaxWidth / 2)) - (rangeRight * percent);
return Positioned(
right: right,
top: top,
child: Container(
width: getScaledWidth(rightMaxWidth, percent),
height: getScaledHeight(rightMaxHeight, percent),
color: Colors.white,
),
);
}
@override
double get maxExtent => 300;
@override
double get minExtent => _topPadding! + 50;
@override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) =>
false;
}
Answered By – rickimaru
Answer Checked By – Senaida (FlutterFixes Volunteer)