Multiple widgets use the same GlobalKey - flutter

I'm' currently facing this issue. I am developing a quiz app and whenever an answer from a questions is pressed, the app should scroll down to the next question but I'm getting this error:
I am assigning 10 keys which are then thrown in a custom widget which takes them in a function and on a press of any of the buttons the app should scroll down to the next question: These is how a quiz question looks like:
Main Code where I call the custom widget:
Questions(
Keys.key1,
'assets/images/malware_quiz.jpeg',
'1. What is a malware?',
'Designed to damage computers, servers or any other devices',
"Used to get user's credentials",
"It's used to destroy networks",
true,
false,
false,
Color(0xFF383c40),
Color(0xff5e517d),
media.height),
Questions(
Keys.key2,
'assets/images/cyberattack.jpg',
'2. What is the most used cyber-attack?',
'DDoS',
'Ransomware',
'Phishing',
false,
false,
true,
Color(0xFF383c40),
Color(0xffe9755c),
media.height * 2),
SizedBox(
height: media.width > 600 ? heightQuestions : 250,
),
Questions(
Keys.key3,
'assets/images/ransomware_quiz.png',
'3. What type of attack is this?',
'Phishing',
'Ransomware',
'Zero-day exploit',
false,
true,
false,
Color(0xFF383c40),
Color(0xff061f3e),
media.height * 3),
Custom class for the keys:
class Keys {
static final key1 = GlobalKey();
static final key2 = GlobalKey();
static final key3 = GlobalKey();
static final key4 = GlobalKey();
static final key5 = GlobalKey();
static final key6 = GlobalKey();
static final key7 = GlobalKey();
static final key8 = GlobalKey();
static final key9 = GlobalKey();
static final key10 = GlobalKey();
static final key11 = GlobalKey();
}
The custom widget where I set the function to scroll:
class Questions extends StatefulWidget {
final String imagePath;
final String question;
final String answer1;
final String answer2;
final String answer3;
final bool iscorrectAnswer1;
final bool iscorrectAnswer2;
final bool iscorrectAnswer3;
final double offset;
final GlobalKey key;
final Color colorTop;
final Color colorBot;
int score = 0;
bool questionsAnswered = false;
Questions(
this.key,
this.imagePath,
this.question,
this.answer1,
this.answer2,
this.answer3,
this.iscorrectAnswer1,
this.iscorrectAnswer2,
this.iscorrectAnswer3,
this.colorBot,
this.colorTop,
this.offset,
);
#override
_QuestionsState createState() => _QuestionsState();
}
class _QuestionsState extends State<Questions> {
disableButton() {
setState(() {
widget.questionsAnswered = true;
});
}
#override
Widget build(BuildContext context) {
var media = MediaQuery.of(context).size;
void scrollPage() {
Scrollable.ensureVisible(widget.key.currentContext!,
alignment: 1, duration: Duration(seconds: 2), curve: Curves.ease);
print(widget.offset);
}
void createSnackBar(String text, Color c) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Container(
alignment: Alignment.center,
height: media.width > 600 ? 100 : 50,
child: Text(
text,
style: TextStyle(fontSize: 30, color: Colors.white),
)),
duration: const Duration(milliseconds: 4000),
width: 500.0,
backgroundColor: c,
padding: EdgeInsets.symmetric(horizontal: 8.0),
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
));
}
return Align(
alignment: Alignment.topCenter,
child: Container(
key: widget.key,
width: 500,
height: media.width < 600 ? 600 : 900,
decoration: BoxDecoration(
border: Border.all(width: 1, color: Color(0xFFdbe1e4)),
borderRadius: BorderRadius.all(Radius.circular(15)),
color: widget.colorBot,
),
child: Column(
children: [
Align(
alignment: Alignment.topCenter,
child: Padding(
padding: EdgeInsets.only(
top: 0,
),
child: Container(
width: 700,
height: media.width >= 600 ? 500 : 350,
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
color: Colors.white.withOpacity(0.4),
spreadRadius: 2,
blurRadius: 3,
offset: Offset(0, 4)),
],
border: Border.all(
width: 1, color: Color(0xFFdbe1e4)),
borderRadius:
BorderRadius.all(Radius.circular(15)),
color: widget.colorTop,
),
child: Column(children: [
Text(
widget.question,
style: TextStyle(
color: Colors.white,
fontSize: media.width > 600 ? 30 : 25,
),
),
Padding(
padding: EdgeInsets.only(top: 15),
child: Image.asset(
widget.imagePath,
height: media.width > 600 ? 300 : 250,
width: media.width > 600 ? 400 : 300,
),
)
])))),
Padding(
padding: EdgeInsets.only(
top: 40,
),
child: SizedBox(
width: 300,
height: 60,
child: ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(15)),
child: ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(
Color(0xFF304e60),
),
),
child: Text(
widget.answer1,
style: TextStyle(
color: Colors.white,
fontSize: 15,
),
),
onPressed: widget.questionsAnswered == false
? () {
setState(() {
if (widget.iscorrectAnswer1 == true) {
scrollPage(); //THIS IS WHERE I USE THE FUNCTION TO SCROLL ON ALL BUTTONS
createSnackBar(
'Correct!', Color(0xFFa4d2ac));
disableButton();
Score.score++;
} else {
scrollPage();
disableButton();
createSnackBar(
'Wrong Answer!', Color(0xFFEA4C46));
}
});
}
: null),
P.S. Removed some of the unnecessary code

you have to create and Initialize the global key dynamically Please look at the code snippet
``
`1.Map<int, GlobalKey> globalKeyMap = {}; 2.void initializeKeyMap() { for (int index = 0; index < yourListname.length; index++) { globalKeyMap[index] = GlobalKey(); }} 3.#override void initState() { super.initState(); initializeKeyMap(); } 4.Container( key: globalKeyMap[index],child:.......<your Code>)`

