ElevatedButton, TextButton and OutlinedButton gradient - flutter

Before it was easy to make a gradient button with the default ButtonThemeData... But now I can't figure out how to properly make a custom gradient button using the new MaterialButtons.
I am trying to make a three custom gradient button which must use the ButtonStyle defined in the AppTheme (splashColor, elevation, ...).
ElevatedButton
GradientElevatedButton that uses the ElevatedButtonThemeData with a gradient background
TextButton
GradientTextButton that uses the TextButtonThemeData with a gradient text
OutlinedButton
GradientOutlinedButton that uses the OutlinedButtonThemeData with a gradient border and a gradient text
Already tried
I have tried to wrap an ElevatedButton with a ShaderMask but it covers the ink animations so it doesn't accomplish my goals.

ElevatedButton
DecoratedBox(
decoration: BoxDecoration(gradient: LinearGradient(colors: [Colors.red, Colors.blue])),
child: ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(primary: Colors.transparent),
child: Text('Elevated Button'),
),
)
OutlinedButton
For OutlinedButton, you need to do some extra steps. Create a class (null-safe):
class MyOutlinedButton extends StatelessWidget {
final VoidCallback onPressed;
final Widget child;
final ButtonStyle? style;
final Gradient? gradient;
final double thickness;
const MyOutlinedButton({
Key? key,
required this.onPressed,
required this.child,
this.style,
this.gradient,
this.thickness = 2,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: BoxDecoration(gradient: gradient),
child: Container(
color: Colors.white,
margin: EdgeInsets.all(thickness),
child: OutlinedButton(
onPressed: onPressed,
style: style,
child: child,
),
),
);
}
}
Usage:
MyOutlinedButton(
onPressed: () {},
gradient: LinearGradient(colors: [Colors.indigo, Colors.pink]),
child: Text('OutlinedButton'),
)

I don't know if it helps, but I just did it by myself this way. Still don't get why they have to make things more complicated.
import 'package:flutter/material.dart';
import 'package:my_app/src/utils/style_helper.dart';
import 'package:my_app/src/utils/themes.dart';
class SecondaryButtonState extends State<SecondaryButton> {
setPressed(bool value) {
setState(() {
widget.pressed = value;
});
}
getDecoration() {
if (widget.disabled) {
return BoxDecoration(
borderRadius: BorderRadius.circular(SH.BORDER_RADIUS),
color: ThemeColors.secondaryLight,
);
}
if (!widget.pressed) {
return BoxDecoration(
borderRadius: BorderRadius.circular(16),
gradient: LinearGradient(
begin: FractionalOffset.topCenter,
end: FractionalOffset.bottomCenter,
colors: [ThemeColors.gradient, ThemeColors.secondary]),
);
} else {
return BoxDecoration(
borderRadius: BorderRadius.circular(SH.BORDER_RADIUS),
color: ThemeColors.secondary);
}
}
void onTapDown(TapDownDetails details) {
if (!widget.disabled) {
setPressed(true);
}
}
void onTapUp(TapUpDetails details) {
if (!widget.disabled) {
setPressed(false);
widget.onPressed();
}
}
TextStyle getTextStyle(context, Set<MaterialState> states) {
return Theme.of(context)
.textButtonTheme
.style
.textStyle
.resolve(states)
.copyWith();
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: onTapDown,
onTapUp: onTapUp,
child: Container(
width: 400,
decoration: getDecoration(),
padding: EdgeInsets.symmetric(vertical: SH.PC, horizontal: SH.P3),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
widget.icon != null ? widget.icon : Container(),
Text(
widget.label,
style: Theme.of(context)
.elevatedButtonTheme
.style
.textStyle
.resolve(null)
.copyWith(),
),
])),
);
}
}
class SecondaryButton extends StatefulWidget {
SecondaryButton(
{#required this.onPressed,
#required this.label,
this.icon,
this.disabled = false});
final GestureTapCallback onPressed;
final String label;
bool disabled;
final Icon icon;
bool pressed = false;
#override
State<StatefulWidget> createState() {
return SecondaryButtonState();
}
}

Related

Is it possible to trigger a CupertinoContextMenu from a single click on an icon in flutter?

