I need AppBar that shows other view on up scrolling.
Is it possible?
Bellow is my attempts to realize it. But...
This is what I need:
when scrolled down:
and when up scrolled
but I have now this:
Here is how I do this:
class DetailedEvent extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverPersistentHeader(
pinned: true,
floating: false,
delegate: _SliverAppBarDelegate(),//HERE IS CUSTOM HEADER CLASS
)
This class used in MainPage:
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
double scrollAnimationValue(double shrinkOffset) {
double maxScrollAllowed = maxExtent - minExtent;
return ((maxScrollAllowed - shrinkOffset) / maxScrollAllowed)
.clamp(0, 1)
.toDouble();
}
#override
Widget build(
BuildContext context,
double shrinkOffset,
bool overlapsContent,
) {
final tableTitleStyle =
TextStyle(color: AppColors.appWhiteDarker, fontSize: 16);
final tableValueStyle = TextStyle(color: AppColors.pureWhite, fontSize: 16);
final double visibleMainHeight = max(maxExtent - shrinkOffset, minExtent);
final double animationVal = scrollAnimationValue(shrinkOffset);
return Container(
height: visibleMainHeight,
width: MediaQuery.of(context).size.width,
child: Stack(
fit: StackFit.expand,
children: <Widget>[
...,
Positioned(
bottom: 0.0,
child: SizedBox(
width: MediaQuery.of(context).size.width,
child: Column(
mainAxisSize: MainAxisSize.max,
children: <Widget>[...],
),
),
)
],
),
);
}
#override...
}
Related
I am trying to create a profile header sliver that can animate.
If you consider above image, Section 1 is what we see in the fully expanded sliver, and Section 2 is what we want to see in pinned mode.
Now I would like transition to move the image - purple circle - to the side, shrink it slightly, and also move the name and the links.
I can achieve all of that but one thing: How to center them in the expanded view.
As I have to use transform to move widgets around, I cannot simply use a centring widget like column or center. And I didn't find a way to calculate the exact position to center the widget, as it needs the size of the widget, that I don't have.
Firstly I am using SliverPersistentHeaderDelegate and it provides shrinkOffset that will be used on linear interpolation(lerp method).
Then CompositedTransformTarget widget to follow the center widget.
On this example play with targetAnchor and followerAnchor and use t/shrinkOffset to maintain other animation.
class SFeb223 extends StatelessWidget {
const SFeb223({super.key});
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
SliverPersistentHeader(
delegate: MySliverPersistentHeaderDelegate(),
pinned: true,
),
SliverToBoxAdapter(
child: SizedBox(
height: 1333,
),
)
],
),
);
}
}
class MySliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {
final LayerLink layerLink = LayerLink();
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
double t = shrinkOffset / maxExtent;
return Material(
color: Colors.cyanAccent.withOpacity(.2),
child: Stack(
children: [
Align(
alignment:
Alignment.lerp(Alignment.center, Alignment.centerLeft, t)!,
child: CompositedTransformTarget(
link: layerLink,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
height: lerpDouble(100, kToolbarHeight - 10, t),
width: lerpDouble(100, kToolbarHeight - 10, t),
decoration: const ShapeDecoration(
shape: CircleBorder(),
color: Colors.deepPurple,
),
),
),
),
),
CompositedTransformFollower(
link: layerLink,
targetAnchor: Alignment.lerp(
Alignment.bottomCenter, Alignment.centerRight, t)!,
followerAnchor:
Alignment.lerp(Alignment.topCenter, Alignment.centerLeft, t)!,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
child: Column(
children: [Text("Sheikh")],
),
),
),
),
],
),
);
}
#override
double get maxExtent => kToolbarHeight * 6;
#override
double get minExtent => kToolbarHeight;
#override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) =>
false;
}
I am trying to replicate this animation with my appBar:
I know I can use SliverAppBar and simply animate the textSize. But how would I implement the logic for the image? It moves to the right and slightly shrinks.
This is what I have for the text:
SliverAppBar(
expandedHeight: 200,
flexibleSpace: FlexibleSpaceBar(
title: Text('Test', textScaleFactor: 1),
),
pinned: true,
),
Any idea how I could solve this?
You play with SliverPersistentHeaderDelegate
class AppSliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
final t = shrinkOffset / maxExtent;
return Stack(
children: [
Align(
alignment: Alignment(0, .7), //perhaps it should also use lerp
child: Text(
"Title",
style: TextStyle(fontSize: ui.lerpDouble(34, 14, t)),
),
),
Align(
alignment:
Alignment.lerp(Alignment.topCenter, Alignment.topRight, t)!,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(Icons.settings),
),
)
],
);
}
#override
double get maxExtent => 200;
#override
double get minExtent => kToolbarHeight;
#override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
return false;
}
}
And used on
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
body: CustomScrollView(
slivers: [
SliverPersistentHeader(
pinned: true,
delegate: AppSliverPersistentHeaderDelegate(),
),
SliverToBoxAdapter(
child: Container(
height: 12222,
color: Colors.red,
),
)
],
),
),
);
}
}
You can try using AnimatedPositioned class which flutter already provide .
Check this link
https://api.flutter.dev/flutter/widgets/AnimatedPositioned-class.html
You can use it and the change the position and size depending on a specific action .
Please give some code regarding this image
You can try this SliverPersistentHeaderDelegate. Play with shrinkOffset. I thought It might be easier with CupertinoSliverNavigationBar but it needed to maintain height for multiline.
class AppSliverPersistentHeader extends SliverPersistentHeaderDelegate {
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return Stack(
children: [
Align(
alignment: Alignment.lerp(const Alignment(-1, 0),
const Alignment(0, 0), (shrinkOffset / maxExtent))!,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text("Text A"),
Text("Text B"),
],
),
)
],
);
}
#override
double get maxExtent => 120;
#override
double get minExtent => kToolbarHeight;
#override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
return false;
}
}
And test snippet
class AppBarTest extends StatelessWidget {
const AppBarTest({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
// CupertinoSliverNavigationBar(
// largeTitle: Column(
// children: [
// Text.rich(TextSpan(text: "MyContaint", children: [
// TextSpan(
// text: "\nothers",
// style: TextStyle(
// fontSize: 12,
// ))
// ])),
// ],
// ),
// ),
SliverPersistentHeader(
pinned: true,
delegate: AppSliverPersistentHeader(),
),
SliverToBoxAdapter(
child: Container(
height: 3333,
),
),
],
),
);
}
}
You can choose two stack items instead of one column.
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'm new to flutter and I want to implement something like this: (klook app)
It's basically a button being shown when the user scrolls a bit.
I tried different things with a SliverAppBar and SliverStickyHeader, but I can't make it work like this. I also played with Opacity and Visibility but it moves my hole view and does not 'overlap' my banner/searchby widget.
My code so far:
class _ExplorePageState extends State<ExplorePage> {
ScrollController _scrollController;
bool lastStatus = true;
_scrollListener() {
if (isShrink != lastStatus) {
print("listen");
setState(() {
lastStatus = isShrink;
});
}
}
bool get isShrink {
return _scrollController.hasClients &&
_scrollController.offset > (400 - kToolbarHeight);
}
#override
void initState() {
_scrollController = ScrollController();
_scrollController.addListener(_scrollListener);
super.initState();
}
#override
void dispose() {
_scrollController.removeListener(_scrollListener());
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
controller: _scrollController,
slivers: <Widget>[
SliverStickyHeader(
header: Visibility(
child: Container(
color: Colors.red,
height: isShrink ? 100 : 0,
child: Text('Header 1'),
),
visible: isShrink ? true : false,
maintainState: true,
maintainSize: true,
maintainAnimation: true,
),
sliver: SliverList(
delegate: SliverChildListDelegate(
[
BannerWidget(),
ButtonWidget(),
],
),
),
),
],
),
);
}
}
The BannerWidget and ButtomWidget are two Containers similar to the app shown above.
I hope you can help me or tell me maybe what this behaviour is called.
Thank you!
If you're ok with using CustomScrollView, you could use SliverPersistentHeader with your own delegate. It will allow you to access current header scroll state and make your own layout depending on how much space you have left.
const double _kSearchHeight = 50.0;
const double _kHeaderHeight = 250.0;
class _ExplorePageState extends State<ExplorePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: CustomScrollView(
slivers: <Widget>[
SliverPersistentHeader(
delegate: DelegateWithSearchBar(),
pinned: true,
),
SliverList(
delegate: SliverChildListDelegate(
[
for (int i = 0; i < 4; i++)
Container(
height: 200,
child: Text('test'),
color: Colors.black26
),
],
),
)
],
),
),
);
}
}
class DelegateWithSearchBar extends SliverPersistentHeaderDelegate {
#override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
final showSearchBar = shrinkOffset > _kHeaderHeight - _kSearchHeight;
return Stack(
children: <Widget>[
AnimatedOpacity(
opacity: !showSearchBar ? 1 : 0,
duration: Duration(milliseconds: 100),
child: LayoutBuilder(
builder: (context, constraints) {
return Container(
decoration: BoxDecoration(
image: DecorationImage(
image: NetworkImage('xxx'),
fit: BoxFit.cover
)
),
height: constraints.maxHeight,
child: SafeArea(
child: Container(
padding: EdgeInsets.only(left: 20, bottom: 20),
alignment: Alignment.bottomLeft,
child: Text(
'Sample Text',
style: TextStyle(color: Colors.white, fontSize: 22)
),
),
),
);
}
),
),
AnimatedOpacity(
opacity: showSearchBar ? 1 : 0,
duration: Duration(milliseconds: 100),
child: Container(
height: _kSearchHeight,
color: Colors.white,
alignment: Alignment.center,
child: Text('search bar')
),
),
],
);
}
#override
bool shouldRebuild(SliverPersistentHeaderDelegate _) => true;
#override
double get maxExtent => _kHeaderHeight;
#override
double get minExtent => _kSearchHeight;
}