The project has 4 registration screens with a dynamic header that displays the current page by the means of color. Below is the state when we move to the first page
Once we complete pages and move on, the indicator for the completed steps changes color and state should be as below
I am able to achieve the icon set and color difference, but I am not able to implement the dashed line between the items. Here are the WIP widgets. Please ignore the icon mismatch
Here's the code for Generating the header
Padding(
padding: const EdgeInsets.only(bottom: 18.0, top: 12),
child: Container(
color: Colors.white,
padding: const EdgeInsets.all(14),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
RegisterStepWidget(
title: "Basic Details",
icon: Drawables.icBasicDetails,
currentStep: currentStep,
stepIndex: 1),
RegisterStepWidget(
title: "Contact Details",
icon: Drawables.icContactDetails,
currentStep: currentStep,
stepIndex: 2),
RegisterStepWidget(
title: "Extra Details",
icon: Drawables.icExtraDetails,
currentStep: currentStep,
stepIndex: 3),
RegisterStepWidget(
title: "Garda Vetting",
icon: Drawables.icBasicDetails,
currentStep: currentStep,
stepIndex: 4),
],
),
),
),
Individual Widget
class RegisterStepWidget extends StatelessWidget {
const RegisterStepWidget(
{Key? key,
required this.title,
required this.icon,
required this.currentStep,
required this.stepIndex})
: super(key: key);
final String title;
final String icon;
final int currentStep;
final int stepIndex;
bool get isCompleted => currentStep > stepIndex;
#override
Widget build(BuildContext context) {
return Column(
children: [
SvgPicture.asset(
icon,
width: 28,
color: isCompleted ? ColorResource.blueGray : ColorResource.darkBlue,
),
const SizedBox(
height: 12,
),
Text(
title,
style: GoogleFonts.notoSans(
fontSize: 12,
color: isCompleted
? ColorResource.blueGray
: ColorResource.darkBlue),
),
],
);
}
}
Change MainAxisAlignment to start and use in middle of widgets
DashLineView(
fillRate: 0.7,
),
.
class DashLineView extends StatelessWidget {
final double dashHeight;
final double dashWith;
final Color dashColor;
final double fillRate; // [0, 1] totalDashSpace/totalSpace
final Axis direction;
DashLineView(
{this.dashHeight = 1,
this.dashWith = 8,
this.dashColor = Colors.black,
this.fillRate = 0.5,
this.direction = Axis.horizontal});
#override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
final boxSize = direction == Axis.horizontal
? constraints.constrainWidth()
: constraints.constrainHeight();
final dCount = (boxSize * fillRate / dashWith).floor();
return Flex(
children: List.generate(dCount, (_) {
return SizedBox(
width: direction == Axis.horizontal ? dashWith : dashHeight,
height: direction == Axis.horizontal ? dashHeight : dashWith,
child: DecoratedBox(
decoration: BoxDecoration(color: dashColor),
),
);
}),
mainAxisAlignment: MainAxisAlignment.spaceBetween,
direction: direction,
);
},
);
}
}
Modify accordingly
original credit -> here
I have made radius based flutter google maps in which I am using circle property to draw a circle radius based. It only shows up when I increment or decrement the radius but I want it to be showing by default as well when user navigates to location screen in my flutter app. This is how the map screen loads and as soon as i tap the + or - buttons it will start showing the circle like this I don't know why this is happening. If anyone knows what am I doing wrong then please let me know. I am attaching the code of the screen below :
class MapRadius extends StatefulWidget {
const MapRadius({Key? key}) : super(key: key);
#override
State<MapRadius> createState() => _MapRadiusState();
}
class _MapRadiusState extends State<MapRadius> {
// List<Marker> myMarker = [];
final Set<Circle> circle = {};
// late GoogleMapController mapController;
final Completer<GoogleMapController> _controller = Completer();
int _n = 5;
// LatLng startLoc = const LatLng(52.0907374, 5.1214201);
LatLng? currentLatLng;
late GoogleMapController mapController;
Location location = Location();
var latitude;
var longitude;
late LocationData _locationData;
get() async {
_locationData = await location.getLocation();
latitude = _locationData.latitude;
longitude = _locationData.longitude;
setState(() {
currentLatLng = LatLng(latitude, longitude);
});
}
#override
initState() {
super.initState();
get();
}
#override
void setState(VoidCallback fn) {
super.setState(fn);
FirebaseFirestore.instance
.collection("userpreferences")
.doc(FirebaseAuth.instance.currentUser!.uid)
.update({"radius": _n});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
backgroundColor: const Color(0xFF2A3B6A),
title: const DelayedDisplay(
delay: Duration(seconds: 1), child: Text('Location Range')),
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios),
onPressed: () {
Navigator.of(context).pop();
},
),
actions: const [
Padding(
padding: EdgeInsets.only(right: 20),
child: Tooltip(
showDuration: Duration(seconds: 5),
triggerMode: TooltipTriggerMode.tap,
textStyle: TextStyle(
fontSize: 18, color: Colors.white, fontFamily: "ProductSans"),
message:
'Increase or decrease radius according to your own preference. + and - can be used to add Kilometers.\nThe Kilometers are multiples of 10 (e.g. 5 = 50KM)\nMax limit 100 KM nearby. Default is 50KM.\nRange gets updated as soon as you add or remove a kilometer.',
child: Icon(Icons.info),
),
),
],
),
body: currentLatLng == null
? Center(
child: SpinKitSpinningLines(
color: Theme.of(context).primaryColor,
size: 90.0,
lineWidth: 5,
))
: Stack(children: <Widget>[
GoogleMap(
circles: circle,
myLocationEnabled: true,
onMapCreated: (GoogleMapController controller) {
_controller.complete(controller);
},
initialCameraPosition:
CameraPosition(target: currentLatLng!, zoom: 12),
),
]),
floatingActionButton: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(left: 40.0),
child: Row(
children: <Widget>[
SizedBox(
width: 40.0,
height: 40.0,
child: FloatingActionButton(
heroTag: "btnAdd",
onPressed: add,
child: LineIcon(
LineIcons.plus,
color: Colors.black,
size: 30,
),
backgroundColor: Colors.white,
),
),
const SizedBox(
width: 5,
),
Text('$_n', style: const TextStyle(fontSize: 26.0)),
const SizedBox(
width: 5,
),
SizedBox(
width: 40.0,
height: 40.0,
child: FloatingActionButton(
heroTag: "btnMinus",
onPressed: minus,
child: LineIcon(
LineIcons.minus,
color: Colors.black,
size: 30,
),
backgroundColor: Colors.white,
),
),
],
),
),
],
),
);
}
void add() {
setState(() {
if (_n < 10) {
_n++;
}
addRadiusToMap(_n);
});
}
void minus() {
if (_n != 1) {
setState(() {
_n--;
addRadiusToMap(_n);
});
}
}
void addRadiusToMap(radius) {
double reciprocal(double d) => 700 * d; // 1000 before
circle.clear();
circle.add(Circle(
circleId: const CircleId("1"),
center: currentLatLng!,
radius: reciprocal(radius.toDouble()),
));
}
}
I created a stepper (4 steps) with two buttons for next and previous. Each step has a form, and each form is in a widget in its own class.
The first problem is that every time I click the previous button, the data in the text fields disappear.
How can I preserve the state of each widget in each step?
The second problem, I want the last step to be a summary of what the user has entered in the previous steps. What is the best way to get the data from each step and display them in the last step?
I would really appreciate it if you could give me a solution. Thank you
I tried using AutomaticKeepAliveClientMixin but it didn't work .
import 'package:flutter/material.dart';
class CustomeStepper extends StatelessWidget {
final double width;
final List<IconData> icons;
final List<String> titles;
final int curStep;
final Color circleActiveColor;
final Color circleInactiveColor;
final Color iconActiveColor;
final Color iconInactiveColor;
final Color textActiveColor;
final Color textInactiveColor;
final double lineWidth = 4.0;
final List<Widget> content;
CustomeStepper(
{required this.icons,
required this.curStep,
required this.titles,
required this.width,
required this.circleActiveColor,
required this.circleInactiveColor,
required this.iconActiveColor,
required this.iconInactiveColor,
required this.textActiveColor,
required this.textInactiveColor,
required this.content})
: assert(curStep > 0 && curStep <= icons.length),
assert(width > 0);
#override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.rtl,
child: Container(
width: width,
padding: const EdgeInsets.only(
top: 32.0,
left: 24.0,
right: 24.0,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Row(
children: _iconViews(),
),
const SizedBox(
height: 10,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _titleViews(),
),
Expanded(
child: Container(
margin: const EdgeInsets.only(top: 16),
child: content[curStep - 1]),
)
],
)),
);
}
List<Widget> _iconViews() {
var list = <Widget>[];
icons.asMap().forEach((i, icon) {
var circleColor = (i == 0 || curStep >= i + 1)
? circleActiveColor
: circleInactiveColor;
var lineColor = (i == 0 || curStep >= i + 1)
? circleActiveColor
: circleInactiveColor;
var iconColor =
(i == 0 || curStep >= i + 1) ? iconActiveColor : iconInactiveColor;
list.add(
Container(
width: 50.0,
height: 50.0,
padding: const EdgeInsets.all(0),
child: Icon(
icon,
color: iconColor,
size: 25.0,
),
decoration: BoxDecoration(
color: circleColor,
borderRadius: const BorderRadius.all(
Radius.circular(25.0),
),
),
),
);
//line between icons
if (i != icons.length - 1) {
list.add(Expanded(
child: Container(
height: lineWidth,
color: lineColor,
)));
}
});
return list;
}
List<Widget> _titleViews() {
var list = <Widget>[];
titles.asMap().forEach((i, text) {
var _textColor =
(i == 0 || curStep > i + 1) ? textActiveColor : textInactiveColor;
list.add(
Container(
width: 50.0,
alignment: Alignment.topCenter,
padding: const EdgeInsets.all(0),
child: Text(
text,
textAlign: TextAlign.center,
style: TextStyle(color: _textColor, fontWeight: FontWeight.bold),
),
),
);
});
return list;
}
}
import 'package:flutter/material.dart';
import 'package:project_five/widgets/business/adding_product_widgets/first_step.dart';
import 'package:project_five/widgets/business/adding_product_widgets/four_step.dart';
import 'package:project_five/widgets/business/adding_product_widgets/second_step.dart';
import 'package:project_five/widgets/business/adding_product_widgets/third_step.dart';
import 'package:project_five/widgets/business/custome_stepper.dart';
class AddProduct extends StatefulWidget {
const AddProduct({Key? key}) : super(key: key);
#override
State<AddProduct> createState() => _AddProductState();
}
class _AddProductState extends State<AddProduct> {
static const _stepIcons = [
Icons.add_circle,
Icons.document_scanner,
Icons.camera_alt_rounded,
Icons.check,
];
static const _titles = ['المنتج', 'تفاصيل', 'الصور', 'نشر'];
var _contnet = [
FirstStep(),
SecondStep(),
ThirdStep(),
Forth()];
var _curStep = 1;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('إضافة منتج'),
centerTitle: true,
),
persistentFooterButtons: [
Row(
children: [
Expanded(
child: ElevatedButton(
child: const Text('التالي'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.all(16),
),
onPressed: () => setState(() {
if (_curStep < _stepIcons.length) _curStep++;
}),
),
),
const SizedBox(
width: 8,
),
Expanded(
child: ElevatedButton(
child: const Text('رجوع'),
style: ElevatedButton.styleFrom(
primary: Colors.white,
onPrimary: Colors.black,
padding: const EdgeInsets.all(16)),
onPressed: () => setState(() {
if (_curStep > 1) _curStep--;
}),
),
),
],
)
],
body: CustomeStepper(
icons: _stepIcons,
width: MediaQuery.of(context).size.width,
curStep: _curStep,
titles: _titles,
circleActiveColor: Colors.green,
circleInactiveColor: const Color(0xffD5D5D5),
iconActiveColor: Colors.white,
iconInactiveColor: Colors.white,
textActiveColor: Colors.green,
textInactiveColor: const Color(0xffD5D5D5),
content: _contnet,
),
);
}
}
I had the same problem, It would help to see your forms widgets. I will try my best to describe what you need to do.
Your textfields in your forms should be tied to your model class. Example: onChange: Product.title = TextField.value. and you should use initial value with your model properties, example: initialValue: Product.title. I think this way you can retain the state of the inputs in your forms.
As for the second part of your question, the Main widget that is controlling the stepper should have a state variable, such as isCompleted, on the last step you set this variable to 'true' and the main body of the stepper should be in a stack, in your stack you check if "isCompleted" ? Stepper : SummaryWidget.
How are handling Arabic titles for text fields and matching them with your class model properties?
I hope my answer can help!
i have a GridView Widget that contain some GridTiles that are wrapped with GestureDetector trying to showUp a menu to delete the GridTile when i have longPress on it ,,, everything is fine except that i want that menu to be shown from the point that i have clicked into not at the top of the app
showMenu(
context: context,
position: ..........,// Here i want the solution
items: [
PopupMenuItem(
child: FlatButton.icon(
onPressed: () {
_notesProv.deleteNote(id);
Navigator.of(context).pop();
},
icon: Icon(
Icons.delete,
color: Colors.black,
),
label: Text(
'delete note',
style: TextStyle(color: Colors.black),
),
),
),
],
color: Colors.green[100],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
));
See comments in the code for guidance on what's going on.
Wherever there's a comment, that code was added to the example Gridview stolen from Flutter official cookbook in order to pop up the menu where you tapped.
class GridPositionPage extends StatefulWidget {
#override
_GridPositionPageState createState() => _GridPositionPageState();
}
class _GridPositionPageState extends State<GridPositionPage> {
// ↓ hold tap position, set during onTapDown, using getPosition() method
Offset tapXY;
// ↓ hold screen size, using first line in build() method
RenderBox overlay;
#override
Widget build(BuildContext context) {
// ↓ save screen size
overlay = Overlay.of(context).context.findRenderObject();
return BaseExamplePage(
title: 'Grid Position',
child: GridView.count(
crossAxisCount: 2,
children: List.generate(100, (index) {
return Center(
child: InkWell(
child: Text(
'Item $index',
style: Theme.of(context).textTheme.headline5,
),
// ↓ save screen tap position now
onTapDown: getPosition,
onLongPress: () => showMenu(
context: context,
position: relRectSize,
items: [
PopupMenuItem(
child: FlatButton.icon(
label: Text('Delete'),
icon: Icon(Icons.delete),
),
)
]
),
),
);
}),
),
);
}
// ↓ create the RelativeRect from size of screen and where you tapped
RelativeRect get relRectSize => RelativeRect.fromSize(tapXY & const Size(40,40), overlay.size);
// ↓ get the tap position Offset
void getPosition(TapDownDetails detail) {
tapXY = detail.globalPosition;
}
}
This function handles everything, you just need to use the findRenderObject method to get the RenderObject on each grid item:
void showGridMenu() async {
Rect? rect;
RenderBox? overlay = Overlay.of(context).context.findRenderObject() as RenderBox;
final renderObject = context.findRenderObject();
final translation = renderObject?.getTransformTo(null).getTranslation();
if (translation != null && renderObject?.paintBounds != null) {
final offset = Offset(translation.x, translation.y);
rect = renderObject!.paintBounds.shift(offset);
}
if (rect != null) {
var value = await showMenu<String>(
context: context,
position: RelativeRect.fromRect(
rect,
Offset.zero & overlay.size,
),
items: [
const PopupMenuItem(value: 'yes', child: Text('Yes')),
const PopupMenuItem(value: 'no', child: Text('No')),
],
);
if (value != null) {
debugPrint(value);
}
}
}
position: RelativeRect.fromSize(
Rect.fromCenter(
center: Offset.zero, width: 100, height: 100),
Size(100, 100),
),
Somthing like that
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",
),
)