I have the code below, which I shortened as much as possible to make it easier to deal with. I want to scroll down to show the default appBar, not the background. I did some solutions, but it didn't work. Switch between them with a smooth motion.
I want to use the same existing code because I built on it.
I have attached an illustration of the problem
The main code:
import 'package:flutter/material.dart';
import 'home_page.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
HomePage code:
import 'package:flutter/material.dart';
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
const SliverPersistentHeader(pinned: true, delegate: SliverHeaderDelegateComponent(expandedHeight: 300)),
SliverList(
delegate: SliverChildListDelegate(
[
Container(
height: 1000,
color: Colors.blue.withOpacity(0.5),
child: const Center(child: Text('Body')),
)
],
),
),
],
),
);
}
}
The SliverHeaderDelegateComponent code :
class SliverHeaderDelegateComponent extends SliverPersistentHeaderDelegate {
final double expandedHeight;
const SliverHeaderDelegateComponent({required this.expandedHeight});
#override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
final appBarSize = expandedHeight - shrinkOffset;
final proportion = 2 - (expandedHeight / appBarSize);
final percent = proportion < 0 || proportion > 1 ? 0.0 : proportion;
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) => SizedBox(
height: expandedHeight + expandedHeight / 2,
child: Stack(
clipBehavior: Clip.none,
children: [
Container(
height: 500,
decoration: const BoxDecoration(
color: Colors.black,
image: DecorationImage(
image: NetworkImage(
'https://www.digitalartsonline.co.uk/cmsdata/slideshow/3662115/baby-driver-rory-hi-res.jpg'),
fit: BoxFit.cover,
),
),
),
PositionedDirectional(
start: 0.0,
end: 0.0,
top: appBarSize > 0 ? appBarSize : 0,
bottom: -100,
child: Opacity(
opacity: percent,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 30 * percent),
child: const Card(
elevation: 20.0,
child: Center(
child: Text("Widget"),
),
),
),
),
),
],
),
),
);
}
#override
double get maxExtent => expandedHeight + expandedHeight / 2;
#override
double get minExtent => kToolbarHeight;
#override
bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) {
return true;
}
}
Here is the solution using SliverHeaderDelegateComponent as you requested.
In this example, the AppBar is shown when collapsed, but you can uncomment the commented part if you want to show it on expand. (UPDATE: improved fading as requested in the comment section)
class SliverHeaderDelegateComponent extends SliverPersistentHeaderDelegate {
final double expandedHeight;
const SliverHeaderDelegateComponent({required this.expandedHeight});
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
final deadline = (expandedHeight + minExtent);
double percent = shrinkOffset > deadline ? 1 : shrinkOffset / deadline;
final appBarSize = expandedHeight - shrinkOffset;
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) => SizedBox(
height: expandedHeight + expandedHeight / 2,
child: Stack(
clipBehavior: Clip.none,
children: [
// shrinkOffset == 0 // if you want to show it on expand
shrinkOffset > expandedHeight + minExtent // show it on collapse
? AppBar(title: Text('App Bar'))
: Container(
height: 500,
decoration: const BoxDecoration(
color: Colors.black,
image: DecorationImage(
image: NetworkImage(
'https://www.digitalartsonline.co.uk/cmsdata/slideshow/3662115/baby-driver-rory-hi-res.jpg'),
fit: BoxFit.cover,
),
),
),
PositionedDirectional(
start: 0.0,
end: 0.0,
top: appBarSize > 0 ? appBarSize : 0,
bottom: -100,
child: Opacity(
opacity: 1 - percent,
// opacity: percent < 0.5 ? 1 : (1 - percent) * 2, // if you want to start fading when reach half way scroll
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 30 * percent),
child: const Card(
elevation: 20.0,
child: Center(
child: Text("Widget"),
),
),
),
),
),
],
),
),
);
}
#override
double get maxExtent => expandedHeight + expandedHeight / 2;
#override
double get minExtent => kToolbarHeight;
#override
bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) {
return true;
}
}
Related
I want to make dot indicator move along with the image as the window is stretched to adjust to bigger screens and smaller screens?
I have used MediaQuery but it doesn't really solve my problem. I want it to smoothly adjust to the image as its aspect Ratio changes for which I have used Media Query.
import 'package:flutter/material.dart';
class ImageCarousel extends StatefulWidget {
const ImageCarousel({super.key});
#override
State<ImageCarousel> createState() => _ImageCarouselState();
}
class _ImageCarouselState extends State<ImageCarousel> {
int _currentPage = 0;
#override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
return AspectRatio(
aspectRatio: size.aspectRatio * 3,
child: Stack(
alignment: Alignment.bottomRight,
children: [
PageView.builder(
onPageChanged: (value) {
setState(() => _currentPage = value);
},
itemCount: demoBigImages.length,
itemBuilder: (context, i) => ClipRRect(
borderRadius: const BorderRadius.all(
Radius.circular(12),
),
child: Image.asset(
demoBigImages[i],
),
),
),
Positioned(
bottom: size.height * 0.04,
right: size.width * 0.04,
child: Row(
children: List.generate(
demoBigImages.length,
(index) => Padding(
padding: const EdgeInsets.all(defaultPadding / 4.5),
child: IndicatorDot(
isActive: index == _currentPage,
),
),
),
),
),
],
),
);
}
}
class IndicatorDot extends StatelessWidget {
final bool isActive;
const IndicatorDot({super.key, required this.isActive});
#override
Widget build(BuildContext context) {
return Container(
width: 8,
height: 4,
decoration: BoxDecoration(
color: isActive ? Colors.red : Colors.red.shade50,
borderRadius: const BorderRadius.all(
Radius.circular(12),
),
),
);
}
}
To move the indicator dot alongside the PageView scrolling transition you'd need to move the Stack to the itemBuilder.
It's going to be something like bellow. Notice the commented lines:
class ImageCarousel extends StatefulWidget {
const ImageCarousel({Key? key}) : super(key: key);
#override
State<ImageCarousel> createState() => _ImageCarouselState();
}
class _ImageCarouselState extends State<ImageCarousel> {
int _currentPage = 0;
#override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
return AspectRatio(
aspectRatio: size.aspectRatio * 3,
child: PageView.builder(
onPageChanged: (value) {
setState(() => _currentPage = value);
},
itemCount: 'demoBigImages.length'.length,
itemBuilder: (context, i) => Stack( // <- Here
children: [
ClipRRect(
borderRadius: const BorderRadius.all(
Radius.circular(12),
),
child: Image.asset(
'demoBigImages[i]',
),
),
Positioned( // <- Here
bottom: size.height * 0.04,
right: size.width * 0.04,
child: Row(
children: List.generate(
'demoBigImages'.length,
(index) => Padding(
padding: const EdgeInsets.all(defaultPadding / 4.5),
child: IndicatorDot(
isActive: index == _currentPage,
),
),
),
),
),
],
),
),
);
}
}
Try to use the margins in the dot indicator widget with media query not hard code values.
use below experssion
margin: EdgeInsets.all(MediaQuery.of(context).size.width*0.02),
this is my code to qibla deriction i add cordination qibla but the donst work and i add deriction to the compass but its move a lot and dont give the right direction.its been a month and i didnt found the solution for this ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
import 'dart:math';
import 'package:adhan_dart/adhan_dart.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_compass/flutter_compass.dart';
class Kibla2 extends StatelessWidget {
const Kibla2({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
),
debugShowCheckedModeBanner: false,
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key? key}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
backgroundColor: HSLColor.fromAHSL(1, 0, 0, 0.05).toColor(),
body: Builder(builder: (context) {
return Column(
children: <Widget>[
Expanded(child: _buildCompass()),
],
);
}),
),
);
}
Widget _buildCompass() {
Coordinates coordinates = Coordinates(23.8479, 90.2576);
double width = MediaQuery.of(context).size.width;
double height = MediaQuery.of(context).size.height;
// might need to accound for padding on iphones
//var padding = MediaQuery.of(context).padding;
return StreamBuilder<CompassEvent>(
stream: FlutterCompass.events,
builder: (context, snapshot) {
if (snapshot.hasError) {
return Text('Error reading heading: ${snapshot.error}');
}
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: CircularProgressIndicator(),
);
}
double? direction = snapshot.data?.heading;
// if direction is null, then device does not support this sensor
// show error message
if (direction == null)
return Center(
child: Text("Device does not have sensors !"),
);
int ang = (direction.round());
return Stack(
children: [
Container(
padding: EdgeInsets.all(5.0),
alignment: Alignment.center,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Color(0xFFEBEBEB),
),
child: Transform.rotate(
angle: ((direction ?? 0) * (pi / 180) * -1),
child: Image.asset('assets/compass.png'),
),
),
Transform.rotate(
angle: ((direction ?? 0) * Qibla.qibla(coordinates)),
child: Image.asset(
''),
),
Center(
child: Text(
"$ang",
style: TextStyle(
color: Color(0xFFEBEBEB),
fontSize: 56,
),
),
),
Positioned(
// center of the screen - half the width of the rectangle
left: (width / 2) - ((width / 80) / 2),
// height - width is the non compass vertical space, half of that
top: (height - width) / 2,
child: SizedBox(
width: width / 80,
height: width / 10,
child: Container(
//color: HSLColor.fromAHSL(0.85, 0, 0, 0.05).toColor(),
color: Color(0xBBEBEBEB),
),
),
),
],
);
},
);
}
}
In Flutter you suppose I have a simple Container and I would like to change the size of that to up, for example in this simple screenshot I want to change top container in section 1 to up to have a top container in section 2
and top container in section 1 should behave only 100.0 after size to up
container B in section 1 and section 2 are in the same axis without change position when container A will be resized to up
for example, this is what I want to have
I'm not sure with which one animation I can have this feature
this code work, but this is not what I want to have.
i want to have draggable bottom sheet with changing border radius when bottom sheet arrived to top of screen like with pastes sample video screen and fade0n/out widget inside appbar which that inside top of bottom sheet, which that visible when bottom sheet arrived top or hide when bottom sheet don't have maximum size
import 'package:flutter/material.dart';
void main()=>runApp(SizeUp());
class SizeUp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'test',
home: SizeUpAnim(),
);
}
}
class SizeUpAnim extends StatefulWidget {
#override
State<StatefulWidget> createState() =>_SizeUpAnim();
}
class _SizeUpAnim extends State with SingleTickerProviderStateMixin {
AnimationController _controller;
// ignore: constant_identifier_names
static const _PANEL_HEADER_HEIGHT = 32.0;
bool get _isPanelVisible {
final AnimationStatus status = _controller.status;
return status == AnimationStatus.completed ||
status == AnimationStatus.forward;
}
#override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 100), value: 1.0, vsync: this);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
elevation: 8.0,
title: const Text("Step4"),
leading: IconButton(
onPressed: () {
_controller.fling(velocity: _isPanelVisible ? -1.0 : 1.0);
},
icon: AnimatedIcon(
icon: AnimatedIcons.close_menu,
progress: _controller.view,
),
),
),
body: Column(
children: <Widget>[
Expanded(
child: LayoutBuilder(
builder: _buildStack,
),
),
Text('aaa'),
],
),
);
}
Animation<RelativeRect> _getPanelAnimation(BoxConstraints constraints) {
final double height = constraints.biggest.height;
final double top = height - _PANEL_HEADER_HEIGHT;
const double bottom = -_PANEL_HEADER_HEIGHT;
return RelativeRectTween(
begin: RelativeRect.fromLTRB(0.0, top, 0.0, bottom),
end: const RelativeRect.fromLTRB(0.0, 0.0, 0.0, 0.0),
).animate( CurvedAnimation(parent: _controller, curve: Curves.linear));
}
Widget _buildStack(BuildContext context, BoxConstraints constraints) {
final Animation<RelativeRect> animation = _getPanelAnimation(constraints);
final ThemeData theme = Theme.of(context);
return Container(
color: theme.primaryColor,
child: Stack(
children: <Widget>[
const Center(
child: Text("base"),
),
PositionedTransition(
rect: animation,
child: Material(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16.0),
topRight: Radius.circular(16.0)),
elevation: 12.0,
child: Container(
height: _PANEL_HEADER_HEIGHT,
child: const Center(child: Text("panel")),
),
),
),
],
),
);
}
#override
void dispose() {
super.dispose();
_controller.dispose();
}
}
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool isLong = false;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('First'),
),
body: Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('hey'),
RaisedButton(
onPressed: () {
setImages();
setState(() {
isLong = !isLong;
});
},
child: Text(isLong ? 'long' : 'short'),
),
RaisedButton(
onPressed: _onPressed,
child: Text('open'),
)
],
),
),
);
}
_onPressed() {
Navigator.of(context)
.push(TransparentRoute(builder: (context) => NewWidget(images)));
}
List<String> images = List.generate(
5,
(_) => 'http://placeimg.com/100/100/any',
);
void setImages() {
images = List.generate(
isLong ? 5 : 25,
(_) => 'http://placeimg.com/100/100/any',
);
}
}
class NewWidget extends StatefulWidget {
const NewWidget(this.images, {Key key}) : super(key: key);
final List<String> images;
#override
_NewWidgetState createState() => _NewWidgetState();
}
class _NewWidgetState extends State<NewWidget> {
bool isBig = false;
bool isStack = false;
bool isBounsing = true;
final double topOffset = 200;
final double miniHandleHeigh = 30;
double safeAreaPadding = 0;
double startBigAnimationOffset;
double startStickyOffset;
double backgroundHeight = 0;
double get savedAppBarHeigh => safeAreaPadding + kToolbarHeight;
final ScrollController controller = ScrollController();
#override
void initState() {
WidgetsBinding.instance.addPostFrameCallback(_afterLayout);
super.initState();
}
void _afterLayout(_) {
var media = MediaQuery.of(context);
safeAreaPadding = media.padding.top;
startBigAnimationOffset = topOffset - savedAppBarHeigh;
startStickyOffset = startBigAnimationOffset + 20;
backgroundHeight = media.size.height - miniHandleHeigh - topOffset;
var canScroll = !_isImageSizeBiggerThenBottomSheetSize(
media.size.width,
media.size.height,
);
controller.addListener(
canScroll ? withoutScrolling : withScrolling,
);
}
void withoutScrolling() {
var offset = controller.offset;
if (offset < 0) {
goOut();
} else {
controller.animateTo(
0,
duration: Duration(milliseconds: 100),
curve: Curves.easeIn,
);
}
}
void withScrolling() {
var offset = controller.offset;
if (offset < 0) {
goOut();
} else if (offset < startBigAnimationOffset && isBig) {
setState(() {
isBig = false;
});
} else if (offset > startBigAnimationOffset && !isBig) {
setState(() {
isBig = true;
});
} else if (offset > startStickyOffset && !isStack) {
setState(() {
isStack = true;
});
} else if (offset < startStickyOffset && isStack) {
setState(() {
isStack = false;
});
}
if (offset < topOffset && !isBounsing) {
setState(() => isBounsing = true);
} else if (offset > topOffset && isBounsing) {
setState(() => isBounsing = false);
}
}
void goOut() {
controller.dispose();
Navigator.of(context).pop();
}
_isImageSizeBiggerThenBottomSheetSize(
double screenWidth,
double screenHeight,
) {
// padding: 10, 3 in row;
print(screenHeight);
var itemHeight = (screenWidth - 20) / 3;
print(itemHeight);
var gridMaxHeight = screenHeight - topOffset - miniHandleHeigh;
print(gridMaxHeight);
return (widget.images.length / 3).floor() * itemHeight > gridMaxHeight;
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: isStack ? Colors.white : Colors.transparent,
body: Stack(
children: [
Positioned(
bottom: 0,
right: 0,
left: 0,
child: Container(
constraints: BoxConstraints(minHeight: backgroundHeight),
decoration: BoxDecoration(
color: Colors.white,
),
),
),
ListView(
physics:
isBounsing ? BouncingScrollPhysics() : ClampingScrollPhysics(),
controller: controller,
children: <Widget>[
Container(
alignment: Alignment.bottomCenter,
height: topOffset,
child: TweenAnimationBuilder(
tween: Tween(begin: 0.0, end: isBig ? 1.0 : 0.0),
duration: Duration(milliseconds: 500),
child: Align(
alignment: Alignment.topCenter,
child: Padding(
padding: EdgeInsets.only(top: 15),
child: Container(
decoration: BoxDecoration(
color: Colors.black38,
borderRadius: BorderRadius.circular(2),
),
height: 5,
width: 60,
),
),
),
builder: (_, number, child) {
return Container(
height: savedAppBarHeigh * number + miniHandleHeigh,
decoration: BoxDecoration(
borderRadius: BorderRadius.vertical(
top: Radius.circular((1 - number) * 20)),
color: Colors.white,
),
child: Opacity(opacity: 1 - number, child: child),
);
}),
),
Container(
padding: EdgeInsets.all(10),
constraints: BoxConstraints(
minHeight:
MediaQuery.of(context).size.height - savedAppBarHeigh),
decoration: BoxDecoration(
color: Colors.white,
),
child: getGrid(),
)
],
),
if (isStack)
_AppBar(
title: 'Gallery',
)
],
),
);
}
Widget getGrid() {
return GridView.count(
crossAxisSpacing: 10,
mainAxisSpacing: 10,
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
crossAxisCount: 3,
children: widget.images.map((url) {
return Container(
decoration: BoxDecoration(
border: Border.all(
color: Colors.blueAccent,
),
),
child: Image(
image: NetworkImage(url),
),
);
}).toList(),
);
}
}
class _AppBar extends StatelessWidget {
const _AppBar({Key key, this.title}) : super(key: key);
final Color backgroundColor = Colors.white;
final Color color = Colors.grey;
final String title;
#override
Widget build(BuildContext context) {
return Material(
elevation: 5,
child: Container(
height: kToolbarHeight + MediaQuery.of(context).padding.top,
color: backgroundColor,
child: OverflowBox(
alignment: Alignment.topCenter,
maxHeight: 200,
child: SafeArea(
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: kToolbarHeight),
child: AppBar(
centerTitle: false,
backgroundColor: backgroundColor,
iconTheme: IconThemeData(
color: color, //change your color here
),
primary: false,
title: Text(
title,
style: TextStyle(color: color),
),
elevation: 0,
),
),
),
),
),
);
;
}
}
class TransparentRoute extends PageRoute<void> {
TransparentRoute({
#required this.builder,
RouteSettings settings,
}) : assert(builder != null),
super(settings: settings, fullscreenDialog: false);
final WidgetBuilder builder;
#override
bool get opaque => false;
#override
Color get barrierColor => null;
#override
String get barrierLabel => null;
#override
bool get maintainState => true;
#override
Duration get transitionDuration => Duration(milliseconds: 350);
#override
Widget buildPage(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
final result = builder(context);
return Container(
color: Colors.black.withOpacity(0.5),
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 1),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeIn,
)),
child: result,
),
);
}
}
I think you may try this library, sliding_sheet
when you detect the expand status by controller, then you animate/enlarge the container A.
I have implemented a screen with the CustomScrollView, SliverAppBar and FlexibleSpaceBar like the following:
Now, I'm stuck trying to further expand the functionality by trying to replicate the following effect:
Expand image to fullscreen on scroll
Can something like this be done by using the slivers in Flutter?
Basically, I want the image in it's initial size when screen opens, but depending on scroll direction, it should animate -> contract/fade (keeping the list scrolling functionality) or expand to fullscreen (maybe to new route?).
Please help as I'm not sure in which direction I should go.
Here's the code for the above screen:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
static const double bottomNavigationBarHeight = 48;
#override
Widget build(BuildContext context) => MaterialApp(
debugShowCheckedModeBanner: false,
home: SliverPage(),
);
}
class SliverPage extends StatefulWidget {
#override
_SliverPageState createState() => _SliverPageState();
}
class _SliverPageState extends State<SliverPage> {
double appBarHeight = 0.0;
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
physics: AlwaysScrollableScrollPhysics(),
slivers: <Widget>[
SliverAppBar(
centerTitle: true,
expandedHeight: MediaQuery.of(context).size.height * 0.4,
pinned: true,
flexibleSpace: LayoutBuilder(builder: (context, boxConstraints) {
appBarHeight = boxConstraints.biggest.height;
return FlexibleSpaceBar(
centerTitle: true,
title: AnimatedOpacity(
duration: Duration(milliseconds: 200),
opacity: appBarHeight < 80 + MediaQuery.of(context).padding.top ? 1 : 0,
child: Padding(padding: EdgeInsets.only(bottom: 2), child: Text("TEXT"))),
background: Image.network(
'https://images.pexels.com/photos/443356/pexels-photo-443356.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940',
fit: BoxFit.cover,
),
);
}),
),
SliverList(delegate: SliverChildListDelegate(_buildList(40))),
],
),
);
}
List _buildList(int count) {
List<Widget> listItems = List();
for (int i = 0; i < count; i++) {
listItems.add(
new Padding(padding: new EdgeInsets.all(20.0), child: new Text('Item ${i.toString()}', style: new TextStyle(fontSize: 25.0))));
}
return listItems;
}
}
use CustomScrollView with SliverPersistentHeader
child: LayoutBuilder(
builder: (context, constraints) {
return CustomScrollView(
controller: ScrollController(initialScrollOffset: constraints.maxHeight * 0.6),
slivers: <Widget>[
SliverPersistentHeader(
pinned: true,
delegate: Delegate(constraints.maxHeight),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(ctx, i) => Container(height: 100, color: i.isOdd? Colors.green : Colors.green[700]),
childCount: 12,
),
),
],
);
},
),
the Delegate class used by SliverPersistentHeader looks like:
class Delegate extends SliverPersistentHeaderDelegate {
final double _maxExtent;
Delegate(this._maxExtent);
#override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
var t = shrinkOffset / maxExtent;
return Material(
elevation: 4,
child: Stack(
fit: StackFit.expand,
children: <Widget>[
Image.asset('images/bg.jpg', fit: BoxFit.cover,),
Opacity(
opacity: t,
child: Container(
color: Colors.deepPurple,
alignment: Alignment.bottomCenter,
child: Transform.scale(
scale: ui.lerpDouble(16, 1, t),
child: Text('scroll me down',
style: Theme.of(context).textTheme.headline5.copyWith(color: Colors.white)),
),
),
),
],
),
);
}
#override double get maxExtent => _maxExtent;
#override double get minExtent => 64;
#override bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) => true;
}
I would like to add widgets in the Sliver AppBar area and make it feel more natural. The AppBar gives the liberty to add images but does it have any functionalities to add up more widgets into it?
I am focusing on how to use those CircleAvatar and Text widgets on the sliver appbar.
You won't be able to do that with the SliverAppBar :/.
What you should do is using SliverPersistentHeader with SliverPersistentHeaderDelegate.
You'll have to write a little bit more code but no too much :).
Example:
slivers = [
SliverPersistentHeader(
pinned: True,
delegate: _SliverAppBarDelegate(
minHeight: 60.0,
maxHeight: 250.
),
),
SliverList(
delegate: SliverChildListDelegate([
Padding(
padding: const EdgeInsets.symmetric(horizontal: 25.0),
child: Column(
children: ...
),
)
]),
),
];
...
class _SliverAppBarDelegate extends x {
_SliverAppBarDelegate({
#required this.minHeight,
#required this.maxHeight,
#required this.child,
});
final double minHeight;
final double maxHeight;
final Widget child;
#override
double get minExtent => minHeight;
#override
double get maxExtent => math.max(maxHeight, minHeight);
#override
Widget build(
BuildContext context,
double shrinkOffset,
bool overlapsContent)
{
return new SizedBox.expand(
child: LayoutBuilder(
builder: (_, constraints) {
DO WHAT YOU WANT HERE !
}
)
);
}
#override
bool shouldRebuild(_SliverAppBarDelegate oldDelegate) {
return maxHeight != oldDelegate.maxHeight ||
minHeight != oldDelegate.minHeight ||
child != oldDelegate.child;
}
}
There is a sample using SliverPersistentHeader with SliverPersistentHeaderDelegate.
In this sample when you scroll up the header will resized and button dissapear
slivers: <Widget>[
makeHeader(),
// You can put more slivers here
],
This is the method makeHeader:
SliverPersistentHeader makeHeader() {
return SliverPersistentHeader(
pinned: pinned,
floating: true,
delegate: _SliverAppBarDelegate(
minHeight: 60.0,
maxHeight: 200.0,
),
);
}
And the class _SliverAppBarDelegate:
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
final double minHeight;
final double maxHeight;
_SliverAppBarDelegate(
{#required this.minHeight,
#required this.maxHeight,
this.hideButtonWhenExpanded = true});
#override
double get minExtent => minHeight;
#override
double get maxExtent => math.max(maxHeight, minHeight);
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
final appBarSize = maxHeight - shrinkOffset;
final proportion = 2 - (maxHeight / appBarSize);
final photoToButton = 160 * proportion;
final percent = proportion < 0 || proportion > 1 ? 0.0 : proportion;
return new SizedBox.expand(
child: Container(
color: Colors.white,
child: Stack(
alignment: Alignment.topCenter,
children: <Widget>[
Positioned(
top: 10.0,
child: CircleAvatar(
minRadius: 20.0,
maxRadius: 75.0 * proportion > 20 ? 75.0 * proportion : 20.0,
backgroundImage: NetworkImage(
'https://randomuser.me/api/portraits/men/75.jpg'),
),
),
Positioned(
left: 0.0,
right: 0.0,
top: photoToButton,
child: Opacity(
opacity: percent,
child: FlatButton(
onPressed: () {},
child: Text(
'Add Contact',
style: TextStyle(
color: Colors.blue, fontSize: 14.0 * proportion),
),
),
),
),
Positioned(
left: 0.0,
right: 0.0,
top: appBarSize - 1.0 > 59.0 ? appBarSize - 1 : 59.0,
child: const Divider(
// color: Colors.grey,
height: 1,
thickness: 0.5,
),
)
],
),
),
);
}
#override
bool shouldRebuild(_SliverAppBarDelegate oldDelegate) {
return maxHeight != oldDelegate.maxHeight ||
minHeight !=
oldDelegate
.minHeight;
}
}