I have the CupertinoContextMenu setup and it works well. However, I have an Icon in a separate widget which when tapped (single tap) would also trigger the opening of the Context Menu.
Is anything like this possible?
I hacked something a little remotly iosy together from the material PopupMenuButton. Maybe it can help you as a basis for styling.
import 'package:flutter/material.dart';
class IosLikePopupMenuButton extends StatelessWidget {
final double borderRadius;
final Color _backgroundColor = const Color.fromARGB(209, 235, 235, 235);
final List<IosLikePopupMenuItem> Function(BuildContext) itemBuilder;
const IosLikePopupMenuButton({
Key? key,
this.borderRadius = 24.0,
required this.itemBuilder,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return PopupMenuButton<String>(
icon: const Icon(Icons.more_horiz),
itemBuilder: (context1) {
/// give the current context and map the IosLikePopupMenuItem dtos to PopupMenuEntry while splicing in dividers
return itemBuilder(context1)
.map((e) => e.popupMenuItems())
.toList()
.fold(
List<PopupMenuEntry<String>>.empty(growable: true),
(p, e) => [
...p,
...[e, const PopupMenuDivider()]
])
/// delete the last divider
..removeLast();
},
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(borderRadius),
),
),
color: _backgroundColor,
);
}
}
class IosLikePopupMenuItem {
final double menuItemHeight, minSpaceBtwnTxtAndIcon;
final String lableText;
final IconData icon;
final void Function() onTap;
final TextStyle _textStyle =
const TextStyle(color: Colors.black, fontSize: 22.0);
IosLikePopupMenuItem({
this.menuItemHeight = 24.0,
this.minSpaceBtwnTxtAndIcon = 48.0,
required this.lableText,
required this.icon,
required this.onTap,
});
PopupMenuItem<String> popupMenuItems() => PopupMenuItem<String>(
height: menuItemHeight,
textStyle: _textStyle,
onTap: onTap,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
lableText,
),
SizedBox(width: minSpaceBtwnTxtAndIcon),
Icon(icon),
],
),
);
}

flutter circle checkbox with text inside

How can I create round checkboxes with text inside ?
I want it like on the picture.
You can make a button that on pressed toggles a bool and based on if bool is true or false you can make the border be transparent or not.
This may not be the best solution but it should work
class CustomCheckbox extends StatefulWidget {
#override
State<CustomCheckbox> createState() => _CustomCheckboxState();
}
class _CustomCheckboxState extends State<CustomCheckbox> {
bool isChecked = false;
#override
Widget build(BuildContext context) {
return RawMaterialButton(
onPressed: () {
setState(() => isChecked = !isChecked);
},
splashColor: Colors.transparent,
child: Text(
'AS',
style: TextStyle(
color: isChecked ? Colors.white : Colors.grey,
fontSize: 20
)
),
padding: EdgeInsets.all(13.0),
shape: CircleBorder(
side: BorderSide(
color: isChecked ? Colors.yellowAccent : Colors.transparent
)
),
);
}
}
try using ClipOval in a row children
ClipOval(
child:
Container(
color: yourColor
height: 10.0,
width: 10.0,
))
class Checkbox extends StatefulWidget {
final String value;
final bool check;
const Checkbox({
Key? key, required this.value, required this.check,
}) : super(key: key);
#override
State<Checkbox> createState() => _CheckboxState();
}
class _CheckboxState extends State<Checkbox> {
late bool check;
#override
void initState() {
check = widget.check;
super.initState();
}
#override
Widget build(BuildContext context) {
return InkWell(
onTap: (){
setState(()=>check = !check);
},
child: Container(
padding: EdgeInsets.all(6),
alignment: Alignment.center,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: check ? Colors.yellow : Colors.transparent,
width: 2),
),
child: Text(widget.value, style: TextStyle(color: check ? Colors.white
: Colors.grey)),
),
);
}
}

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",
),
)

How to update widget state built inside FutureBuilder