I solved this issue by creating another parameter in the Questions() widget and set up a key for each widget. Then I would scroll to the next Question() widget using the moveToWidget key
Code:
class Questions extends StatefulWidget {
final String imagePath;
final String question;
final String answer1;
final String answer2;
final String answer3;
final bool iscorrectAnswer1;
final bool iscorrectAnswer2;
final bool iscorrectAnswer3;
final GlobalKey moveToWidgetKey;
final Color colorTop;
final Color colorBot;
int score = 0;
bool questionsAnswered = false;
Questions({
Key? key, //Set a unique key for each Question Widget
required this.moveToWidgetKey, // Set the key of the next Question widget to scroll at
required this.imagePath,
required this.question,
required this.answer1,
required this.answer2,
required this.answer3,
required this.iscorrectAnswer1,
required this.iscorrectAnswer2,
required this.iscorrectAnswer3,
required this.colorBot,
required this.colorTop,
}) : super(key: key);
scroll() {
Scrollable.ensureVisible(widget.moveToWidgetKey.currentContext! //NEW CODE,
duration: Duration(seconds: 2));
//Instantiate a Question
Questions(
key: Keys.key2,
moveToWidgetKey: Keys.key3,
imagePath: 'assets/images/cyberattack.jpg',
question: '2. What is the most used cyber-attack?',
answer1: 'DDoS',
answer2: 'Ransomware',
answer3: 'Phishing',
iscorrectAnswer1: false,
iscorrectAnswer2: false,
iscorrectAnswer3: true,
colorBot: Color(0xFF383c40),
colorTop: Color(0xffe9755c),
),

Related

Flutter How to stack image and total member text

I was able to show the pictures as in the video by taking advantage of Johannes Milke's video that I left the link of. But that's not all I want. I need a structure that looks like these images but shows the total number of users. I leave the image of exactly what I want and my related codes.
What I want to achieve; Creating a structure where I can write the total number of users
There are a few packages on pub dev but not what I wanted. Thank you
Video:Source Video
image i want to make:
My stacked widget:
import 'package:flutter/material.dart';
class StackedWidgets extends StatelessWidget {
final List<Widget> items;
final TextDirection direction;
final double size;
final double xShift;
const StackedWidgets({
Key? key,
required this.items,
this.direction = TextDirection.ltr,
this.size = 100,
this.xShift = 20,
}) : super(key: key);
#override
Widget build(BuildContext context) {
final allItems = items
.asMap()
.map((index, item) {
final left = size - xShift;
final value = Container(
width: size,
height: size,
child: item,
margin: EdgeInsets.only(left: left * index),
);
return MapEntry(index, value);
})
.values
.toList();
return Stack(
children: direction == TextDirection.ltr
? allItems.reversed.toList()
: allItems,
);
}
}
Usage my stacked widget:
Widget buildStackedImages({
TextDirection direction = TextDirection.ltr,
}) {
final double size = 100;
final double xShift = 20;
final urlImages = [
'https://images.unsplash.com/photo-1554151228-14d9def656e4?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=633&q=80',
'https://images.unsplash.com/photo-1494790108377-be9c29b29330?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=634&q=80',
'https://images.unsplash.com/photo-1616766098956-c81f12114571?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=634&q=80',
];
final items = urlImages.map((urlImage) => buildImage(urlImage)).toList();
return StackedWidgets(
direction: direction,
items: items,
size: size,
xShift: xShift,
);
}
Widget buildImage(String urlImage) {
final double borderSize = 5;
return ClipOval(
child: Container(
padding: EdgeInsets.all(borderSize),
color: Colors.white,
child: ClipOval(
child: Image.network(
urlImage,
fit: BoxFit.cover,
),
),
),
);
}
Add a label also to this class
import 'package:flutter/material.dart';
class StackedWidgets extends StatelessWidget {
final List<Widget> items;
final TextDirection direction;
final double size;
final double xShift;
final String lable;
const StackedWidgets({
Key? key,
required this.items,
this.direction = TextDirection.ltr,
this.size = 100,
this.xShift = 20,
this.label = '',
}) : super(key: key);
#override
Widget build(BuildContext context) {
final allItems = items
.asMap()
.map((index, item) {
final left = size - xShift;
final value = Container(
width: size,
height: size,
child: item,
margin: EdgeInsets.only(left: left * index),
);
return MapEntry(index, value);
})
.values
.toList();
return Row(
children: [
Stack(
children: direction == TextDirection.ltr
? allItems.reversed.toList()
: allItems,
),
Text(label),
]
);
}
}
In items pass the number widget and label too
return StackedWidgets(
direction: direction,
items: [...items, Container(
width: 25,//you can also add padding if required
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.orange,
),
child: Text('+22'))],
size: size,
xShift: xShift,
label: "users are here",
);
I will prefer this way,
Run on dartPad
class InTEST extends StatefulWidget {
const InTEST({Key? key}) : super(key: key);
#override
State<InTEST> createState() => _InTESTState();
}
class _InTESTState extends State<InTEST> {
int maxRenderAvatar = 5;
int numberOfActiveUser = 33;
double size = 100;
double borderSize = 5;
Widget buildStackedImages({
TextDirection direction = TextDirection.ltr,
}) {
List<String> urlImages = List.filled(
numberOfActiveUser, //based on your list
'https://images.unsplash.com/photo-1554151228-14d9def656e4?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=633&q=80');
List<Widget> items = [];
final renderItemCount = numberOfActiveUser > maxRenderAvatar
? maxRenderAvatar
: urlImages.length;
for (int i = 0; i < renderItemCount; i++) {
items.add(
Positioned(
left: (i * size * .8),
child: buildImage(
urlImages[i],
),
),
);
}
// add counter if urlImages.length > maxRenderAvatar
if (numberOfActiveUser > maxRenderAvatar) {
items.add(
Positioned(
left: maxRenderAvatar * size * .8,
child: Container(
width: size,
height: size,
padding: EdgeInsets.all(borderSize),
decoration: BoxDecoration(
border: Border.all(color: Colors.white, width: 4),
color: Colors.amber,
shape: BoxShape.circle,
),
alignment: Alignment.center,
child: Text(
"+ ${urlImages.length - maxRenderAvatar}",
style: TextStyle(
fontSize: 23,
),
),
),
),
);
}
return SizedBox(
height: size + (borderSize * 2), //10 for borderSize
width: MediaQuery.of(context).size.width,
child: Stack(
children: items,
),
);
}
Widget buildImage(String urlImage) {
return ClipOval(
child: Container(
padding: EdgeInsets.all(borderSize),
color: Colors.white,
child: ClipOval(
child: Image.network(
urlImage,
width: size,
height: size,
fit: BoxFit.cover,
),
),
),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Slider(
value: numberOfActiveUser.toDouble(),
min: 0,
max: 55,
onChanged: (v) {
numberOfActiveUser = v.toInt();
setState(() {});
}),
Slider(
value: maxRenderAvatar.toDouble(),
min: 0,
max: 42,
onChanged: (v) {
maxRenderAvatar = v.toInt();
setState(() {});
}),
buildStackedImages(),
],
),
);
}
}

How to Update Clock Time To UTC TIME in Flutter

