Animating widget positions as the screen scrolls in flutter (GIF included)
You could use a Stack together with Positioned widgets to position the ShrinkableBoxes as you need. Since what controls the animation is the scroll offset, you don't need to use animated widgets or an animation controller or something like it. Here's a working example which calculates the positions by linearly interpolating the initial and final position of the boxes (you can get different animation paths by changing the Curves.linear to other curves):
import 'dart:math' as math;
import 'dart:ui';
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(home: Home()));
}
class Home extends StatefulWidget {
@override
State createState() => HomeState();
}
class HomeState extends State<Home> {
static const double kExpandedHeight = 300.0;
static const double kInitialSize = 75.0;
static const double kFinalSize = 30.0;
static const List<Color> kBoxColors = [
Colors.red,
Colors.green,
Colors.yellow,
Colors.purple,
Colors.orange,
Colors.grey,
];
ScrollController _scrollController = new ScrollController();
@override
void initState() {
_scrollController.addListener(() {
setState(() { /* State being set is the Scroll Controller's offset */ });
});
}
@override
void dispose() {
_scrollController.dispose();
}
Widget build(BuildContext context) {
double size = !_scrollController.hasClients || _scrollController.offset == 0
? 75.0
: 75 -
math.min(45.0,
(45 / kExpandedHeight * math.min(_scrollController.offset, kExpandedHeight) * 1.5));
return Scaffold(
body: CustomScrollView(
controller: _scrollController,
slivers: <Widget>[
SliverAppBar(
pinned: true,
expandedHeight: kExpandedHeight,
title: Text("Title!"),
bottom: PreferredSize(
preferredSize: Size.fromHeight(55),
child: buildAppBarBottom(size),
),
),
SliverFixedExtentList(
itemExtent: 50.0,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return ListTile(title: Text('Item $index'));
},
),
),
],
),
);
}
Widget buildAppBarBottom(double size) {
double t = (size - kInitialSize) / (kFinalSize - kInitialSize);
const double initialContainerHeight = 2 * kInitialSize;
const double finalContainerHeight = kFinalSize;
return Container(
height: lerpDouble(initialContainerHeight, finalContainerHeight, t),
child: LayoutBuilder(
builder: (context, constraints) {
List<Widget> stackChildren = [];
for (int i = 0; i < 6; i++) {
Offset offset = getInterpolatedOffset(i, constraints, t);
stackChildren.add(Positioned(
left: offset.dx,
top: offset.dy,
child: buildSizedBox(size, kBoxColors[i]),
));
}
return Stack(children: stackChildren);
},
),
);
}
Offset getInterpolatedOffset(int index, BoxConstraints constraints, double t) {
Curve curve = Curves.linear;
double curveT = curve.transform(t);
Offset a = getOffset(index, constraints, kInitialSize, 3);
Offset b = getOffset(index, constraints, kFinalSize, 6);
return Offset(
lerpDouble(a.dx, b.dx, curveT),
lerpDouble(a.dy, b.dy, curveT),
);
}
Offset getOffset(int index, BoxConstraints constraints, double size, int columns) {
int x = index % columns;
int y = index ~/ columns;
double horizontalMargin = (constraints.maxWidth - size * columns) / 2;
return Offset(horizontalMargin + x * size, y * size);
}
Widget buildSizedBox(double size, Color color) {
return Container(
height: size,
width: size,
color: color,
);
}
}
smbl
Updated on December 09, 2022Comments
-
smbl over 1 year
I am trying to animate two Rows of widgets to collapse into 1 Row of these widgets as one scroll. I am trying to achieve this behavior inside a
SliverAppBar
.For clarification, I have included a GIF here for reference. I would like the behavior you see in the app bar, but instead of 1 row to 2, I would like 2 rows becoming 1.
Here is a quick snippet of what I have so far. I wrapped 2
Row
widgets that contain 3shrinkableBox
widgets each into aWrap
widget. I dynamically adjust the size of these boxes by hooking into_scrollController.offset
and doing some calculations. The rows do move around dynamically but they don't animate and move abruptly instead.double kExpandedHeight = 300.0; Widget build(BuildContext context) { double size = !_scrollController.hasClients || _scrollController.offset == 0 ? 75.0 : 75 - math.min(45.0, (45 / kExpandedHeight * math.min(_scrollController.offset, kExpandedHeight) * 1.5)); return Scaffold( body: CustomScrollView( controller: _scrollController, slivers: <Widget>[ SliverAppBar( pinned: true, expandedHeight: kExpandedHeight, title: new Text( "Title!", ), bottom: PreferredSize(child: Wrap( children: <Widget>[ Row( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: <Widget>[ ShrinkableBox( onClick: () { print("tapped"); }, size: size, ), ShrinkableBox( onClick: () { print("tapped"); }, size: size, ), ShrinkableBox( onClick: () { print("tapped"); }, size: size, ), Row( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: <Widget>[ ShrinkableBox( onClick: () { print("tapped"); }, size: size, ), ShrinkableBox( onClick: () { print("tapped"); }, size: size, ), ShrinkableBox( onClick: () { print("tapped"); }, size: size, ), ], ), ], ), preferredSize: new Size.fromHeight(55),), ) // ... // ...Other sliver list content here... // ...
-
smbl over 5 yearsThis is exactly what i wanted! How were you able to figure this out? This is a pretty complex piece of code. I would like to learn animations like this for myself as well.
-
Luis Fernando Trivelatto over 5 years@smbl Thanks! I guess I've had a fair share of struggles with animations in Flutter hehe. Have you checked out the Animations section of the Flutter website? While it explains how to do more complex animations, you might also want to check out the implicitly animated widgets, which are still powerful and simpler to use, such as AnimatedContainer: flutter.dev/docs/development/ui/widgets/animation
-
devDeejay about 4 yearsFor ios with the bouncing scrolling effect, this will throw an exception.
-
Dennis Barzanoff over 3 yearsYou can now use the new for syntax for the stack children