Search code examples
flutterdartmobiletoggle

Toggle an animation between two separate Card classes in a Dialog with Flutter


In my Flutter application, I have a function that will open a dialog that shows two Stateful cards. I'm hoping to make it so that when one card is pressed, it will light up and the animation will run. Then, the other card will fade. However, in the current configuration, both options can be selected at once, which in a production setting might confuse the user. When the dialog opens, it should look like this:

enter image description here

Then the user should be able to select one or the other, and the buttons should toggle back and forth like so:

enter image description here enter image description here

However, with the current way that my code is set up, the buttons could both be toggled at the same time, like this:

enter image description here

I haven't been able to figure out how to change the way that my code works to fit this. I've tried using Flutter's native ToggleButtons class, but I haven't been able to make it work to fit my needs in this project. Here's the code:

class CustomRoomStateCard extends StatefulWidget {
  final bool isPublicCard; // true: card is green, false: card is red
  static bool
      choice; //true: user's room will be public, false: user's room will be private

  CustomRoomStateCard({this.isPublicCard});
  @override
  _CustomRoomStateCardState createState() => _CustomRoomStateCardState();
}

class _CustomRoomStateCardState extends State<CustomRoomStateCard>
    with SingleTickerProviderStateMixin {
  AnimationController controller;
  Animation animation;

  @override
  void initState() {
    super.initState();

    controller = AnimationController(
      upperBound: 1,
      duration: Duration(milliseconds: 200),
      vsync: this,
    );
    animation = ColorTween(
            begin: (widget.isPublicCard == true
                ? Colors.green[100]
                : Colors.red[100]),
            end: (widget.isPublicCard == true ? Colors.green : Colors.red))
        .animate(controller);
    controller.addListener(() {
      setState(() {});
    });
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
        onTap: () {
          setState(() {
            if (widget.isPublicCard == true) {
              CustomRoomStateCard.choice = true;
            } else {
              CustomRoomStateCard.choice = false;
            }
            if (animation.isCompleted) {
              controller.reverse();
              CustomRoomStateCard.choice = false;

              print("choice is ${CustomRoomStateCard.choice}");
            } else {
              controller.forward();
              print("choice is ${CustomRoomStateCard.choice}");
            }
          });
        },
        child: Card(
          shape:
              RoundedRectangleBorder(borderRadius: BorderRadius.circular(15.0)),
          color: animation.value,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Padding(
                padding: EdgeInsets.all(15.0),
                child: widget.isPublicCard
                    ? Icon(Icons.radar, color: Colors.white)
                    : Icon(Icons.shield, color: Colors.white),
              ),
              Padding(
                  padding: EdgeInsets.all(15.0),
                  child: Text(
                    widget.isPublicCard ? "Public" : "Private",
                    style: kBoldText.copyWith(color: Colors.white),
                    textAlign: TextAlign.center,
                  ))
            ],
          ),
        ));
  }
}

Future<void> showPublicPrivateChoiceDialog(BuildContext context) {
  List<bool> toggledValues = [false, false]; // an idea
  return showDialog(
      context: context,
      builder: (context) {
        return AlertDialog(
          shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.all(Radius.circular(20.0))),
          title: Text(
            "Set room privacy level",
            style: TextStyle(fontWeight: FontWeight.bold),
          ),
          content: Container(
            height: MediaQuery.of(context).size.height * 0.2,
            width: MediaQuery.of(context).size.height * 0.7,
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                Expanded(
                  child: CustomRoomStateCard(
                    isPublicCard: true,
                  ),
                ),
                Expanded(
                  child: CustomRoomStateCard(
                    isPublicCard: false,
                  ),
                )
              ],
            ),
          ),
          actions: [
            TextButton(
                onPressed: () {
                  print("the choice is ${CustomRoomStateCard.choice}");
                  isBroadcasting = CustomRoomStateCard.choice ??
                      true; // default to true in case they don't press anything
                  Navigator.pop(context);
                  return;
                },
                child: Text(
                  "Create",
                  style: TextStyle(fontWeight: FontWeight.bold),
                ))
          ],
        );
      });
}

My first thought would be to make a boolean variable that is true if one of the cards is already active. When I press a card, it would check this variable, change itself accordingly, but then would also have to call setState() in the other card, which I'm not sure how to do at the moment. How can I make it so these two cards will toggle back and forth and not be active at the same time? Any assistance would be greatly appreciated!


Solution

  • This depends on how much control you need over your animations. But if you don't need the controls, you can user AnimatedOpacity(..) to achieve this.

    See this example:

    import 'package:flutter/material.dart';
    
    void main() {
      runApp(const MyApp());
    }
    
    class MyApp extends StatelessWidget {
      const MyApp({Key? key}) : super(key: key);
    
      // This widget is the root of your application.
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: const MyHomePage(title: 'Flutter Demo Home Page'),
        );
      }
    }
    
    class MyHomePage extends StatefulWidget {
      const MyHomePage({Key? key, required this.title}) : super(key: key);
    
      final String title;
    
      @override
      State<MyHomePage> createState() => _MyHomePageState();
    }
    
    class _MyHomePageState extends State<MyHomePage> {
      bool isPublic = true;
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text(widget.title),
          ),
          body: Container(
              child: Column(
            children: [
              AnimatedOpacity(
                duration: const Duration(milliseconds: 500),
                opacity: isPublic ? 1.0 : 0.20,
                child: Card(
                  child: InkWell(
                    onTap: () {
                      setState(() {
                        isPublic = true;
                      });
    
                      print('is public = true');
                    },
                    child: SizedBox(
                      child: Text('Public'),
                      height: 120,
                      width: 120,
                    ),
                  ),
                  color: Colors.green[600],
                ),
              ),
              SizedBox(height: 20),
              AnimatedOpacity(
                duration: const Duration(milliseconds: 500),
                opacity: !isPublic ? 1.0 : 0.20,
                child: Card(
                  child: InkWell(
                    onTap: () {
                      setState(() {
                        isPublic = false;
                      });
    
                      print('is public = false');
                    },
                    child: SizedBox(
                      child: Text('Private'),
                      height: 120,
                      width: 120,
                    ),
                  ),
                  color: Colors.red[600],
                ),
              ),
            ],
          )), // This trailing comma makes auto-formatting nicer for build methods.
        );
      }
    }