I used a flutter package called analog clock - link here to create a clock. I don't want to use Datetime.now but want to use a UTC time of +3. I've tweaked the UTC code a couple of times but still don't know to make it work with this package. There seems to be a bug with the package.
In my code I changed the date time to
DateTime.now().toUtc().add(Duration(hours: 3)), to show UTC +3.
However, whenever I run the app, the clock keeps showing the current daytime now. I've tried tweaking the code but cant fix
Here is my code where I call the analog clock package
class TimeAndWeatherPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
var size = MediaQuery.of(context)
.size; //this gonna give us total height and with of our device
return Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
body: Stack(
children: <Widget>[
Container(
// Here the height of the container is 45% of our total height
height: size.height * .50,
decoration: BoxDecoration(
color: Theme.of(context).canvasColor,
),
),
SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
children: <Widget>[
Padding(
padding: EdgeInsets.fromLTRB(0, 50, 0, 10),
child: Center(
child: TitleText(
text: "time",
)),
),
CurrentDate(),
Center(
child: AnalogClock(
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).shadowColor, width: 0.5),
color: Theme.of(context).cardColor,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
offset: Offset(0.0, 10.0),
blurRadius: 2.0,
spreadRadius: 0.0,
color: Theme.of(context).shadowColor,
),
],
),
width: 270.0,
height: 400,
isLive: true,
showTicks: true,
showAllNumbers: true,
showDigitalClock: true,
showNumbers: true,
showSecondHand: true,
hourHandColor: Theme.of(context).iconTheme.color,
minuteHandColor: Theme.of(context).iconTheme.color,
secondHandColor: Color(0xB35FD245),
numberColor: Theme.of(context).iconTheme.color,
textScaleFactor: 1.2,
digitalClockColor: Theme.of(context).iconTheme.color,
datetime:
DateTime.now().toUtc().add(Duration(hours: 3)),
),
),
],
),
),
)
],
),
);
}
}
here is the original code of the package
class AnalogClock extends StatefulWidget {
final DateTime datetime;
final bool showDigitalClock;
final bool showTicks;
final bool showNumbers;
final bool showAllNumbers;
final bool showSecondHand;
final Color hourHandColor;
final Color minuteHandColor;
final Color secondHandColor;
final Color tickColor;
final Color digitalClockColor;
final Color numberColor;
final bool isLive;
final double textScaleFactor;
final double width;
final double height;
final BoxDecoration decoration;
const AnalogClock(
{this.datetime,
this.showDigitalClock = true,
this.showTicks = true,
this.showNumbers = true,
this.showSecondHand = true,
this.showAllNumbers = false,
this.hourHandColor = Colors.black,
this.minuteHandColor = Colors.black,
this.secondHandColor = Colors.redAccent,
this.tickColor = Colors.grey,
this.digitalClockColor = Colors.black,
this.numberColor = Colors.black,
this.textScaleFactor = 1.0,
this.width = double.infinity,
this.height = double.infinity,
this.decoration = const BoxDecoration(),
isLive,
Key key})
: this.isLive = isLive ?? (datetime == null),
super(key: key);
const AnalogClock.dark(
{datetime,
showDigitalClock = true,
showTicks = true,
showNumbers = true,
showAllNumbers = false,
showSecondHand = true,
width = double.infinity,
height = double.infinity,
decoration = const BoxDecoration(),
Key key})
: this(
datetime: datetime,
showDigitalClock: showDigitalClock,
showTicks: showTicks,
showNumbers: showNumbers,
showAllNumbers: showAllNumbers,
showSecondHand: showSecondHand,
width: width,
height: height,
hourHandColor: Colors.white,
minuteHandColor: Colors.white,
secondHandColor: Colors.redAccent,
tickColor: Colors.grey,
digitalClockColor: Colors.white,
numberColor: Colors.white,
decoration: decoration,
key: key);
#override
_AnalogClockState createState() => _AnalogClockState(datetime);
}
class _AnalogClockState extends State<AnalogClock> {
DateTime datetime;
_AnalogClockState(datetime) : this.datetime = datetime ?? DateTime.now();
initState() {
super.initState();
if (widget.isLive) {
// update clock every second or minute based on second hand's visibility.
Duration updateDuration =
widget.showSecondHand ? Duration(seconds: 1) : Duration(minutes: 1);
Timer.periodic(updateDuration, update);
}
}
update(Timer timer) {
if (mounted) {
// update is only called on live clocks. So, it's safe to update datetime.
datetime = DateTime.now();
setState(() {});
}
}
#override
Widget build(BuildContext context) {
return Container(
width: widget.width,
height: widget.height,
decoration: widget.decoration,
child: Center(
child: AspectRatio(
aspectRatio: 1.0,
child: new Container(
constraints: BoxConstraints(minWidth: 48.0, minHeight: 48.0),
width: double.infinity,
child: new CustomPaint(
painter: new AnalogClockPainter(
datetime: datetime,
showDigitalClock: widget.showDigitalClock,
showTicks: widget.showTicks,
showNumbers: widget.showNumbers,
showAllNumbers: widget.showAllNumbers,
showSecondHand: widget.showSecondHand,
hourHandColor: widget.hourHandColor,
minuteHandColor: widget.minuteHandColor,
secondHandColor: widget.secondHandColor,
tickColor: widget.tickColor,
digitalClockColor: widget.digitalClockColor,
textScaleFactor: widget.textScaleFactor,
numberColor: widget.numberColor),
)))),
);
}
}
It looks like you might have a few things going on here.
You should be updating the datetime property within the setState call:
update(Timer timer) {
if (!mounted) return;
setState(() => datetime = DateTime.now());
}
You're updating the time without setting the timezone again, which would revert it to your local time. So the above should look like this:
update(Timer timer) {
if (!mounted) return;
setState(() => datetime = DateTime.now().toUtc());
// Or to an arbitrary time in the future, if that's what you need:
// setState(() => datetime = DateTime.now().toUtc().add(const Duration(hours: 3)));
}
Aside from that...
You should store a reference to the timer, so you can cancel() it in the widget's dispose() method.
Note that you are only updating the "mode" of the clock (seconds, or minutes) in the initState method of your widget, which only gets called once in the lifetime of a widget. It doesn't get called again when the widget is updated. For that you need to use didUpdateWidget.
OR...
I highly recommend you check out the Riverpod package, as it makes state management a lot easier. For example, the above code could look something like this...
final clockProvider = StateProvider<DateTime>((ref) => DateTime.now().toUtc());
class CustomClock extends ConsumerWidget{
...
#override
Widget build(BuildContext context, WidgetRef ref) {
return AnalogClock(
...
datetime: ref.watch(clockProvder),
...
);
}
}
class ClockModeButton extends ConsumerWidget{
...
#override
Widget build(BuildContext context, WidgetRef ref) {
return TextButton(
...
onPressed: () {
final DateTime newDateTime = DateTime.now().toUtc().add(const Duration(hours: 3));
ref.read(clockPrivder.notifier).state = newDateTime;
},
...
);
}
}

flutter reset only custom widget state after it's optional function execution

