I'm trying to create an animation where a widget moves from outside its container to its final position.
Something like this:
Or something like this (In the Choose Exercises screen):
I don't know the final position inside the container in advance (computed by the container, like a grid, a row, a column, etc.).
I think that I need a widget that knows its position on the screen. An overlay to draw the animation above the containers and to animate the opacity of the two widgets in the containers. Am I on the right track?
The animations are considered hard work and take time to learn. Flutter made it easy with its packages. To animate the widgets without much effort, we can wrap them inside different defined animated widgets in the animate_do package.
Here's how you do it: Step 1: Wrap the Stack's child widget inside the Position widget. Step 2: Inside the Position widget, add the top , right , bottom , left property and give it a value. For example, setting top:15 and right:0 will position a widget on the top right of your screen with 15 px space from the top.
I created a package for doing this: https://github.com/letsar/flutter_sidekick.
To accomplish these kinds of animations, you can use the SidekickTeamBuilder
widget.
Here a code example that creates the following animation:
import 'package:flutter/material.dart';
import 'package:flutter_sidekick/flutter_sidekick.dart';
import '../widgets/utils.dart';
class BubblesExample extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SidekickTeamBuilder<String>(
animationDuration: Duration(milliseconds: 500),
initialSourceList: <String>[
'Log\nextension',
'Goblet\nSquats',
'Squats',
'Barbell\nLunge',
'Burpee',
'Dumbell\nLunge',
'Front\nSquats',
],
builder: (context, sourceBuilderDelegates, targetBuilderDelegates) {
return Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: ListView(
children: <Widget>[
ConstrainedBox(
constraints: BoxConstraints(minHeight: 150.0),
child: Wrap(
spacing: 4.0,
runSpacing: 4.0,
children: targetBuilderDelegates.map((builderDelegate) {
return builderDelegate.build(
context,
GestureDetector(
onTap: () => builderDelegate.state
.move(builderDelegate.message),
child: Bubble(
radius: 30.0,
fontSize: 12.0,
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
child: Padding(
padding: const EdgeInsets.all(2.0),
child: Text(
builderDelegate.message,
textAlign: TextAlign.center,
),
),
),
),
animationBuilder: (animation) => CurvedAnimation(
parent: animation,
curve: FlippedCurve(Curves.easeOut),
),
flightShuttleBuilder: (
context,
animation,
type,
from,
to,
) =>
buildShuttle(
animation,
builderDelegate.message,
),
);
}).toList(),
),
),
SizedBox(
height: 100.0,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
CircleButton(
text: '>',
onPressed: () => SidekickTeamBuilder.of<String>(context)
.moveAll(SidekickFlightDirection.toSource),
),
SizedBox(width: 60.0, height: 60.0),
CircleButton(
text: '<',
onPressed: () => SidekickTeamBuilder.of<String>(context)
.moveAll(SidekickFlightDirection.toTarget),
),
],
),
),
Center(
child: Wrap(
spacing: 4.0,
runSpacing: 4.0,
children: sourceBuilderDelegates.map((builderDelegate) {
return builderDelegate.build(
context,
GestureDetector(
onTap: () => builderDelegate.state
.move(builderDelegate.message),
child: Bubble(
radius: 50.0,
fontSize: 20.0,
backgroundColor: Colors.green,
foregroundColor: Colors.white,
child: Padding(
padding: const EdgeInsets.all(2.0),
child: Text(
builderDelegate.message,
textAlign: TextAlign.center,
),
),
),
),
animationBuilder: (animation) => CurvedAnimation(
parent: animation,
curve: Curves.easeOut,
),
flightShuttleBuilder: (
context,
animation,
type,
from,
to,
) =>
buildShuttle(
animation,
builderDelegate.message,
),
);
}).toList(),
),
),
],
),
),
);
},
);
}
Widget buildShuttle(
Animation<double> animation,
String message,
) {
return AnimatedBuilder(
animation: animation,
builder: (_, __) {
return Bubble(
radius: Tween<double>(begin: 50.0, end: 30.0).evaluate(animation),
fontSize: Tween<double>(begin: 20.0, end: 12.0).evaluate(animation),
backgroundColor: ColorTween(begin: Colors.green, end: Colors.blue)
.evaluate(animation),
foregroundColor: Colors.white,
child: Padding(
padding: const EdgeInsets.all(2.0),
child: Text(
message,
textAlign: TextAlign.center,
),
),
);
},
);
}
}
class Bubble extends StatelessWidget {
const Bubble({
Key key,
this.child,
this.backgroundColor,
this.foregroundColor,
this.radius,
this.fontSize,
}) : super(key: key);
final Widget child;
final Color backgroundColor;
final Color foregroundColor;
final double radius;
final double fontSize;
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
TextStyle textStyle =
theme.primaryTextTheme.subhead.copyWith(color: foregroundColor);
Color effectiveBackgroundColor = backgroundColor;
if (effectiveBackgroundColor == null) {
switch (ThemeData.estimateBrightnessForColor(textStyle.color)) {
case Brightness.dark:
effectiveBackgroundColor = theme.primaryColorLight;
break;
case Brightness.light:
effectiveBackgroundColor = theme.primaryColorDark;
break;
}
} else if (foregroundColor == null) {
switch (ThemeData.estimateBrightnessForColor(backgroundColor)) {
case Brightness.dark:
textStyle = textStyle.copyWith(color: theme.primaryColorLight);
break;
case Brightness.light:
textStyle = textStyle.copyWith(color: theme.primaryColorDark);
break;
}
}
textStyle = textStyle.copyWith(fontSize: fontSize);
final double diameter = radius * 2;
return Container(
width: diameter,
height: diameter,
decoration: BoxDecoration(
color: effectiveBackgroundColor,
shape: BoxShape.circle,
),
child: Center(
child: IconTheme(
data: theme.iconTheme.copyWith(color: textStyle.color),
child: DefaultTextStyle(
style: textStyle,
child: child,
),
),
),
);
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With