Before, I used ListBuilder to generate a list of 70 numbers and it worked but it took a long time to generate the 70 numbers into custom widgets and also when I tapped a number just to change the background color state, it took a couple of milliseconds before the state being changed.
Now I am using a FutureBuilder to be able to load the screen while waiting for the generated 70 integers. But when I tap on the ball number, the background color is not updated ... It's like setState() is not working in Future ListBuilder.
This question: "Flutter - How to update state (or value?) of a Future/List used to build ListView (via FutureBuilder)" is very similar, but it did not solve my problem.
Here is the code I have in the build method
Flexible(
child:FutureBuilder<List<Widget>>(
future: ballNumbers,
builder: (context, snapshot){
if(snapshot.connectionState != ConnectionState.done){
return Center(child: CircularProgressIndicator());
}
if(snapshot.hasError){
return Center(child: Text("An error has occured"));
}
List<Widget> balls = snapshot.data ?? [];
return GridView.count(
crossAxisCount: 9,
children: balls,
);
}
)
Here is how I start the state for the function:
Future<List<Widget>> ballNumbers;
List<int> picks = [];
#override
void initState() {
ballNumbers = getBallNumbers();
});
Future<List<Widget>> getBallNumbers() async {
return List.generate(limitBallNumber,(number){
number = number + 1;
return Padding(
padding:EdgeInsets.all(2.5),
child:Ball(
number : number,
size: ballWidth,
textColor:(picks.contains(number)) ? Colors.black : Colors.white,
ballColor: (picks.contains(number)) ? Style.selectedBallColor : Style.ballColor,
onTap:(){
setState((){
picks.contains(number) ? picks.remove(number) : picks.add(number);
});
}
)
);
});
}
UPDATED: Here is the class the Ball widget
class Ball extends StatelessWidget {
final Color ballColor;
final Color textColor;
final double size;
final double fontSize;
final int number;
final VoidCallback onTap;
Ball({Key key, #required this.number,
this.textColor,
this.ballColor,
this.onTap,
this.size = 55.0,
this.fontSize = 14,
}) : super(key : key);
#override
Widget build(BuildContext context) {
return Container(
height: size,
width: size,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: [
Style.secondaryColor,
ballColor != null ? ballColor : Style.ballColor,
],
begin: Alignment.bottomLeft,
end: Alignment.topRight
)
),
child: FlatButton(
padding: EdgeInsets.all(0),
child: Container(
child: Text(
number.toString().length > 1 ? number.toString() : "0" + number.toString(),
style: TextStyle(
fontSize: fontSize,
color: textColor != null ? textColor : Colors.white
),
),
padding: const EdgeInsets.all(4.0),
decoration:BoxDecoration(
color: Colors.transparent,
border: Border.all(color: textColor != null ? textColor : Colors.white, width: 1),
borderRadius: BorderRadius.circular(32),
)
),
color: Colors.transparent,
onPressed: onTap,
),
);
}
}
The issue is that getBallNumbers is only being called once in initState, so when picks is updated, it doesn't matter because getBallNumbers isn't called again to update the colors being passed to the Ball widgets.
A simple fix would be to call getBallNumbers in your build with future: getBallNumbers(), but this would lead to the CircularProgressIndicator being shown on every click as the List regenerates.
However, ideally, you should be handling all of the color changing within the state of each Ball so that you're not forced to rebuild that List on every click. And to maintain a List of selected numbers within the parent widget's State, you should pass a callback to each ball that adds and removes their number from the List in the parent.
Rough example:
Ball class(modified to be stateful and removed parameters that became unecessary; active state is now stored within the ball instead of solely in the parent):
class Ball extends StatefulWidget {
final double size;
final double fontSize;
final int number;
final VoidCallback toggleBall;
final bool initialActiveState;
Ball({Key key, #required this.number,
this.toggleBall,
this.size = 55.0,
this.fontSize = 14,
this.initialActiveState,
}) : super(key : key);
_BallState createState() => _BallState();
}
class _BallState extends State<Ball> {
bool isActive;
#override
void initState() {
super.initState();
isActive = widget.initialActiveState;
}
#override
Widget build(BuildContext context) {
return Container(
height: widget.size,
width: widget.size,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: [
Style.secondaryColor,
isActive ? Style.selectedBallColor : Style.ballColor,
],
begin: Alignment.bottomLeft,
end: Alignment.topRight
)
),
child: FlatButton(
padding: EdgeInsets.all(0),
child: Container(
child: Text(
widget.number.toString().length > 1 ? widget.number.toString() : "0" + widget.number.toString(),
style: TextStyle(
fontSize: widget.fontSize,
color: isActive ? Colors.black : Colors.white,
),
),
padding: const EdgeInsets.all(4.0),
decoration:BoxDecoration(
color: Colors.transparent,
border: Border.all(color: isActive ? Colors.black : Colors.white, width: 1),
borderRadius: BorderRadius.circular(32),
)
),
color: Colors.transparent,
onPressed: () {
if(!isActive && widget.activeBallList.length >= 7) {
return;
}
setState(() {
isActive = !isActive;
});
widget.activeBallList.contains(widget.number) ? widget.activeBallList.remove(widget.number) : widget.activeBallList.add(widget.number);
},
),
);
}
}
Parent class(the only part that needs to be modified is the parameters for Ball):
Future<List<Widget>> getBallNumbers() async {
return List.generate(limitBallNumber,(number){
number = number + 1;
return Padding(
padding:EdgeInsets.all(2.5),
child: Ball(
number: number,
size: ballWidth,
initialActiveState: picks.contains(number),
activeBallList: picks,
)
);
});
}

