I've made a widget that takes a list of children, and a List<double> of gaps, which displays the children with the respective gap between them. I've made it so passing a new list of gaps causes the widget to animate from the old gap to the new gaps (changing number of gaps not supported).
What's the best way to handle implicity animating the gaps?
This is the kind of behaviour I'm looking for:
(source: gfycat.com)
To avoid unneeded repetition you can move the tween logic to a custom widget.
You can also fuse List<Widget> children with List<double> gaps with a custom Gap widget.
Ultimately you can keep using ListView via separated constructor and use our custom Gap as separators.
Taking all of this into consideration, in the end your Gap widget is simply an AnimatedContainer with a custom height:
class Gap extends StatelessWidget {
final double gap;
const Gap(this.gap, {Key key})
: assert(gap >= .0),
super(key: key);
#override
Widget build(BuildContext context) {
return AnimatedContainer(
duration: const Duration(milliseconds: 250),
curve: Curves.easeOut,
height: gap,
);
}
}
And you can then use it using the following:
ListView.separated(
itemCount: 42,
addAutomaticKeepAlives: true,
itemBuilder: (context, index) {
return RaisedButton(onPressed: null, child: Text("Foo $index"));
},
separatorBuilder: (context, index) {
return Gap(10.0);
},
),
The addAutomaticKeppAlives: true here is used to ensure that items leaving then reappearing don't have their animation reset. But it is not a necessity.
Here's a full example with dynamically changing gap size:
class Home extends StatefulWidget {
#override
HomeState createState() {
return new HomeState();
}
}
class HomeState extends State<Home> {
final rand = Random.secure();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Column(
children: <Widget>[
RaisedButton(
onPressed: () {
setState(() {});
},
child: Text("Reroll random gaps"),
),
Expanded(
child: ListView.separated(
addAutomaticKeepAlives: true,
itemCount: 42,
itemBuilder: (context, index) {
print("Bar");
return RaisedButton(onPressed: () {}, child: Text("Foo $index"));
},
separatorBuilder: (context, index) {
print("Foo $index");
return Gap(rand.nextDouble() * 10.0);
},
),
),
],
),
);
}
}
This was my solution, but my code is pretty messy. In particular, I'm not sure having a seperate list for the animations, tweens, controllers and curves (which is what I'm doing now) is the best way to do things. Also doing List<int>.generate(widget.gaps.length, (i) => i).forEach in the build function seems wrong, but the usual for (var i; i<x; i++) doesn't seem very dart-y either.
Is there a better way to handle these two issues?
class GappedList extends StatefulWidget {
final List<Widget> children;
final List<double> gaps;
GappedList({#required this.children, #required this.gaps}) :
assert(children != null),
assert(children.length > 0),
assert(gaps != null),
assert (gaps.length >= children.length - 1);
#override
GappedListState createState() {
return new GappedListState();
}
}
class GappedListState extends State<GappedList> with TickerProviderStateMixin{
List<Animation> _animations = [];
List<AnimationController> _controllers = [];
List<CurvedAnimation> _curves = [];
List<Tween<double>> _tweens;
#override
void initState() {
super.initState();
_tweens = widget.gaps.map((g) => Tween(
begin: g ?? 0.0,
end: g ?? 0.0,
)).toList();
_tweens.forEach((t) {
_controllers.add(AnimationController(
value: 1.0,
vsync: this,
duration: Duration(seconds: 1),
));
_curves.add(CurvedAnimation(parent: _controllers.last, curve: Curves.ease));
_animations.add(t.animate(_curves.last));
});
}
#override
void dispose() {
_controllers.forEach((c) => c.dispose());
super.dispose();
}
#override
void didUpdateWidget(GappedList oldWidget) {
super.didUpdateWidget(oldWidget);
assert(oldWidget.gaps.length == widget.gaps.length);
List<Tween<double>> oldTweens = _tweens;
List<int>.generate(widget.gaps.length, (i) => i).forEach(
(i) {
_tweens[i] = Tween<double>(
begin: oldTweens[i].evaluate(_curves[i]),
end: widget.gaps[i] ?? 0.0,
);
_animations[i] = _tweens[i].animate(_curves[i]);
if (_tweens[i].begin != _tweens[i].end) {
_controllers[i].forward(from: 0.0);
}
}
);
}
#override
Widget build(BuildContext context) {
List<Widget> list = [];
List<int>.generate(widget.children.length, (i) => i).forEach(
(i) {
list.add(widget.children[i]);
if (widget.children[i] != widget.children.last) {
list.add(
AnimatedBuilder(
animation: _animations[i],
builder: (context, _) => ConstrainedBox(
constraints: BoxConstraints.tightForFinite(
height: _animations[i].value,
),
),
)
);
}
}
);
return ListView(
primary: true,
shrinkWrap: true,
children: list,
);
}
}
Related
I'm trying to create a basic Hero animation between a gallery grid view page (using photo manager plugin) and a detail page. When the hero animation is done both back and forward, the picture is flickering .
Here is the example I've made :
The full code runnable :
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:photo_manager/photo_manager.dart';
void main() {
runApp(const TestApp());
}
class TestApp extends StatefulWidget {
const TestApp({Key? key}) : super(key: key);
#override
State<TestApp> createState() => _TestAppState();
}
class _TestAppState extends State<TestApp> {
final List<AssetEntity> assetsList = [];
bool granted = false;
void loadAssets() async {
granted = await PhotoManager.requestPermission();
if (granted) {
FilterOptionGroup option = FilterOptionGroup()
..addOrderOption(const OrderOption(
type: OrderOptionType.createDate,
asc: false,
));
final albums = await PhotoManager.getAssetPathList(filterOption: option, type: RequestType.image);
print("albums : $albums");
if (albums.isNotEmpty) {
var alb = albums.where((element) {
return element.name == 'Test';
});
var album = alb.first;
// Now that we got the album, fetch all the assets it contains
List<AssetEntity> currentList =
await album.getAssetListRange(start: 0, end: 200);
// Update the state and notify UI
assetsList.clear();
assetsList.addAll(currentList);
}
setState(() {});
}
}
#override
void initState() {
loadAssets();
super.initState();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: granted
? _gridView()
: Center(
child: Container(
color: Colors.blue,
width: 200,
height: 200,
child: TextButton(
onPressed: () async {
granted = await PhotoManager.requestPermission();
setState(() {});
},
child: const Text(
"Ask permission",
style: TextStyle(color: Colors.white),
),
)),
)),
);
}
Widget _gridView() {
return GridView.builder(
itemCount: assetsList.length,
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3),
itemBuilder: (context, index) {
return Hero(
createRectTween: (Rect? begin, Rect? end) {
RectTween _rectTween = RectTween(begin: begin, end: end);
return _rectTween;
},
tag: assetsList[index].id,
child: GalleryThumbnail(
asset: assetsList[index],
onTap: (bytes) {
Navigator.push(context, MaterialPageRoute(builder: (context) {
return DetailsPage(asset: assetsList[index], bytes: bytes);
}));
},
));
});
}
}
class GalleryThumbnail extends StatelessWidget {
final AssetEntity asset;
final ValueChanged<Uint8List> onTap;
const GalleryThumbnail({Key? key, required this.asset, required this.onTap})
: super(key: key);
#override
Widget build(BuildContext context) {
return FutureBuilder<Uint8List?>(
future: Platform.isIOS
? asset.thumbDataWithOption(
ThumbOption.ios(
width: 500,
height: 500,
deliveryMode: DeliveryMode.opportunistic,
resizeMode: ResizeMode.fast,
resizeContentMode: ResizeContentMode.fit,
quality: 100
// resizeContentMode: ResizeContentMode.fill,
),
)
: asset.thumbDataWithSize(250, 250),
builder: (_, snapshot) {
final bytes = snapshot.data;
if (snapshot.hasError) {
return Container();
}
// If we have no data
if (bytes == null) return Container();
// If there's data, display it as an image
return GestureDetector(
onTap: () {
onTap(bytes);
},
child: Image.memory(bytes, fit: BoxFit.cover,gaplessPlayback: true,));
},
);
}
}
class DetailsPage extends StatefulWidget {
final AssetEntity asset;
final Uint8List bytes;
const DetailsPage({Key? key, required this.asset, required this.bytes})
: super(key: key);
#override
_DetailsPageState createState() => _DetailsPageState();
}
class _DetailsPageState extends State<DetailsPage> {
late ImageProvider _imageProvider;
Future<void> loadFile() async {
try {
File? file = await widget.asset.file;
if (file == null) return;
_imageProvider = Image.file(file).image;
setState(() {});
} catch (e) {
print("error to load file : " + e.toString());
}
}
#override
void initState() {
_imageProvider = Image.memory(widget.bytes).image;
loadFile();
super.initState();
}
#override
Widget build(BuildContext context) {
double ratio = widget.asset.height / widget.asset.width;
return Scaffold(
body: Stack(
children: [
Hero(
createRectTween: (Rect? begin, Rect? end) {
RectTween _rectTween = RectTween(begin: begin, end: end);
return _rectTween;
},
tag: widget.asset.id,
child: Center(
child: Image(image: _imageProvider,
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height*ratio,
gaplessPlayback: true),
),
),
const SafeArea(
child: Padding(
padding: EdgeInsets.all(8.0),
child: BackButton(),
)),
],
),
);
}
}
I've implemented hero animation somewhere else in my project and I'm not getting this behavior where as here I do.
Why this is happening and how to correct this ?
Add
transitionOnUserGestures: true,
to Hero() widget in current & next page
This happens because the Hero of the image thumbail wraps the FutureBuilder, and then the future triggers again during the animation, wraps the Image instead of the FutureBuilder.
On my signup form, I have a checkbox which needs to shake a bit whenever the user tries to login before accepting the terms and conditions. How can I achieve something like this Flutter?
import 'package:flutter/material.dart';
#immutable
class ShakeWidget extends StatelessWidget {
final Duration duration;
final double deltaX;
final Widget child;
final Curve curve;
const ShakeWidget({
Key key,
this.duration = const Duration(milliseconds: 500),
this.deltaX = 20,
this.curve = Curves.bounceOut,
this.child,
}) : super(key: key);
/// convert 0-1 to 0-1-0
double shake(double animation) =>
2 * (0.5 - (0.5 - curve.transform(animation)).abs());
#override
Widget build(BuildContext context) {
return TweenAnimationBuilder<double>(
key: key,
tween: Tween(begin: 0.0, end: 1.0),
duration: duration,
builder: (context, animation, child) => Transform.translate(
offset: Offset(deltaX * shake(animation), 0),
child: child,
),
child: child,
);
}
}
If you need to re-enable shaking, just change ShakeWidget key to some random one.
I achieved this a different way because I wanted to be able to control the duration and get a bit more vigorous shaking. I also wanted to be able to add this easily as a wrapper for other child widgets so I looked up how to use keys to have a parent control actions in a child widget. Here is the class:
class ShakerState extends State<Shaker> with SingleTickerProviderStateMixin {
late AnimationController animationController;
late Animation<double> animation;
#override
void initState() {
super.initState();
animationController = AnimationController(
vsync: this,
duration: Duration(milliseconds: 800), // how long the shake happens
)..addListener(() => setState(() {}));
animation = Tween<double>(
begin: 00.0,
end: 120.0,
).animate(animationController);
}
math.Vector3 _shake() {
double progress = animationController.value;
double offset = sin(progress * pi * 10.0); // change 10 to make it vibrate faster
return math.Vector3(offset * 25, 0.0, 0.0); // change 25 to make it vibrate wider
}
shake() {
animationController.forward(from:0);
}
#override
Widget build(BuildContext context) {
return Transform(
transform: Matrix4.translation(_shake()),
child: widget.child,
);
}
}
And then to use this you need a key in your parent:
final GlobalKey<ShakerState> _shakeKey = GlobalKey<ShakerState>();
And then you can do something like this inside your parent body (see where "Shaker" is used around the child I want to shake):
...
Container(
height: 50,
width: 250,
decoration: BoxDecoration(color: Colors.blue, borderRadius: BorderRadius.circular(20)),
child: TextButton(
onPressed: () => _handleEmailSignIn(loginController.text, loginPasswordController.text),
child: Shaker(_shakeKey, Text('Login', // <<================
style: TextStyle(color: Colors.white, fontSize: 25),
)),
),
),
...
Then with the controller you can trigger the shake at a time you want programmatically like this (see the use of "_shakeKey"):
Future<void> _handleEmailSignIn(String user, password) async {
try {
await auth.signInWithEmailAndPassword(email: user, password: password);
FocusScope.of(context).unfocus();
await Navigator.pushNamedAndRemoveUntil(context, '/next_page', ModalRoute.withName('/'));
} on FirebaseAuthException catch (e) {
_shakeKey.currentState?.shake(); // <<=============
if (e.code == 'user-not-found') {
print('No user found for that email.');
} else if (e.code == 'wrong-password') {
print('Wrong password provided for that user.');
}
}
setState(() {});
}
Little improved of #Kent code (added controller).
import 'package:flutter/material.dart';
class ShakeX extends StatefulWidget {
final Widget child;
final double horizontalPadding;
final double animationRange;
final ShakeXController controller;
final Duration animationDuration;
const ShakeX(
{Key key,
#required this.child,
this.horizontalPadding = 30,
this.animationRange = 24,
this.controller,
this.animationDuration = const Duration(milliseconds: 500)})
: super(key: key);
#override
_ShakeXState createState() => _ShakeXState();
}
class _ShakeXState extends State<ShakeX> with SingleTickerProviderStateMixin {
AnimationController animationController;
#override
void initState() {
animationController =
AnimationController(duration: widget.animationDuration, vsync: this);
if (widget.controller != null) {
widget.controller.setState(this);
}
super.initState();
}
#override
Widget build(BuildContext context) {
final Animation<double> offsetAnimation =
Tween(begin: 0.0, end: widget.animationRange)
.chain(CurveTween(curve: Curves.elasticIn))
.animate(animationController)
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
animationController.reverse();
}
});
return AnimatedBuilder(
animation: offsetAnimation,
builder: (context, child) {
return Container(
margin: EdgeInsets.symmetric(horizontal: widget.animationRange),
padding: EdgeInsets.only(
left: offsetAnimation.value + widget.horizontalPadding,
right: widget.horizontalPadding - offsetAnimation.value),
child: widget.child,
);
},
);
}
}
class ShakeXController {
_ShakeXState _state;
void setState(_ShakeXState state) {
_state = state;
}
Future<void> shake() {
print('shake');
return _state.animationController.forward(from: 0.0);
}
}
import 'package:flutter/material.dart';
class ShakeError extends StatefulWidget {
const ShakeError({
Key? key,
required this.child,
this.controller,
this.duration = const Duration(milliseconds: 500),
this.deltaX = 20,
this.curve = Curves.bounceOut,
}) : super(key: key);
final Widget child;
final Duration duration;
final double deltaX;
final Curve curve;
final Function(AnimationController)? controller;
#override
State<ShakeError> createState() => _ShakeErrorState();
}
class _ShakeErrorState extends State<ShakeError>
with SingleTickerProviderStateMixin<ShakeError> {
late AnimationController controller;
late Animation<double> offsetAnimation;
#override
void initState() {
controller = AnimationController(duration: widget.duration, vsync: this);
offsetAnimation = Tween<double>(begin: 0.0, end: 1.0)
.chain(CurveTween(curve: widget.curve))
.animate(controller);
if (widget.controller is Function) {
widget.controller!(controller);
}
super.initState();
}
#override
void dispose() {
controller.dispose();
super.dispose();
}
/// convert 0-1 to 0-1-0
double shake(double animation) =>
2 * (0.5 - (0.5 - widget.curve.transform(animation)).abs());
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: offsetAnimation,
builder: (BuildContext context, Widget? child) {
return Transform.translate(
offset: Offset(widget.deltaX * shake(offsetAnimation.value), 0),
child: child,
);
},
child: widget.child,
);
}
}
Here is some code from my app. It shakes a red x on the screen. redx.png. I'm sure you could adopt it to your use case. I'm using AnimatedBuilder.
Code in action:
https://giphy.com/gifs/Yo2u06oMu1ksPYRD3B
import 'package:flutter/material.dart';
class ShakeX extends StatefulWidget {
const ShakeX({
Key key,
}) : super(key: key);
#override
_ShakeXState createState() => _ShakeXState();
}
class _ShakeXState extends State<ShakeX> with SingleTickerProviderStateMixin{
AnimationController controller;
#override
void initState() {
controller = AnimationController(duration: const Duration(milliseconds: 500), vsync: this);
super.initState();
}
#override
Widget build(BuildContext context) {
final Animation<double> offsetAnimation =
Tween(begin: 0.0, end: 24.0).chain(CurveTween(curve: Curves.elasticIn)).animate(controller)
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.reverse();
}
});
controller.forward(from: 0.0);
return AnimatedBuilder(animation: offsetAnimation,
builder: (context, child){
if (offsetAnimation.value < 0.0) print('${offsetAnimation.value + 8.0}');
return Container(
margin: EdgeInsets.symmetric(horizontal: 24.0),
padding: EdgeInsets.only(left: offsetAnimation.value + 30.0, right: 30.0 - offsetAnimation.value),
child: Image.asset("assets/redx.png"),
);
},);
}
}
For those still looking... You can achieve something like that using Animated Widget
ShakeAnimatedWidget(
enabled: this._enabled,
duration: Duration(milliseconds: 1500),
shakeAngle: Rotation.deg(z: 40),
curve: Curves.linear,
child: FlutterLogo(
style: FlutterLogoStyle.stacked,
),
),
Check the link for even advance usage.
There are many animation based packages in flutter, you can check that site (https://fluttergems.dev/animation-transition/) to see them. As developer, you don't have to to create animation classes from scratch.
Regarding shaking animation, I would suggest flutter_animator package. There is a shake widget which exactly performs what you need.
My version on previous code. It prevents usage of paddings and margins
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class ShakeX extends StatefulWidget {
final Widget child;
final double horizontalPadding;
final double animationRange;
final ShakeXController shakeXController;
final Duration animationDuration;
const ShakeX({
Key? key,
required this.child,
this.horizontalPadding = 16,
this.animationRange = 16,
required this.shakeXController,
this.animationDuration = const Duration(milliseconds: 500),
}) : super(key: key);
#override
ShakeXState createState() => ShakeXState();
}
class ShakeXState extends State<ShakeX> with SingleTickerProviderStateMixin {
late AnimationController animationController;
var childSize = Size.zero;
#override
void initState() {
super.initState();
animationController = AnimationController(duration: widget.animationDuration, vsync: this);
widget.shakeXController.setState(this);
}
#override
Widget build(BuildContext context) {
final Animation<double> offsetAnimation = Tween(begin: 0.0, end: widget.animationRange)
.chain(CurveTween(curve: Curves.elasticIn))
.animate(animationController)
..addStatusListener((status) {
if (status == AnimationStatus.completed && status != AnimationStatus.reverse) {
animationController.reverse();
}
});
return AnimatedBuilder(
animation: offsetAnimation,
builder: (context, child) {
return SizedBox(
width: childSize.width,
height: childSize.height,
child: Stack(
clipBehavior: Clip.none,
children: [
Positioned(
left: offsetAnimation.value,
child: MeasureSize(
onChange: (size) {
setState(() {
childSize = size;
});
},
child: widget.child),
),
],
),
);
},
);
}
}
class ShakeXController {
late ShakeXState _state;
void setState(ShakeXState state) {
_state = state;
}
Future<void> shake() {
return _state.animationController.forward(from: 0.0);
}
}
typedef void OnWidgetSizeChange(Size size);
class MeasureSizeRenderObject extends RenderProxyBox {
Size? oldSize;
OnWidgetSizeChange onChange;
MeasureSizeRenderObject(this.onChange);
#override
void performLayout() {
super.performLayout();
Size newSize = child!.size;
if (oldSize == newSize) return;
oldSize = newSize;
WidgetsBinding.instance.addPostFrameCallback((_) {
onChange(newSize);
});
}
}
class MeasureSize extends SingleChildRenderObjectWidget {
final OnWidgetSizeChange onChange;
const MeasureSize({
Key? key,
required this.onChange,
required Widget child,
}) : super(key: key, child: child);
#override
RenderObject createRenderObject(BuildContext context) {
return MeasureSizeRenderObject(onChange);
}
#override
void updateRenderObject(BuildContext context, covariant MeasureSizeRenderObject renderObject) {
renderObject.onChange = onChange;
}
}
I have a Flare character on that I have to display some items by their node when I am updating the node and the item is displaying on character, it is taking some time and getting delayed, in my ChimpCharacter class whenever I choose the item by a tap on the item, the whole class is updating and its initState is calling again, I am using ScopedModel for updating state.
Here is the link to my flare character that I am using
https://drive.google.com/file/d/14hI_cezteapxiF8xDT6w-DrcIwPIhb8n/view?usp=sharing
My main.dart
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
Item item1 = Item('brac1');
Item item2 = Item('glasses1');
return MaterialApp(
home: ScopedModel<AppModel>(
model: AppModel(),
child: Scaffold(
body: Container(
child: Column(
children: <Widget>[
Expanded(
flex: 6,
child: ScopedModelDescendant<AppModel>(
builder: (context, child, model) {
return ChimpCharacter(
itemNames: model.items,
key: UniqueKey(),
);
}),
),
Expanded(
flex: 4,
child: ScopedModelDescendant<AppModel>(
builder: (context, child, model) {
return Wrap(
spacing: 100.0,
children: <Widget>[
RaisedButton(
color: Colors.blue,
elevation: 3.0,
onPressed: () {
model.addItem(item1);
print('model is ${model.items}');
},
child: Text('Braclet'),
),
RaisedButton(
color: Colors.blue,
elevation: 3.0,
onPressed: () {
model.addItem(item2);
print('model is ${model.items}');
},
child: Text('Glasses'),
),
],
);
},
),
),
],
),
),
),
),
);
}
}
My model.dart
import 'package:scoped_model/scoped_model.dart';
class AppModel extends Model {
List<Item> _items = [];
List<Item> get items => _items;
void addItem(Item item) {
this._items = items;
if (_items.contains(item)) {
_items.remove(item);
} else {
_items.add(item);
}
notifyListeners();
}
}
class Item {
String itemName;
Item(this.itemName);
}
my chimpcharacter.dart
class ChimpCharacter extends StatefulWidget {
ChimpCharacter({Key key, this.itemNames}) : super(key: key);
final List<Item> itemNames;
#override
_ChimpCharacterState createState() => _ChimpCharacterState();
}
class _ChimpCharacterState extends State<ChimpCharacter>
implements FlareController {
List<String> items = [];
FlareController flareController;
#override
void initState() {
super.initState();
print('initState called ');
if (widget.itemNames != null) {
widget.itemNames.map((s) {
items.add(s.itemName);
}).toList(growable: false);
}
}
#override
Widget build(BuildContext context) {
return FlareActor(
'assets/chimp.flr',
alignment: Alignment.center,
fit: BoxFit.contain,
animation: 'walking',
controller: this,
);
}
#override
bool advance(FlutterActorArtboard artboard, double elapsed) {
// print('advance');
if (items != null) {
items.forEach((String item) {
final ActorNode acceName = artboard.getNode(item);
acceName.collapsedVisibility = false;
});
}
return false;
}
#override
void initialize(FlutterActorArtboard artboard) {}
#override
void setViewTransform(Mat2D viewTransform) {
print('setViewTransform');
}
#override
ValueNotifier<bool> isActive;
}
I've created an animated Floating Action Button (FAB) menu:
Main FAB which spawns other FABs when clicked.
It is based on several online tutorials.
Works fine besides one major issue:
The spawned FABs don't trigger an onPressed event.
Seems like there's a problem with the combination of transform animation and the stack widget (which the FABs are children of).
Works fine if I replace the stack with a row...
Seems like the issue was addressed here, but without a proper solution:
FloatingActionButton onPressed not triggering
Here's the complete code for that FAB menu.
Just supply it a list with buttons data, similar to this one:
List<Map<String, dynamic>> _buttonsData = [
{'color': Colors.green ,'icon': Icons.stop},
{'color': Colors.blue ,'icon': Icons.subway},
{'color': Colors.green ,'icon': Icons.add},];
FancyFab2(_buttonsData, null)
The code:
import 'package:flutter/material.dart';
class FancyFab extends StatefulWidget {
FancyFab(this._buttonsData, this._onSelected);
final List<Map<String, dynamic>> _buttonsData;
final ValueChanged<int> _onSelected;
#override
createState() => FancyFabState(_buttonsData, _onSelected);
}
class FancyFabState extends State<FancyFab> with SingleTickerProviderStateMixin {
final List<Map<String, dynamic>> _buttonsData;
final ValueChanged<int> _onSelected;
// state vars
AnimationController _controller;
Animation <double> _transform;
bool _isOpened = false;
FancyFabState(this._buttonsData, this._onSelected);
#override
void initState() {
// call base
super.initState();
// _controller
_controller = AnimationController(
duration: Duration(milliseconds: 100),
vsync: this
);
_transform = Tween<double>(
begin: 0.0,
end: -64.0,
).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.fastOutSlowIn
),
);
}
#override
void dispose() {
// controller
_controller.dispose();
// call base
super.dispose();
}
#override
Widget build(BuildContext context) {
return _buildFab(_controller);
}
Widget _buildFab(AnimationController controller) {
return AnimatedBuilder(
animation: controller,
builder: (context, builder) {
return Stack(
children: List.generate(_buttonsData.length, (index) => _buildStackWidget(context, index))
);
}
);
}
Widget _buildStackWidget(BuildContext context, int index) {
Map<String, dynamic> buttonData = _buttonsData[index];
if (index == _buttonsData.length - 1)
return _buildMenuButton(index, buttonData);
else
return _buildMenuItem(index, buttonData);
}
Widget _buildMenuItem(int index, Map<String, dynamic> buttonData) {
return Transform.translate(
offset: Offset((1 + index) * _transform.value, 0.0),
child: FloatingActionButton(
heroTag: 100 + index,
backgroundColor: buttonData['color'],
//onPressed: () => _onSelected(index),
onPressed: () => print('click'),
child: Icon(buttonData['icon']),
),
);
}
Widget _buildMenuButton(int index, Map<String, dynamic> buttonData) {
return FloatingActionButton(
heroTag: 200,
backgroundColor: buttonData['color'],
onPressed: _toggle,
child: Icon(buttonData['icon']),
);
}
void _toggle() {
print('toggle');
_isOpened = !_isOpened;
if (true == _isOpened)
_controller.forward();
else
_controller.reverse();
}
}
That's because you are moving the items outside your Stack so you can't hitTest on those positions.
I modified a few lines of your code to make it works :
Add constraints to your Stack (you can use SizedBox also) , I'm using container to set a different color of background.
Widget _buildFab(AnimationController controller) {
return AnimatedBuilder(
animation: controller,
builder: (context, builder) {
return Container(
width: MediaQuery.of(context).size.width,
height: 100,
color: Colors.blueGrey,
child: Stack(
children: List.generate(_buttonsData.length,
(index) => _buildStackWidget(context, index))),
);
});
}
Center your items
Widget _buildMenuItem(int index, Map<String, dynamic> buttonData) {
return Center(
child: Transform.translate(
offset: Offset((1 + index) * _transform.value, 0.0),
child: FloatingActionButton(
heroTag: 100 + index,
backgroundColor: buttonData['color'],
//onPressed: () => _onSelected(index),
onPressed: () => print('click'),
child: Icon(buttonData['icon']),
),
),
);
}
Widget _buildMenuButton(int index, Map<String, dynamic> buttonData) {
return Center(
child: FloatingActionButton(
heroTag: 200,
backgroundColor: buttonData['color'],
onPressed: _toggle,
child: Icon(buttonData['icon']),
),
);
}
I have two animating menus. dropDownMenu is working fine, and I can set onPress events from this child to perform functions in the parent class using callbacks, for example:
class dropDownMenu extends StatefulWidget {
final Function() onPressed;
final String tooltip;
final IconData icon;
final _callback;
dropDownMenu({Key key, this.onPressed, this.tooltip, this.icon, #required void singlePlayerCallbacks(String callBackType) } ):
_callback = singlePlayerCallbacks, super(key: key);
#override
_dropDownMenuState createState() => _dropDownMenuState();
}
class _dropDownMenuState extends State<dropDownMenu>
with SingleTickerProviderStateMixin {
#override
Widget build(BuildContext context) {
return Stack(
children: <Widget> [
Column(
Container(
child: Opacity(
opacity: 0.0,
child: FloatingActionButton(
heroTag: null,
onPressed: isOpened == true? (){
widget?._callback('dice');
} : () {},
),
),
),
],
);
}
And then in the parent class:
class SinglePlayerMode extends StatefulWidget {
#override
SinglePlayerModeParentState createState() => SinglePlayerModeParentState();
}
class SinglePlayerModeParentState extends State<SinglePlayerMode> {
callBacks(String callBackType) {
switch(callBackType) {
case 'dice':
{
diceVisible = !diceVisible;
int rng = new Random().nextInt(19) + 1;
setState(() {
diceResult = rng;
});
}
}
break;
}
#override
Widget build(BuildContext context) {
child: Scaffold(
body: Container(
Padding(
padding: EdgeInsets.all(0.0),
child: dropDownMenu(
singlePlayerCallbacks: callBacks,
),
),
),
),
}
As a quick example, and this works perfectly fine.
What I need to do next is have another animated menu, called styleMenu, that animates when a button from dropDownMenu is pressed. This is where I am running into massive hurdles. I honestly don't mind HOW I get this done, I just need to get it done. This is what I am trying currently, without any success:
In dropDownMenu I have another button with a callback to the parent first:
Container(
child: Opacity(
opacity: 0.0,
child: FloatingActionButton(
heroTag: null,
onPressed: isOpened == true? (){
widget?._callback('theme');
} : () {},
),
),
),
Which triggers the callback function of the parent again, with a different switch case:
callBacks(String callBackType) {
case 'theme':
{
styleMenuState().animate();
}
break;
I obviously can't do this because it tells me that I am trying to animate a null object. Like I somehow have to instantiate styleMenu before I can call this function from here, but I don't know how to do this or even if it is possible.
My styleMenu class (extract):
class styleMenu extends StatefulWidget {
final Function() onPressed;
final String tooltip;
final IconData icon;
final _callback;
final VoidCallback animate;
styleMenu({this.onPressed, this.tooltip, this.animate, this.icon, #required void singlePlayerCallbacks(String callBackType) } ):
_callback = singlePlayerCallbacks;
#override
styleMenuState createState() => styleMenuState();
}
class styleMenuState extends State<styleMenu>
with SingleTickerProviderStateMixin {
bool isOpened = false;
AnimationController animationController;
Animation<Color> _buttonColor;
Animation<double> _animateIcon;
Animation<double> _translateButton;
Curve _curve = Curves.easeOut;
double _fabHeight = 52.0;
#override
initState() {
animationController =
AnimationController(vsync: this, duration: Duration(milliseconds: 600))
..addListener(() {
setState(() {});
});
_animateIcon =
Tween<double>(begin: 0.0, end: 1.0).animate(animationController);
_buttonColor = ColorTween(
begin: Colors.blue,
end: Colors.red,
).animate(CurvedAnimation(
parent: animationController,
curve: Interval(
0.0,
1.0,
curve: Curves.linear,
),
));
_translateButton = Tween<double>(
begin: 0.0,
end: _fabHeight,
).animate(CurvedAnimation(
parent: animationController,
curve: Interval(
0.0,
1.0,
curve: _curve,
),
));
super.initState();
}
#override
dispose() {
animationController.dispose();
super.dispose();
}
animate() {
if (!isOpened) {
styleMenuState().animationController.forward();
} else {
styleMenuState().animationController.reverse();
}
isOpened = !isOpened;
}
#override
Widget build(BuildContext context) {
return Stack(
children: <Widget> [
Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Stack(
children: <Widget>[
Transform(
transform: Matrix4.translationValues(
0,
_translateButton.value,
0,
),
child: blueTheme(),
),
Transform(
transform: Matrix4.translationValues(
0,
_translateButton.value * 2,
0,
),
child: greenTheme(),
),
Transform(
transform: Matrix4.translationValues(
0,
_translateButton.value * 3,
0,
),
child: redTheme(),
),
blackTheme(),
],
),
],
),
);
}
Again, I just need to be able to trigger the animate function from the styleMenu to pop this menu out by pressing a button inside the dropDownMenu and I just can't get my head around how to do this! I am sure there must be a simple way, but I am unable to find anything online.
Any pros out there?
I was just working on a similar problem. Using Streams, Provider with ChangeNotifiers, or inherited widgets are viable options. If you do want to simply trigger the child widget animations on setState called from the parent you can do that by using the didUpdateWidget in the child widget as shown below
#override
void didUpdateWidget(ImageHeartAnimation oldWidget) {
_controller.forward().orCancel;
super.didUpdateWidget(oldWidget);
}