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:
Then the user should be able to select one or the other, and the buttons should toggle back and forth like so:
However, with the current way that my code is set up, the buttons could both be toggled at the same time, like this:
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!
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.
);
}
}