How to position iconbuttons closer?

I would like to make a trailing button bar at the end of my title. How can I position the buttons closer to each other?
https://szirom.hu/Fisha/so.jpg
I assume they are completily next to each other but the inner padding is too big imo.
ButtonTheme (
padding: EdgeInsets.all(0),
child: ButtonBar (
alignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
IconButton(
icon: Icon(Icons.delete),
onPressed:() {
_deleteDialog(context, campaigns, i);
}),
IconButton(
icon: Icon(Icons.edit,),
onPressed: () async {
....
}
),
],
),
)
Can I solve this with IconButtons or should I create buttons from scratch?
IconButton have is wrapped in an ConstrainedBox with minHeight&minWidth set to 48 ,so you'll have a modify the constrains.
something like this ...
CustomIconButton(
icon: Icon(Icons.delete),
onPressed: () {},
padding: EdgeInsets.zero,
constraints: BoxConstraints(
minHeight: 20.0,
minWidth: 30.0,
),
),
The code for CustomIconButton...
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
class CustomIconButton extends StatelessWidget {
const CustomIconButton(
{Key key,
this.iconSize = 24.0,
this.padding = const EdgeInsets.all(8.0),
this.alignment = Alignment.center,
#required this.icon,
this.color,
this.highlightColor,
this.splashColor,
this.disabledColor,
#required this.onPressed,
this.tooltip,
#required this.constraints})
: assert(iconSize != null),
assert(padding != null),
assert(alignment != null),
assert(icon != null),
assert(constraints != null),
super(key: key);
final double iconSize;
final BoxConstraints constraints;
final EdgeInsetsGeometry padding;
final AlignmentGeometry alignment;
final Widget icon;
final Color color;
final Color splashColor;
final Color highlightColor;
final Color disabledColor;
final VoidCallback onPressed;
final String tooltip;
#override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
Color currentColor;
if (onPressed != null)
currentColor = color;
else
currentColor = disabledColor ?? Theme.of(context).disabledColor;
Widget result = Semantics(
button: true,
enabled: onPressed != null,
child: ConstrainedBox(
constraints: constraints,
child: Padding(
padding: padding,
child: SizedBox(
height: iconSize,
width: iconSize,
child: Align(
alignment: alignment,
child: IconTheme.merge(
data: IconThemeData(size: iconSize, color: currentColor),
child: icon),
),
),
),
),
);
if (tooltip != null) {
result = Tooltip(message: tooltip, child: result);
}
return InkResponse(
onTap: onPressed,
child: result,
highlightColor: highlightColor ?? Theme.of(context).highlightColor,
splashColor: splashColor ?? Theme.of(context).splashColor,
radius: math.max(
Material.defaultSplashRadius,
(iconSize + math.min(padding.horizontal, padding.vertical)) * 0.7,
// x 0.5 for diameter -> radius and + 40% overflow derived from other Material apps.
),
);
}
#override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<Widget>('icon', icon, showName: false));
properties.add(ObjectFlagProperty<VoidCallback>('onPressed', onPressed,
ifNull: 'disabled'));
properties.add(
StringProperty('tooltip', tooltip, defaultValue: null, quoted: false));
}
}
Hope it helps!