I have a page where we have some pickup session when you select a pickup session at the bottom SwipeActionButton widget activate
now user can swipe right side and after swipe complete an async function execute which most time hit an api so if api result is success app goes to next page no problem here but if api result gave an error it shows a dialog
Press Ok and dialog pop but SwipeActionButton widget still show complete swipe how I can reset it.
code
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: NormalAppBar(
title: Text("Assign Requests"),
),
body: Consumer<PickupSessionProvider>(
builder: (context, provider, child) => Stack(
children: [
widget.requestIds.length == 0
? _requestsLoaded
? provider.unassignedRequestCount == 0
? Center(
child: Text("No requests.",
style: Theme.of(context).textTheme.headline6),
)
: _buildRequestsList(provider.unassignedRequests!)
: Center(
child: CircularProgressIndicator(),
)
: provider.pickupSessions!.length == 0
? Center(
child: Text("No active pickup session.",
style: Theme.of(context).textTheme.headline6),
)
: ListView(
padding: const EdgeInsets.only(bottom: 80),
children: [
Padding(
padding: const EdgeInsets.all(20),
child: Text(
"Select pickup session",
style: Theme.of(context).textTheme.headline4,
),
),
for (var pickupSession in provider.pickupSessions!)
_buildPickupSessionTile(pickupSession)
],
),
Positioned(
bottom: 0,
child: SwipeActionButton(
margin: EdgeInsets.symmetric(horizontal: 15, vertical: 20),
onDone: (_selectRequestList && requestIds.length == 0) ||
(!_selectRequestList &&
_selectedPickupSessionId == null)
? null
: () async {
var result = await showDialog(
context: context,
builder: (context) => _ProgressDialog(
requestIds: requestIds,
pickupSessionId: _selectedPickupSessionId),
barrierDismissible: true);
if (result == true) Navigator.of(context).pop(true);
},
doneText: "Assign request",
disabledText: "Assign request",
infoText: "Swipe to assign request",
),
)
],
),
),
);
}
Custom SwipeActionButton widget
class SwipeActionButton extends StatefulWidget {
final double height;
final Color doneColor;
final Color swiperColor;
final Color textColor;
final String doneText;
final String disabledText;
final VoidCallback? onDone;
final String? infoText;
final double? width;
final Color? backgroundColor;
final EdgeInsetsGeometry margin;
SwipeActionButton({
Key? key,
this.height = 50,
this.doneColor = const Color(0xff44b31f),
this.swiperColor = const Color(0xff44b31f),
this.textColor = const Color(0xff44b31f),
required this.doneText,
required this.disabledText,
this.onDone,
this.infoText,
this.width,
this.backgroundColor,
this.margin = EdgeInsets.zero
}) : super(key: key);
#override
_SwipeActionButtonState createState() => _SwipeActionButtonState();
}
class _SwipeActionButtonState extends State<SwipeActionButton>
with SingleTickerProviderStateMixin {
double swipePercent = 0.0;
bool swipeDone = false;
bool isDisabled = false;
late Color backgroundColor;
late AnimationController _controller;
Animation<double>? _animation;
void initState() {
super.initState();
backgroundColor = widget.backgroundColor ?? Color(0xff3344b31f);
_controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 500))
..addListener(() {
setState(() {
swipePercent = _animation?.value ??0;
});
})
..addStatusListener((AnimationStatus status) {
if (status == AnimationStatus.completed && swipeDone) {
widget.onDone!();
}
});
}
void dispose() {
_controller.dispose();
super.dispose();
}
_onDragStart(DragStartDetails details) {
_controller.reset();
swipePercent = 0.0;
}
_onDragUpdate(DragUpdateDetails details) {
setState(() {
swipePercent =
details.globalPosition.dx / MediaQuery.of(context).size.width;
if (swipePercent > 0.90) swipeDone = true;
});
}
_onDragEnd(DragEndDetails details) {
if (swipePercent > 0.90 || swipeDone) {
_animation =
Tween<double>(begin: swipePercent, end: 1).animate(_controller);
} else {
_animation =
Tween<double>(end: 0, begin: swipePercent).animate(_controller);
}
_controller.forward();
}
#override
Widget build(BuildContext context) {
isDisabled = widget.onDone == null;
double screenWidth = MediaQuery.of(context).size.width;
return Container(
alignment: Alignment.center,
margin: widget.margin,
width: screenWidth - widget.margin.horizontal,
child: Stack(
clipBehavior: Clip.hardEdge,
children: <Widget>[
Container(
height: widget.height,
decoration: BoxDecoration(
color: isDisabled ? Colors.grey : Color(0xff3344b31f),
borderRadius: BorderRadius.all(Radius.circular(100.0)),
border: Border.all(
color: isDisabled ? Colors.grey : Color(0xff3344b31f),
width: 1.5,
),
),
child: Center(
child: Text(widget.infoText ?? "",
style: Theme.of(context)
.textTheme
.subtitle1!
.copyWith(color: widget.textColor))),
),
Container(
width: isDisabled
? screenWidth
: lerpDouble(widget.height, screenWidth, swipePercent),
height: widget.height,
child: Center(
child: Opacity(
opacity: isDisabled ? 1 : lerpDouble(0, 1, swipePercent)!,
child: Text(
isDisabled ? widget.disabledText : widget.doneText,
style: Theme.of(context).textTheme.subtitle1!.copyWith(
color: Colors.white,
),
textScaleFactor:
isDisabled ? 1 : lerpDouble(0, 1, swipePercent),
))),
decoration: BoxDecoration(
color: isDisabled ? Colors.grey : null,
borderRadius: BorderRadius.all(Radius.circular(100.0)),
/* border: Border.all(
color: Theme.of(context).primaryColor,
width: 1.5,
), */
gradient: isDisabled
? null
: LinearGradient(
begin: Alignment.center,
end: Alignment.centerRight,
colors: [
widget.doneColor,
swipeDone ? widget.doneColor : backgroundColor
])),
),
isDisabled
? Container()
: Positioned(
left: lerpDouble(
0, screenWidth -(15 +widget.margin.horizontal) - (widget.height * .9), swipePercent),
/* top: widget.height * .1,
bottom: widget.height * .1,
*/
child: AbsorbPointer(
absorbing: swipeDone,
child: GestureDetector(
onHorizontalDragStart: _onDragStart,
onHorizontalDragUpdate: _onDragUpdate,
onHorizontalDragEnd: _onDragEnd,
child: Opacity(
opacity: 1,
child: AnimatedContainer(
duration: Duration(milliseconds: 500),
height: widget.height,
width: widget.height,
decoration: BoxDecoration(
borderRadius:
BorderRadius.all(Radius.circular(100.0)),
border: Border.all(
color: Theme.of(context).primaryColor,
width: 1.5,
),
boxShadow: swipeDone
? null
: [
BoxShadow(
color: Colors.black45,
blurRadius: 4)
],
color: swipeDone
? backgroundColor
: widget.swiperColor),
child: swipeDone
? Icon(
Icons.check,
size: 20,
color: Colors.white,
)
: Icon(
Icons.arrow_forward,
size: 20,
color: Colors.white,
),
))))),
],
),
);
}
}
You're asking how you can make the swipeButton reset in case the request doesn't return with a valid value.
The swipeButton's state is defined by its swipeDone and swipePercent variables. To achieve what you want you need to pass swipeDone as a parameter when constructing the widget.
class SwipeActionButton extends StatefulWidget {
// Make swipeDone a class variable for the widget
bool swipeDone;
final double height;
final Color doneColor;
final Color swiperColor;
final Color textColor;
final String doneText;
final String disabledText;
final VoidCallback? onDone;
final String? infoText;
final double? width;
final Color? backgroundColor;
final EdgeInsetsGeometry margin;
SwipeActionButton({
// Add it to the constructor
required this.swipeDone,
Key? key,
this.height = 50,
this.doneColor = const Color(0xff44b31f),
this.swiperColor = const Color(0xff44b31f),
this.textColor = const Color(0xff44b31f),
required this.doneText,
required this.disabledText,
this.onDone,
this.infoText,
this.width,
this.backgroundColor,
this.margin = EdgeInsets.zero,
}) : super(key: key);
#override
_SwipeActionButtonState createState() => _SwipeActionButtonState();
}
In _SwipeActionButtonState, delete bool swipeDone = false; and replace every swipeDone by widget.swipeDone.
You also need to reset the value of swipePercent.
You can do this by adding at the beginning of the swipeButton's build method :
if (widget.swipeDone == false && swipePercent > 0.9) swipePercent = 0;
Now you can declare the variable swipeDone in the parent widget state, pass it as a parameter and modify it whenever needed. For more clarity I give you an example with a simple widget that reset the swipe button when the floating action button is pressed.
Complete code :
import 'package:flutter/material.dart';
import 'dart:ui';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
bool swipeDone = false;
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
appBar: AppBar(),
body: SwipeActionButton(
disabledText: 'disabled',
doneText: 'doneText',
onDone: () {},
swipeDone: swipeDone,
),
floatingActionButton: FloatingActionButton(onPressed: () {
setState(() {
swipeDone = false;
});
}),
),
);
}
}
class SwipeActionButton extends StatefulWidget {
// Make swipeDone a class variable for the widget
bool swipeDone;
final double height;
final Color doneColor;
final Color swiperColor;
final Color textColor;
final String doneText;
final String disabledText;
final VoidCallback? onDone;
final String? infoText;
final double? width;
final Color? backgroundColor;
final EdgeInsetsGeometry margin;
SwipeActionButton({
// Add it to the constructor
required this.swipeDone,
Key? key,
this.height = 50,
this.doneColor = const Color(0xff44b31f),
this.swiperColor = const Color(0xff44b31f),
this.textColor = const Color(0xff44b31f),
required this.doneText,
required this.disabledText,
this.onDone,
this.infoText,
this.width,
this.backgroundColor,
this.margin = EdgeInsets.zero,
}) : super(key: key);
#override
_SwipeActionButtonState createState() => _SwipeActionButtonState();
}
class _SwipeActionButtonState extends State<SwipeActionButton> with SingleTickerProviderStateMixin {
double swipePercent = 0.0;
bool isDisabled = false;
late Color backgroundColor;
late AnimationController _controller;
Animation<double>? _animation;
#override
void initState() {
super.initState();
backgroundColor = widget.backgroundColor ?? Color(0xff3344b31f);
_controller = AnimationController(vsync: this, duration: Duration(milliseconds: 500))
..addListener(() {
setState(() {
swipePercent = _animation?.value ?? 0;
});
})
..addStatusListener((AnimationStatus status) {
if (status == AnimationStatus.completed && widget.swipeDone) {
widget.onDone!();
}
});
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
_onDragStart(DragStartDetails details) {
_controller.reset();
swipePercent = 0.0;
}
_onDragUpdate(DragUpdateDetails details) {
setState(() {
swipePercent = details.globalPosition.dx / MediaQuery.of(context).size.width;
if (swipePercent > 0.90) widget.swipeDone = true;
});
}
_onDragEnd(DragEndDetails details) {
if (swipePercent > 0.90 || widget.swipeDone) {
_animation = Tween<double>(begin: swipePercent, end: 1).animate(_controller);
} else {
_animation = Tween<double>(end: 0, begin: swipePercent).animate(_controller);
}
_controller.forward();
}
#override
Widget build(BuildContext context) {
if (widget.swipeDone == false && swipePercent > 0.9) swipePercent = 0;
isDisabled = widget.onDone == null;
double screenWidth = MediaQuery.of(context).size.width;
return Container(
alignment: Alignment.center,
margin: widget.margin,
width: screenWidth - widget.margin.horizontal,
child: Stack(
clipBehavior: Clip.hardEdge,
children: <Widget>[
Container(
height: widget.height,
decoration: BoxDecoration(
color: isDisabled ? Colors.grey : Color(0xff3344b31f),
borderRadius: BorderRadius.all(Radius.circular(100.0)),
border: Border.all(
color: isDisabled ? Colors.grey : Color(0xff3344b31f),
width: 1.5,
),
),
child: Center(
child: Text(widget.infoText ?? "",
style: Theme.of(context).textTheme.subtitle1!.copyWith(color: widget.textColor))),
),
Container(
width: isDisabled ? screenWidth : lerpDouble(widget.height, screenWidth, swipePercent),
height: widget.height,
child: Center(
child: Opacity(
opacity: isDisabled ? 1 : lerpDouble(0, 1, swipePercent)!,
child: Text(
isDisabled ? widget.disabledText : widget.doneText,
style: Theme.of(context).textTheme.subtitle1!.copyWith(
color: Colors.white,
),
textScaleFactor: isDisabled ? 1 : lerpDouble(0, 1, swipePercent),
))),
decoration: BoxDecoration(
color: isDisabled ? Colors.grey : null,
borderRadius: BorderRadius.all(Radius.circular(100.0)),
/* border: Border.all(
color: Theme.of(context).primaryColor,
width: 1.5,
), */
gradient: isDisabled
? null
: LinearGradient(
begin: Alignment.center,
end: Alignment.centerRight,
colors: [widget.doneColor, widget.swipeDone ? widget.doneColor : backgroundColor])),
),
isDisabled
? Container()
: Positioned(
left:
lerpDouble(0, screenWidth - (15 + widget.margin.horizontal) - (widget.height * .9), swipePercent),
/* top: widget.height * .1,
bottom: widget.height * .1,
*/
child: AbsorbPointer(
absorbing: widget.swipeDone,
child: GestureDetector(
onHorizontalDragStart: _onDragStart,
onHorizontalDragUpdate: _onDragUpdate,
onHorizontalDragEnd: _onDragEnd,
child: Opacity(
opacity: 1,
child: AnimatedContainer(
duration: Duration(milliseconds: 500),
height: widget.height,
width: widget.height,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(100.0)),
border: Border.all(
color: Theme.of(context).primaryColor,
width: 1.5,
),
boxShadow:
widget.swipeDone ? null : [BoxShadow(color: Colors.black45, blurRadius: 4)],
color: widget.swipeDone ? backgroundColor : widget.swiperColor),
child: widget.swipeDone
? Icon(
Icons.check,
size: 20,
color: Colors.white,
)
: Icon(
Icons.arrow_forward,
size: 20,
color: Colors.white,
),
))))),
],
),
);
}
}

