6
votes

I am trying to make a radial menu in Flutter and want my menu button to have a rotation animation every time pressed. I followed TensorProgramming's tutorial on Youtube on The Basics of Animation in Flutter but my animation for some reason does not work. Below I have included a code of my RadialMenuWidget.

I have properly disposed the animation controller, extended the widget's state with SingleTickerProviderStateMixin. P.S I am running on an emulator and the rotation of the icon get's changed sometimes when I hot reload.

Any help would be deeply appreciated!

import 'package:flutter/material.dart';
import 'package:savings/utils/colors.dart';
import 'package:savings/widgets/themed_radial_menu_item.dart';

class CustomThemedRadialMenu extends StatefulWidget {
final List<CustomThemedRadialMenuItem> items;

 CustomThemedRadialMenu({@required this.items});

@override
_CustomThemedRadialMenuState createState() => 
_CustomThemedRadialMenuState();
}

class _CustomThemedRadialMenuState extends State<CustomThemedRadialMenu>
   with SingleTickerProviderStateMixin {
   Animation animationOpenClose;
   AnimationController animationControllerOpenClose;

 bool isOpen;

@override
void initState() {
  isOpen = false;

  animationControllerOpenClose =
      AnimationController(duration: new Duration(seconds: 5), vsync: this);

  animationOpenClose =
      Tween(begin: 0.0, end: 360.0).animate(animationControllerOpenClose)
        ..addListener(() {
          setState(() {});
        });

  animationControllerOpenClose.repeat();

  super.initState();
  }

 @override
 Widget build(BuildContext context) {
  ///A list of the items and the center menu button
  final List<Widget> menuContents = <Widget>[];

 for (int i = 0; i < widget.items.length; i++) {
   ///Menu items
   menuContents.add(widget.items[1]);

    ///Menu Close/Open button
    menuContents.add(new InkWell(
      onTap: () {},
      child: Container(
       padding: new EdgeInsets.all(10.0),
        decoration: new BoxDecoration(
           color: Colors.white,
          border: new Border.all(color: darkHeadingsTextColor),
          shape: BoxShape.circle,
        ),
        child: new Transform.rotate(
            angle: animationControllerOpenClose.value,
            child: isOpen ? new Icon(Icons.clear) : new Icon(Icons.menu)),
      ),
    ));
  }

  return new Stack(
    alignment: Alignment.center,
   children: menuContents,
 );
}

@override
void dispose() {
  animationControllerOpenClose.dispose();

  super.dispose();
 }

closeMenu() {
  animationControllerOpenClose.forward();
  setState(() {
    isOpen = false;
  });

  print("RadialMenu Closed");
}

openMenu() {
  animationControllerOpenClose.forward();
  setState(() {
    isOpen = true;
  });

 print("RadialMenu Opened");
}
}
1

1 Answers

20
votes

There's a few issues with what you're doing. But a quick FYI - if you clean up your code and make an encapsulated problem, people are more likely to help you. That would entail removing any of your classes that aren't included, and ideally posting a solution that can be pasted into a single file and run as-is.

That being said, I've implemented what I think you were trying to do. I've removed a few of the things you had in there so that it actually builds, so you'll need to add them back in.

The major issues you had are the following:

  1. SetState in listener

    ..addListener(() {
      setState(() {});
    });
    

This is not ideal because it will force the entire widget to rebuild every single tick of the animation. That's going to cause serious performance issues for widget that isn't very basic. Use an AnimatedBuilder instead. Or if you're looking for when the animation ends use ..addStateListener

  1. Angles are in Radians in flutter. I didn't bother importing Math for the Pi constant but you probably should.

  2. You have an on-tap that wasn't actually calling openMenu or closeMenu, so it would definitely not do anything =D.

  3. You'd set the animation controller to repeat. That means it would continue forever.

  4. You weren't passing anything in to the animationControllerOpenClose.forward calls. That means it would animate to the 1.0 state from wherever it currently was, even if that was already 1.0. I've just passed in 0, but you may want to do something around having open/close (if the user taps while it is animating or something).

  5. (tangential problem) You seem to be adding a new Menu Open/Close button after each menu widget. That might be what you're wanting to do, but I'd guess you just want to add one.

Anyways, here's the example that works. Each time you tap, it changes icons and then rotates.

import 'package:flutter/material.dart';

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "Spinnig Menu",
      theme: ThemeData(
        primaryColor: Colors.red,
      ),
      home: new Scaffold(
        body: new SafeArea(
          child: new Column(
            children: <Widget>[new CustomThemedRadialMenu()],
          ),
        ),
      ),
    );
  }
}

class CustomThemedRadialMenu extends StatefulWidget {

  @override
  _CustomThemedRadialMenuState createState() => _CustomThemedRadialMenuState();
}

class _CustomThemedRadialMenuState extends State<CustomThemedRadialMenu> with SingleTickerProviderStateMixin {
  Animation animationOpenClose;
  AnimationController animationControllerOpenClose;

  bool isOpen;

  @override
  void initState() {
    isOpen = false;
    animationControllerOpenClose = AnimationController(duration: new Duration(seconds: 5), vsync: this);
    animationOpenClose = Tween(begin: 0.0, end: 3.14159 * 2).animate(animationControllerOpenClose);
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    ///A list of the items and the center menu button
    final List<Widget> menuContents = <Widget>[];

    ///Menu Close/Open button
    menuContents.add(new InkWell(
      onTap: () {
        if (isOpen) {
          closeMenu();
        } else {
          openMenu();
        }
      },
      child: Container(
          padding: new EdgeInsets.all(10.0),
          decoration: new BoxDecoration(
            color: Colors.white,
            border: new Border.all(color: Colors.black38),
            shape: BoxShape.circle,
          ),
          child: new AnimatedBuilder(
            animation: animationControllerOpenClose,
            builder: (context, child) {
              return new Transform.rotate(angle: animationOpenClose.value, child: child);
            },
            child: isOpen ? new Icon(Icons.clear) : new Icon(Icons.menu),
          )),
    ));

    return new Stack(
      alignment: Alignment.center,
      children: menuContents,
    );
  }

  @override
  void dispose() {
    animationControllerOpenClose.dispose();

    super.dispose();
  }

  closeMenu() {
    animationControllerOpenClose.forward(from: 0.0);
    setState(() {
      isOpen = false;
    });

    print("RadialMenu Closed");
  }

  openMenu() {
    animationControllerOpenClose.forward(from: 0.0);
    setState(() {
      isOpen = true;
    });

    print("RadialMenu Opened");
  }
}

Of note is that I build the minimum possible amount of widgets in the AnimatedBuilder's build function. The less that's built there, the better performance-wise. Because the icon itself doesn't change as part of the rotation, you can simply pass it in as the child.

I'm guessing this might come up next so just an FYI - you can use an animated cross fade widget to make the transition between icons (just drop this in for the AnimatedBuilder's child):

new AnimatedCrossFade(
            firstChild: new Icon(Icons.clear),
            secondChild: new Icon(Icons.menu),
            crossFadeState: isOpen ? CrossFadeState.showFirst : CrossFadeState.showSecond,
            duration: Duration(milliseconds: 300)),