Flutter: Changing color on button press of class in List

I've seen variations of this question but haven't been able to quite piece together a solution for my use case.
I have a list of CircleButton.
List<CircleButton> buttons = [
new CircleButton(onTap: () => print("Arts"), iconData: Icons.palette, label: "Arts"),
new CircleButton(onTap: () => print("Board Games"), iconData: Icons.casino, label: "Board Games"),
new CircleButton(onTap: () => print("Causes"), iconData: Icons.volunteer_activism, label: "Causes"),
];
I display these in a GridView
Widget interests() {
return Expanded(
child:
SizedBox(
height: 200.0,
child:
GridView.count(
primary: false,
padding: const EdgeInsets.all(20),
crossAxisSpacing: 10,
mainAxisSpacing: 10,
crossAxisCount: 4,
children: <Widget>[
for (var button in buttons) Column( children: [ button, Text(button.label)],)
],
)
)
);
}
When the button is tapped I want to update how it looks. I have set up the CircleButton class to have the boolean value isSelected to determine how the coloring should be:
class CircleButton extends StatelessWidget {
final GestureTapCallback onTap;
final IconData iconData;
final String label;
bool isSelected = false;
CircleButton({Key key, this.onTap, this.iconData, this.label}) : super(key: key);
#override
Widget build(BuildContext context) {
double size = 65.0;
return new
InkResponse(
onTap: onTap,
child: new Container(
width: size,
height: size,
decoration: new BoxDecoration(
color: isSelected ? Color.fromRGBO(139, 207, 236, 1.0) : Color.fromRGBO(248, 248, 250, 1.0),
shape: BoxShape.circle,
),
child: new Icon(
iconData,
color: isSelected ? Colors.white : Color.fromRGBO(2, 78, 157, 1.0),
),
),
);
}
}
How can I update the isSelected variable from onTap?
If you want each button to manage its own selected state (meaning that tapping on a button will not unselect the others), you must use StatefulWidget :
class CircleButton extends StatefulWidget {
final GestureTapCallback onTap;
final IconData iconData;
final String label;
CircleButton({Key key, this.onTap, this.iconData, this.label})
: super(key: key);
#override
_CircleButtonState createState() => _CircleButtonState();
}
class _CircleButtonState extends State<CircleButton> {
bool isSelected = false;
#override
Widget build(BuildContext context) {
double size = 65.0;
return new InkResponse(
onTap: () {
widget.onTap;
// ADD THESE LINES
setState(() {
isSelected = !isSelected;
});
},
child: new Container(
width: size,
height: size,
decoration: new BoxDecoration(
color: isSelected
? Color.fromRGBO(139, 207, 236, 1.0)
: Color.fromRGBO(248, 248, 250, 1.0),
shape: BoxShape.circle,
),
child: new Icon(
widget.iconData,
color: isSelected ? Colors.white : Color.fromRGBO(2, 78, 157, 1.0),
),
),
);
}
}
Otherwise, if you want only one button to be selected at a time, juste create a variable in the parent widget that stores which button is selected :
int selectedButtonIndex;
List<CircleButton> buttons = [
new CircleButton(onTap: () {
print("Arts");
setState((){ selectedButtonIndex = selectedButtonIndex != 0 ? 0 : null; });
}, iconData: Icons.palette, label: "Arts",
isSelected: selectedButtonIndex == 0,
),
new CircleButton(onTap: () {
print("Board Games");
setState((){ selectedButtonIndex = selectedButtonIndex != 1 ? 1 : null; });
}, iconData: casino, label: "Board Games",
isSelected: selectedButtonIndex == 1,
),
...
];
And add isSelected as a parameter for CircleButton

Create custom dropdown in flutter - or how to put custom dropdown options in a layer above everything else

I am looking for a way to create a custom dropdown so I can style it myself.
I ran into this answer that seems pretty useful
https://stackoverflow.com/a/63165793/3808307
The problem is that if the container is smaller than the dropdown, flutter complains about pixel overflowing. How can I get this dropdown to be on top of the other elements in the page, so I don't get this warning? Or is there another way to recreate a custom dropdown without this issue?
All answers I find are regarding the built in DropdownButton
Below, the answer linked above, with editions
First, create a dart file named drop_list_model.dart:
import 'package:flutter/material.dart';
class DropListModel {
DropListModel(this.listOptionItems);
final List<OptionItem> listOptionItems;
}
class OptionItem {
final String id;
final String title;
OptionItem({#required this.id, #required this.title});
}
Next, create file file select_drop_list.dart:
import 'package:flutter/material.dart';
import 'package:time_keeping/model/drop_list_model.dart';
import 'package:time_keeping/widgets/src/core_internal.dart';
class SelectDropList extends StatefulWidget {
final OptionItem itemSelected;
final DropListModel dropListModel;
final Function(OptionItem optionItem) onOptionSelected;
SelectDropList(this.itemSelected, this.dropListModel, this.onOptionSelected);
#override
_SelectDropListState createState() => _SelectDropListState(itemSelected, dropListModel);
}
class _SelectDropListState extends State<SelectDropList> with SingleTickerProviderStateMixin {
OptionItem optionItemSelected;
final DropListModel dropListModel;
AnimationController expandController;
Animation<double> animation;
bool isShow = false;
_SelectDropListState(this.optionItemSelected, this.dropListModel);
#override
void initState() {
super.initState();
expandController = AnimationController(
vsync: this,
duration: Duration(milliseconds: 350)
);
animation = CurvedAnimation(
parent: expandController,
curve: Curves.fastOutSlowIn,
);
_runExpandCheck();
}
void _runExpandCheck() {
if(isShow) {
expandController.forward();
} else {
expandController.reverse();
}
}
#override
void dispose() {
expandController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Container(
child: Column(
children: <Widget>[
Container(
padding: const EdgeInsets.symmetric(
horizontal: 15, vertical: 17),
decoration: new BoxDecoration(
borderRadius: BorderRadius.circular(20.0),
color: Colors.white,
boxShadow: [
BoxShadow(
blurRadius: 10,
color: Colors.black26,
offset: Offset(0, 2))
],
),
child: new Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Icon(Icons.card_travel, color: Color(0xFF307DF1),),
SizedBox(width: 10,),
child: GestureDetector(
onTap: () {
this.isShow = !this.isShow;
_runExpandCheck();
setState(() {
});
},
child: Text(optionItemSelected.title, style: TextStyle(
color: Color(0xFF307DF1),
fontSize: 16),),
),
Align(
alignment: Alignment(1, 0),
child: Icon(
isShow ? Icons.arrow_drop_down : Icons.arrow_right,
color: Color(0xFF307DF1),
size: 15,
),
),
],
),
),
SizeTransition(
axisAlignment: 1.0,
sizeFactor: animation,
child: Container(
margin: const EdgeInsets.only(bottom: 10),
padding: const EdgeInsets.only(bottom: 10),
decoration: new BoxDecoration(
borderRadius: BorderRadius.only(bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20)),
color: Colors.white,
boxShadow: [
BoxShadow(
blurRadius: 4,
color: Colors.black26,
offset: Offset(0, 4))
],
),
child: _buildDropListOptions(dropListModel.listOptionItems, context)
)
),
// Divider(color: Colors.grey.shade300, height: 1,)
],
),
);
}
Column _buildDropListOptions(List<OptionItem> items, BuildContext context) {
return Column(
children: items.map((item) => _buildSubMenu(item, context)).toList(),
);
}
Widget _buildSubMenu(OptionItem item, BuildContext context) {
return Padding(
padding: const EdgeInsets.only(left: 26.0, top: 5, bottom: 5),
child: GestureDetector(
child: Row(
children: <Widget>[
child: Container(
padding: const EdgeInsets.only(top: 20),
decoration: BoxDecoration(
border: Border(top: BorderSide(color: Colors.grey[200], width: 1)),
),
child: Text(item.title,
style: TextStyle(
color: Color(0xFF307DF1),
fontWeight: FontWeight.w400,
fontSize: 14),
maxLines: 3,
textAlign: TextAlign.start,
overflow: TextOverflow.ellipsis),
),
],
),
onTap: () {
this.optionItemSelected = item;
isShow = false;
expandController.reverse();
widget.onOptionSelected(item);
},
),
);
}
}
Initialize value:
DropListModel dropListModel = DropListModel([OptionItem(id: "1", title: "Option 1"), OptionItem(id: "2", title: "Option 2")]);
OptionItem optionItemSelected = OptionItem(id: null, title: "Chọn quyền truy cập");
Finally use it:
Container(height: 47, child: SelectDropList(
this.optionItemSelected,
this.dropListModel,
(optionItem){
optionItemSelected = optionItem;
setState(() {
});
},
))
Custom dropdown below button
I understand that the built-in dropdown works very well but for some use cases, I need something different. For example, if I only have a few items I want the drop-down to appear below the button or have full control of where the dropdown appears. I haven't found a good option yet so I have tried to make my own. I have built on what #M123 mentioned with the overlay and tried to implement it in a similar way to the built-in dropdown. I have found this medium post from the developer of flutter_typeahead very useful.
https://medium.com/saugo360/https-medium-com-saugo360-flutter-using-overlay-to-display-floating-widgets-2e6d0e8decb9
The button creates a full-screen stack using overlay. This is so that we can add a full-screen gesture detector behind the dropdown so that it closes when the user taps anywhere on the screen.
The overlay is linked to the button using a LayerLink and the CompositedTransformFollower widget.
We also use RenderBox renderBox = context.findRenderObject(); to easily get the position and size of the button. Then position the dropdown accoridingly.
the Dropdown file
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
class CustomDropdown<T> extends StatefulWidget {
/// the child widget for the button, this will be ignored if text is supplied
final Widget child;
/// onChange is called when the selected option is changed.;
/// It will pass back the value and the index of the option.
final void Function(T, int) onChange;
/// list of DropdownItems
final List<DropdownItem<T>> items;
final DropdownStyle dropdownStyle;
/// dropdownButtonStyles passes styles to OutlineButton.styleFrom()
final DropdownButtonStyle dropdownButtonStyle;
/// dropdown button icon defaults to caret
final Icon icon;
final bool hideIcon;
/// if true the dropdown icon will as a leading icon, default to false
final bool leadingIcon;
CustomDropdown({
Key key,
this.hideIcon = false,
#required this.child,
#required this.items,
this.dropdownStyle = const DropdownStyle(),
this.dropdownButtonStyle = const DropdownButtonStyle(),
this.icon,
this.leadingIcon = false,
this.onChange,
}) : super(key: key);
#override
_CustomDropdownState<T> createState() => _CustomDropdownState<T>();
}
class _CustomDropdownState<T> extends State<CustomDropdown<T>>
with TickerProviderStateMixin {
final LayerLink _layerLink = LayerLink();
OverlayEntry _overlayEntry;
bool _isOpen = false;
int _currentIndex = -1;
AnimationController _animationController;
Animation<double> _expandAnimation;
Animation<double> _rotateAnimation;
#override
void initState() {
super.initState();
_animationController =
AnimationController(vsync: this, duration: Duration(milliseconds: 200));
_expandAnimation = CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
);
_rotateAnimation = Tween(begin: 0.0, end: 0.5).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
}
#override
Widget build(BuildContext context) {
var style = widget.dropdownButtonStyle;
// link the overlay to the button
return CompositedTransformTarget(
link: this._layerLink,
child: Container(
width: style.width,
height: style.height,
child: OutlinedButton(
style: OutlinedButton.styleFrom(
padding: style.padding,
backgroundColor: style.backgroundColor,
elevation: style.elevation,
primary: style.primaryColor,
shape: style.shape,
),
onPressed: _toggleDropdown,
child: Row(
mainAxisAlignment:
style.mainAxisAlignment ?? MainAxisAlignment.center,
textDirection:
widget.leadingIcon ? TextDirection.rtl : TextDirection.ltr,
mainAxisSize: MainAxisSize.min,
children: [
if (_currentIndex == -1) ...[
widget.child,
] else ...[
widget.items[_currentIndex],
],
if (!widget.hideIcon)
RotationTransition(
turns: _rotateAnimation,
child: widget.icon ?? Icon(FontAwesomeIcons.caretDown),
),
],
),
),
),
);
}
OverlayEntry _createOverlayEntry() {
// find the size and position of the current widget
RenderBox renderBox = context.findRenderObject();
var size = renderBox.size;
var offset = renderBox.localToGlobal(Offset.zero);
var topOffset = offset.dy + size.height + 5;
return OverlayEntry(
// full screen GestureDetector to register when a
// user has clicked away from the dropdown
builder: (context) => GestureDetector(
onTap: () => _toggleDropdown(close: true),
behavior: HitTestBehavior.translucent,
// full screen container to register taps anywhere and close drop down
child: Container(
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
child: Stack(
children: [
Positioned(
left: offset.dx,
top: topOffset,
width: widget.dropdownStyle.width ?? size.width,
child: CompositedTransformFollower(
offset:
widget.dropdownStyle.offset ?? Offset(0, size.height + 5),
link: this._layerLink,
showWhenUnlinked: false,
child: Material(
elevation: widget.dropdownStyle.elevation ?? 0,
borderRadius:
widget.dropdownStyle.borderRadius ?? BorderRadius.zero,
color: widget.dropdownStyle.color,
child: SizeTransition(
axisAlignment: 1,
sizeFactor: _expandAnimation,
child: ConstrainedBox(
constraints: widget.dropdownStyle.constraints ??
BoxConstraints(
maxHeight: MediaQuery.of(context).size.height -
topOffset -
15,
),
child: ListView(
padding:
widget.dropdownStyle.padding ?? EdgeInsets.zero,
shrinkWrap: true,
children: widget.items.asMap().entries.map((item) {
return InkWell(
onTap: () {
setState(() => _currentIndex = item.key);
widget.onChange(item.value.value, item.key);
_toggleDropdown();
},
child: item.value,
);
}).toList(),
),
),
),
),
),
),
],
),
),
),
);
}
void _toggleDropdown({bool close = false}) async {
if (_isOpen || close) {
await _animationController.reverse();
this._overlayEntry.remove();
setState(() {
_isOpen = false;
});
} else {
this._overlayEntry = this._createOverlayEntry();
Overlay.of(context).insert(this._overlayEntry);
setState(() => _isOpen = true);
_animationController.forward();
}
}
}
/// DropdownItem is just a wrapper for each child in the dropdown list.\n
/// It holds the value of the item.
class DropdownItem<T> extends StatelessWidget {
final T value;
final Widget child;
const DropdownItem({Key key, this.value, this.child}) : super(key: key);
#override
Widget build(BuildContext context) {
return child;
}
}
class DropdownButtonStyle {
final MainAxisAlignment mainAxisAlignment;
final ShapeBorder shape;
final double elevation;
final Color backgroundColor;
final EdgeInsets padding;
final BoxConstraints constraints;
final double width;
final double height;
final Color primaryColor;
const DropdownButtonStyle({
this.mainAxisAlignment,
this.backgroundColor,
this.primaryColor,
this.constraints,
this.height,
this.width,
this.elevation,
this.padding,
this.shape,
});
}
class DropdownStyle {
final BorderRadius borderRadius;
final double elevation;
final Color color;
final EdgeInsets padding;
final BoxConstraints constraints;
/// position of the top left of the dropdown relative to the top left of the button
final Offset offset;
///button width must be set for this to take effect
final double width;
const DropdownStyle({
this.constraints,
this.offset,
this.width,
this.elevation,
this.color,
this.padding,
this.borderRadius,
});
}
using the dropdown
I have tried to make using the custom dropdown similar to the built-in one with the added bonus of being able to style the actual dropdown, as well as the button.
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: CustomDropdown<int>(
child: Text(
'dropdown',
),
onChange: (int value, int index) => print(value),
dropdownButtonStyle: DropdownButtonStyle(
width: 170,
height: 40,
elevation: 1,
backgroundColor: Colors.white,
primaryColor: Colors.black87,
),
dropdownStyle: DropdownStyle(
borderRadius: BorderRadius.circular(8),
elevation: 6,
padding: EdgeInsets.all(5),
),
items: [
'item 1',
'item 2',
'item 3',
'item 4',
]
.asMap()
.entries
.map(
(item) => DropdownItem<int>(
value: item.key + 1,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(item.value),
),
),
)
.toList(),
),
),
);
}
I am sure there will be some improvements needed in there somewhere. But it's working for me at the moment.
Dropdown decision
I would recommend using the standard Flutter drop down menu. Because it is very robust, easy to write and has been tried and tested. You said that you would like to style your drop down yourself, I suspect that this is the reason why you decided against the standard. But this doesn't have to be the case. The standard drop down menu can be designed pretty well. More on that below
Example Code
String dropdownValue = 'One';
Widget build(BuildContext context) {
return DropdownButton<String>(
value: dropdownValue,
icon: Icon(Icons.arrow_downward),
iconSize: 24,
elevation: 16,
style: TextStyle(color: Colors.deepPurple),
underline: Container(
height: 2,
color: Colors.deepPurpleAccent,
),
onChanged: (String newValue) {
setState(() {
dropdownValue = newValue;
});
},
items: <String>['One', 'Two', 'Free', 'Four']
.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
);
Style
Your DropdownMenuItem will follow your ThemeData class. Not only its backgroundColor will match the canvasColor in your ThemeData class, but also it will follow the same TextStyle.
The Theme data has to be initialized in the Material App:
return MaterialApp(
//....
theme: new ThemeData(
fontFamily: "Encode Sans", //my custom font
canvasColor: _turquoise, //my custom color
//other theme data
),
//.....
),
If you don't want to or can't work with theme data, this may be something for you.
The DropdownButton class has an inbuilt variable called dropdownColor which can be assigned any color you need directly, without changing any ThemeData. Automatically changes the color of the dropdown menu items as well.
For example, if you want to change the With from the dropdown you can feed its child property a new Container and add the desired width. just make sure you use a suitable width so that you do not get overflow problems later on when you use the menu within a more complex layout. I would still recommend leaving the width on dynamic.
In addition, the DropDownButton has the ability to expand, which means that it takes up all the space it can get
DropdownButton<String>(
isExpanded: true,
)
I found a new way to build a custom drop down, by using Overlay.
Docs:
Overlays let independent child widgets "float" visual elements on top
of other widgets by inserting them into the overlay's Stack. The
overlay lets each of these widgets manage their participation in the
overlay using OverlayEntry objects.
This gives you all the design freedom, as every kind of child is allowed. How to move the DropDown I wrote as comments in the code.
Here is a small sample, how to use it.
OverlayEntry floatingDropdown;
AnyButton(
//...
onTap: () {
setState(() {
if (isDropdownOpened) {
floatingDropdown.remove();
} else {
findDropdownData();
floatingDropdown = _createFloatingDropdown();
Overlay.of(context).insert(floatingDropdown);
}
isDropdownOpened = !isDropdownOpened;
});
},
);
OverlayEntry _createFloatingDropdown() {
return OverlayEntry(builder: (context) {
return Positioned(
// You can change the position here
left: xPosition,
width: width,
top: yPosition + height,
height: 4 * height + 40,
// Any child
child: Container(
color: Colors.black,
height: height,
child: Text('Hallo'),
),
);
});
}
A full fully designed example can be found here.
I have improved the answer provided by Dan James with to match 2023.
fixed few issues
added scrollbar for dropdown
added shape customization for dropdown
~ publishing as a answer because there are many pending edits and not responded.
Dropdown class
import 'package:flutter/material.dart';
class CustomDropdown<T> extends StatefulWidget {
/// the child widget for the button, this will be ignored if text is supplied
final Widget child;
/// onChange is called when the selected option is changed.;
/// It will pass back the value and the index of the option.
final void Function(int) onChange;
/// list of DropdownItems
final List<DropdownItem<T>> items;
final DropdownStyle dropdownStyle;
/// dropdownButtonStyles passes styles to OutlineButton.styleFrom()
final DropdownButtonStyle dropdownButtonStyle;
/// dropdown button icon defaults to caret
final Icon? icon;
final bool hideIcon;
/// if true the dropdown icon will as a leading icon, default to false
final bool leadingIcon;
const CustomDropdown({
Key? key,
this.hideIcon = false,
required this.child,
required this.items,
this.dropdownStyle = const DropdownStyle(),
this.dropdownButtonStyle = const DropdownButtonStyle(),
this.icon,
this.leadingIcon = false,
required this.onChange,
}) : super(key: key);
#override
State<CustomDropdown> createState() => _CustomDropdownState();
}
class _CustomDropdownState<T> extends State<CustomDropdown<T>> with TickerProviderStateMixin {
final LayerLink _layerLink = LayerLink();
final ScrollController _scrollController = ScrollController(initialScrollOffset: 0);
late OverlayEntry _overlayEntry;
bool _isOpen = false;
int _currentIndex = -1;
late AnimationController _animationController;
late Animation<double> _expandAnimation;
late Animation<double> _rotateAnimation;
#override
void initState() {
super.initState();
_animationController = AnimationController(vsync: this, duration: const Duration(milliseconds: 200));
_expandAnimation = CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
);
_rotateAnimation = Tween(begin: 0.0, end: 0.5).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
}
#override
Widget build(BuildContext context) {
var style = widget.dropdownButtonStyle;
// link the overlay to the button
return CompositedTransformTarget(
link: _layerLink,
child: Container(
width: style.width,
height: style.height,
padding: style.padding,
decoration: BoxDecoration(
color: style.backgroundColor,
),
child: InkWell(
onTap: _toggleDropdown,
child: Row(
mainAxisAlignment: style.mainAxisAlignment ?? MainAxisAlignment.center,
textDirection: widget.leadingIcon ? TextDirection.rtl : TextDirection.ltr,
mainAxisSize: MainAxisSize.min,
children: [
// if (_currentIndex == -1) ...[
widget.child,
// ]
// else ...[
// widget.items[_currentIndex],
// ],
if (!widget.hideIcon)
RotationTransition(
turns: _rotateAnimation,
child: widget.icon ??
const Padding(
padding: EdgeInsets.only(left: 5, right: 7),
child: RotatedBox(
quarterTurns: 3,
child: Icon(
Icons.arrow_back_ios_rounded,
size: 13,
color: Colors.grey,
),
),
),
),
],
),
),
),
);
}
OverlayEntry _createOverlayEntry() {
// find the size and position of the current widget
RenderBox renderBox = context.findRenderObject()! as RenderBox;
var size = renderBox.size;
var offset = renderBox.localToGlobal(Offset.zero);
var topOffset = offset.dy + size.height + 5;
return OverlayEntry(
// full screen GestureDetector to register when a
// user has clicked away from the dropdown
builder: (context) => GestureDetector(
onTap: () => _toggleDropdown(close: true),
behavior: HitTestBehavior.translucent,
// full screen container to register taps anywhere and close drop down
child: SizedBox(
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
child: Stack(
children: [
Positioned(
left: offset.dx,
top: topOffset,
width: widget.dropdownStyle.width ?? size.width,
child: CompositedTransformFollower(
offset: widget.dropdownStyle.offset ?? Offset(0, size.height + 5),
link: _layerLink,
showWhenUnlinked: false,
child: Material(
elevation: widget.dropdownStyle.elevation ?? 0,
color: widget.dropdownStyle.color,
shape: widget.dropdownStyle.shape,
child: SizeTransition(
axisAlignment: 1,
sizeFactor: _expandAnimation,
child: ConstrainedBox(
constraints: widget.dropdownStyle.constraints ??
BoxConstraints(
maxHeight: (MediaQuery.of(context).size.height - topOffset - 15).isNegative
? 100
: MediaQuery.of(context).size.height - topOffset - 15,
),
child: RawScrollbar(
thumbVisibility: true,
thumbColor: widget.dropdownStyle.scrollbarColor ?? Colors.grey,
controller: _scrollController,
child: ListView(
padding: widget.dropdownStyle.padding ?? EdgeInsets.zero,
shrinkWrap: true,
controller: _scrollController,
children: widget.items.asMap().entries.map((item) {
return InkWell(
onTap: () {
setState(() => _currentIndex = item.key);
widget.onChange(item.key);
_toggleDropdown();
},
child: item.value,
);
}).toList(),
),
),
),
),
),
),
),
],
),
),
),
);
}
void _toggleDropdown({bool close = false}) async {
if (_isOpen || close) {
await _animationController.reverse();
_overlayEntry.remove();
setState(() {
_isOpen = false;
});
} else {
_overlayEntry = _createOverlayEntry();
Overlay.of(context)?.insert(_overlayEntry);
setState(() => _isOpen = true);
_animationController.forward();
}
}
}
/// DropdownItem is just a wrapper for each child in the dropdown list.\n
/// It holds the value of the item.
class DropdownItem<T> extends StatelessWidget {
final T? value;
final Widget child;
const DropdownItem({Key? key, this.value, required this.child}) : super(key: key);
#override
Widget build(BuildContext context) {
return child;
}
}
class DropdownButtonStyle {
final MainAxisAlignment? mainAxisAlignment;
final ShapeBorder? shape;
final double elevation;
final Color? backgroundColor;
final EdgeInsets? padding;
final BoxConstraints? constraints;
final double? width;
final double? height;
final Color? primaryColor;
const DropdownButtonStyle({
this.mainAxisAlignment,
this.backgroundColor,
this.primaryColor,
this.constraints,
this.height,
this.width,
this.elevation = 0,
this.padding,
this.shape,
});
}
class DropdownStyle {
final double? elevation;
final Color? color;
final EdgeInsets? padding;
final BoxConstraints? constraints;
final Color? scrollbarColor;
/// Add shape and border radius of the dropdown from here
final ShapeBorder? shape;
/// position of the top left of the dropdown relative to the top left of the button
final Offset? offset;
///button width must be set for this to take effect
final double? width;
const DropdownStyle({
this.constraints,
this.offset,
this.width,
this.elevation,
this.shape,
this.color,
this.padding,
this.scrollbarColor,
});
}
Usage
CustomDropdown<int>(
onChange: (int index) => print("index: $index"),
dropdownButtonStyle: DropdownButtonStyle(
height: 49,
elevation: 1,
backgroundColor: Colors.white,
primaryColor: Colors.black87,
),
dropdownStyle: DropdownStyle(
elevation: 1,
padding: EdgeInsets.all(5),
shape: RoundedRectangleBorder(
side: BorderSide(
color: Colors.grey,
width: 1,
),
borderRadius: BorderRadius.circular(7))),
items: [
'item 1',
'item 2',
'item 3',
'item 4',
]
.asMap()
.entries
.map(
(item) => DropdownItem<int>(
value: item.key + 1,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(item.value),
),
),
)
.toList(),
child: Text(
"Item 1",
),
)