1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101 |
- /*
- * Created by AHMED ELSAYED on 30 Nov 2021.
- * email: ahmedelsaayid@gmail.com
- * Edits made on original source code by Flutter.
- * Copyright 2014 The Flutter Authors. All rights reserved.
- */
- import 'dart:math' as math;
- import 'package:flutter/foundation.dart';
- import 'package:flutter/material.dart';
- import 'package:flutter/rendering.dart';
- import 'package:flutter/services.dart';
- const Duration _kDropdownMenuDuration = Duration(milliseconds: 300);
- const double _kMenuItemHeight = kMinInteractiveDimension;
- const double _kDenseButtonHeight = 24.0;
- const EdgeInsets _kMenuItemPadding = EdgeInsets.symmetric(horizontal: 16.0);
- const EdgeInsetsGeometry _kAlignedButtonPadding =
- EdgeInsetsDirectional.only(start: 16.0, end: 4.0);
- const EdgeInsets _kUnalignedButtonPadding = EdgeInsets.zero;
- typedef _OnMenuStateChangeFn = void Function(bool isOpen);
- typedef _SearchMatchFn = bool Function(
- DropdownMenuItem item,
- String searchValue,
- );
- double clampDouble(double min, double max, double input) {
- if (input < min) {
- return min;
- } else if (input > max) {
- return max;
- }
- return input;
- }
- _SearchMatchFn _defaultSearchMatchFn = (item, searchValue) =>
- item.value.toString().toLowerCase().contains(searchValue.toLowerCase());
- class _DropdownMenuPainter extends CustomPainter {
- _DropdownMenuPainter({
- this.color,
- this.elevation,
- this.selectedIndex,
- required this.resize,
- required this.itemHeight,
- this.dropdownDecoration,
- }) : _painter = dropdownDecoration
- ?.copyWith(
- color: dropdownDecoration.color ?? color,
- boxShadow: dropdownDecoration.boxShadow ??
- kElevationToShadow[elevation],
- )
- .createBoxPainter() ??
- BoxDecoration(
- // If you add an image here, you must provide a real
- // configuration in the paint() function and you must provide some sort
- // of onChanged callback here.
- color: color,
- borderRadius: const BorderRadius.all(Radius.circular(2.0)),
- boxShadow: kElevationToShadow[elevation],
- ).createBoxPainter(),
- super(repaint: resize);
- final Color? color;
- final int? elevation;
- final int? selectedIndex;
- final Animation<double> resize;
- final double itemHeight;
- final BoxDecoration? dropdownDecoration;
- final BoxPainter _painter;
- @override
- void paint(Canvas canvas, Size size) {
- final Tween<double> top = Tween<double>(
- //Begin at 0.0 instead of selectedItemOffset so that the menu open animation
- //always start from top to bottom instead of starting from the selected item
- begin: 0.0,
- end: 0.0,
- );
- final Tween<double> bottom = Tween<double>(
- begin: clampDouble(top.begin! + itemHeight,
- math.min(itemHeight, size.height), size.height),
- end: size.height,
- );
- final Rect rect = Rect.fromLTRB(
- 0.0, top.evaluate(resize), size.width, bottom.evaluate(resize));
- _painter.paint(canvas, rect.topLeft, ImageConfiguration(size: rect.size));
- }
- @override
- bool shouldRepaint(_DropdownMenuPainter oldPainter) {
- return oldPainter.color != color ||
- oldPainter.elevation != elevation ||
- oldPainter.selectedIndex != selectedIndex ||
- oldPainter.dropdownDecoration != dropdownDecoration ||
- oldPainter.itemHeight != itemHeight ||
- oldPainter.resize != resize;
- }
- }
- // The widget that is the button wrapping the menu items.
- class _DropdownMenuItemButton<T> extends StatefulWidget {
- const _DropdownMenuItemButton({
- key,
- this.padding,
- required this.route,
- required this.buttonRect,
- required this.constraints,
- required this.itemIndex,
- required this.enableFeedback,
- this.customItemsHeights,
- this.customItemsHeight,
- });
- final _DropdownRoute<T> route;
- final EdgeInsets? padding;
- final Rect buttonRect;
- final BoxConstraints constraints;
- final int itemIndex;
- final bool enableFeedback;
- final List<double>? customItemsHeights;
- final double? customItemsHeight;
- @override
- _DropdownMenuItemButtonState<T> createState() =>
- _DropdownMenuItemButtonState<T>();
- }
- class _DropdownMenuItemButtonState<T>
- extends State<_DropdownMenuItemButton<T>> {
- void _handleFocusChange(bool focused) {
- final bool inTraditionalMode;
- switch (FocusManager.instance.highlightMode) {
- case FocusHighlightMode.touch:
- inTraditionalMode = false;
- break;
- case FocusHighlightMode.traditional:
- inTraditionalMode = true;
- break;
- }
- if (focused && inTraditionalMode) {
- final _MenuLimits menuLimits = widget.route.getMenuLimits(
- widget.buttonRect,
- widget.constraints.maxHeight,
- widget.itemIndex,
- );
- widget.route.scrollController!.animateTo(
- menuLimits.scrollOffset,
- curve: Curves.easeInOut,
- duration: const Duration(milliseconds: 100),
- );
- }
- }
- void _handleOnTap() {
- final DropdownMenuItem<T> dropdownMenuItem =
- widget.route.items[widget.itemIndex].item!;
- dropdownMenuItem.onTap?.call();
- Navigator.pop(
- context,
- _DropdownRouteResult<T>(dropdownMenuItem.value),
- );
- }
- static const Map<ShortcutActivator, Intent> _webShortcuts =
- <ShortcutActivator, Intent>{
- // On the web, up/down don't change focus, *except* in a <select>
- // element, which is what a dropdown emulates.
- SingleActivator(LogicalKeyboardKey.arrowDown):
- DirectionalFocusIntent(TraversalDirection.down),
- SingleActivator(LogicalKeyboardKey.arrowUp):
- DirectionalFocusIntent(TraversalDirection.up),
- };
- @override
- Widget build(BuildContext context) {
- final DropdownMenuItem<T> dropdownMenuItem =
- widget.route.items[widget.itemIndex].item!;
- final double unit = 0.5 / (widget.route.items.length + 1.5);
- final double start =
- clampDouble(0.5 + (widget.itemIndex + 1) * unit, 0.0, 1.0);
- final double end = clampDouble(start + 1.5 * unit, 0.0, 1.0);
- final CurvedAnimation opacity = CurvedAnimation(
- parent: widget.route.animation!, curve: Interval(start, end));
- Widget child = Container(
- padding: widget.padding,
- height: widget.customItemsHeights == null
- ? widget.route.itemHeight
- : widget.customItemsHeights![widget.itemIndex],
- child: widget.route.items[widget.itemIndex],
- );
- // An [InkWell] is added to the item only if it is enabled
- // isNoSelectedItem to avoid first item highlight when no item selected
- if (dropdownMenuItem.enabled) {
- final _isSelectedItem = !widget.route.isNoSelectedItem &&
- widget.itemIndex == widget.route.selectedIndex;
- child = InkWell(
- autofocus: _isSelectedItem,
- enableFeedback: widget.enableFeedback,
- onTap: _handleOnTap,
- onFocusChange: _handleFocusChange,
- child: Container(
- color:
- _isSelectedItem ? widget.route.selectedItemHighlightColor : null,
- child: child,
- ),
- );
- }
- child = FadeTransition(opacity: opacity, child: child);
- if (kIsWeb && dropdownMenuItem.enabled) {
- child = Shortcuts(
- shortcuts: _webShortcuts,
- child: child,
- );
- }
- return child;
- }
- }
- class _DropdownMenu<T> extends StatefulWidget {
- const _DropdownMenu({
- key,
- this.padding,
- required this.route,
- required this.buttonRect,
- required this.constraints,
- required this.enableFeedback,
- required this.itemHeight,
- this.dropdownDecoration,
- this.dropdownPadding,
- this.dropdownScrollPadding,
- this.scrollbarRadius,
- this.scrollbarThickness,
- this.scrollbarAlwaysShow,
- required this.offset,
- this.customItemsHeights,
- this.searchController,
- this.searchInnerWidget,
- this.searchMatchFn,
- });
- final _DropdownRoute<T> route;
- final EdgeInsets? padding;
- final Rect buttonRect;
- final BoxConstraints constraints;
- final bool enableFeedback;
- final double itemHeight;
- final BoxDecoration? dropdownDecoration;
- final EdgeInsetsGeometry? dropdownPadding;
- final EdgeInsetsGeometry? dropdownScrollPadding;
- final Radius? scrollbarRadius;
- final double? scrollbarThickness;
- final bool? scrollbarAlwaysShow;
- final Offset offset;
- final List<double>? customItemsHeights;
- final TextEditingController? searchController;
- final Widget? searchInnerWidget;
- final _SearchMatchFn? searchMatchFn;
- @override
- _DropdownMenuState<T> createState() => _DropdownMenuState<T>();
- }
- class _DropdownMenuState<T> extends State<_DropdownMenu<T>> {
- late CurvedAnimation _fadeOpacity;
- late CurvedAnimation _resize;
- late List<Widget> _children;
- late _SearchMatchFn _searchMatchFn;
- @override
- void initState() {
- super.initState();
- // We need to hold these animations as state because of their curve
- // direction. When the route's animation reverses, if we were to recreate
- // the CurvedAnimation objects in build, we'd lose
- // CurvedAnimation._curveDirection.
- _fadeOpacity = CurvedAnimation(
- parent: widget.route.animation!,
- curve: const Interval(0.0, 0.25),
- reverseCurve: const Interval(0.75, 1.0),
- );
- _resize = CurvedAnimation(
- parent: widget.route.animation!,
- curve: const Interval(0.25, 0.5),
- reverseCurve: const Threshold(0.0),
- );
- //If searchController is null, then it'll perform as a normal dropdown
- //and search functions will not be executed.
- if (widget.searchController == null) {
- _children = <Widget>[
- for (int index = 0; index < widget.route.items.length; ++index)
- _DropdownMenuItemButton<T>(
- route: widget.route,
- padding: widget.padding,
- buttonRect: widget.buttonRect,
- constraints: widget.constraints,
- itemIndex: index,
- enableFeedback: widget.enableFeedback,
- customItemsHeights: widget.customItemsHeights,
- ),
- ];
- } else {
- _searchMatchFn = widget.searchMatchFn ?? _defaultSearchMatchFn;
- _children = _getSearchItems();
- // Add listener to searchController (if it's used) to update the shown items.
- widget.searchController?.addListener(_updateSearchItems);
- }
- }
- void _updateSearchItems() {
- _children = _getSearchItems();
- setState(() {});
- }
- List<Widget> _getSearchItems() {
- return <Widget>[
- for (int index = 0; index < widget.route.items.length; ++index)
- if (_searchMatchFn(
- widget.route.items[index].item!, widget.searchController!.text))
- _DropdownMenuItemButton<T>(
- route: widget.route,
- padding: widget.padding,
- buttonRect: widget.buttonRect,
- constraints: widget.constraints,
- itemIndex: index,
- enableFeedback: widget.enableFeedback,
- customItemsHeights: widget.customItemsHeights,
- ),
- ];
- }
- @override
- void dispose() {
- _fadeOpacity.dispose();
- _resize.dispose();
- widget.searchController?.removeListener(_updateSearchItems);
- super.dispose();
- }
- @override
- Widget build(BuildContext context) {
- // The menu is shown in three stages (unit timing in brackets):
- // [0s - 0.25s] - Fade in a rect-sized menu container with the selected item.
- // [0.25s - 0.5s] - Grow the otherwise empty menu container from the center
- // until it's big enough for as many items as we're going to show.
- // [0.5s - 1.0s] Fade in the remaining visible items from top to bottom.
- //
- // When the menu is dismissed we just fade the entire thing out
- // in the first 0.25s.
- assert(debugCheckHasMaterialLocalizations(context));
- final MaterialLocalizations localizations =
- MaterialLocalizations.of(context);
- final _DropdownRoute<T> route = widget.route;
- return FadeTransition(
- opacity: _fadeOpacity,
- child: CustomPaint(
- painter: _DropdownMenuPainter(
- color: Theme.of(context).canvasColor,
- elevation: route.elevation,
- selectedIndex: route.selectedIndex,
- resize: _resize,
- itemHeight: widget.itemHeight,
- dropdownDecoration: widget.dropdownDecoration,
- ),
- child: Semantics(
- scopesRoute: true,
- namesRoute: true,
- explicitChildNodes: true,
- label: localizations.popupMenuLabel,
- child: ClipRRect(
- //Prevent scrollbar, ripple effect & items from going beyond border boundaries when scrolling.
- clipBehavior: widget.dropdownDecoration?.borderRadius != null
- ? Clip.antiAlias
- : Clip.none,
- borderRadius: widget.dropdownDecoration?.borderRadius
- ?.resolve(Directionality.of(context)) ??
- BorderRadius.zero,
- child: Material(
- type: MaterialType.transparency,
- textStyle: route.style,
- child: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- if (widget.searchInnerWidget != null)
- widget.searchInnerWidget!,
- Flexible(
- child: Padding(
- padding: widget.dropdownScrollPadding ?? EdgeInsets.zero,
- child: ScrollConfiguration(
- // Dropdown menus should never overscroll or display an overscroll indicator.
- // Scrollbars are built-in below.
- // Platform must use Theme and ScrollPhysics must be Clamping.
- behavior: ScrollConfiguration.of(context).copyWith(
- scrollbars: false,
- overscroll: false,
- physics: const ClampingScrollPhysics(),
- platform: Theme.of(context).platform,
- ),
- child: PrimaryScrollController(
- controller: widget.route.scrollController!,
- child: Scrollbar(
- radius: widget.scrollbarRadius,
- thickness: widget.scrollbarThickness,
- isAlwaysShown: widget.scrollbarAlwaysShow,
- child: ListView(
- // Ensure this always inherits the PrimaryScrollController
- primary: true,
- padding: widget.dropdownPadding ??
- kMaterialListPadding,
- shrinkWrap: true,
- children: _children,
- ),
- ),
- ),
- ),
- ),
- ),
- ],
- ),
- ),
- ),
- ),
- ),
- );
- }
- }
- class _DropdownMenuRouteLayout<T> extends SingleChildLayoutDelegate {
- _DropdownMenuRouteLayout({
- required this.buttonRect,
- required this.route,
- required this.textDirection,
- required this.itemHeight,
- this.itemWidth,
- required this.offset,
- });
- final Rect buttonRect;
- final _DropdownRoute<T> route;
- final TextDirection? textDirection;
- final double itemHeight;
- final double? itemWidth;
- final Offset offset;
- @override
- BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
- // The maximum height of a simple menu should be one or more rows less than
- // the view height. This ensures a tappable area outside of the simple menu
- // with which to dismiss the menu.
- // -- https://material.io/design/components/menus.html#usage
- double maxHeight = math.max(0.0, constraints.maxHeight - 2 * itemHeight);
- if (route.menuMaxHeight != null && route.menuMaxHeight! <= maxHeight) {
- maxHeight = route.menuMaxHeight!;
- }
- // The width of a menu should be at most the view width. This ensures that
- // the menu does not extend past the left and right edges of the screen.
- final double width =
- itemWidth ?? math.min(constraints.maxWidth, buttonRect.width);
- return BoxConstraints(
- minWidth: width,
- maxWidth: width,
- maxHeight: maxHeight,
- );
- }
- @override
- Offset getPositionForChild(Size size, Size childSize) {
- final _MenuLimits menuLimits =
- route.getMenuLimits(buttonRect, size.height, route.selectedIndex);
- assert(() {
- final Rect container = Offset.zero & size;
- if (container.intersect(buttonRect) == buttonRect) {
- // If the button was entirely on-screen, then verify
- // that the menu is also on-screen.
- // If the button was a bit off-screen, then, oh well.
- assert(menuLimits.top >= 0.0);
- assert(menuLimits.top + menuLimits.height <= size.height);
- }
- return true;
- }());
- assert(textDirection != null);
- final double left;
- switch (textDirection!) {
- case TextDirection.rtl:
- left = clampDouble(buttonRect.right + offset.dx, 0.0, size.width) -
- childSize.width;
- break;
- case TextDirection.ltr:
- left = clampDouble(
- buttonRect.left + offset.dx, 0.0, size.width - childSize.width);
- break;
- }
- return Offset(left, menuLimits.top);
- }
- @override
- bool shouldRelayout(_DropdownMenuRouteLayout<T> oldDelegate) {
- return buttonRect != oldDelegate.buttonRect ||
- textDirection != oldDelegate.textDirection;
- }
- }
- // We box the return value so that the return value can be null. Otherwise,
- // canceling the route (which returns null) would get confused with actually
- // returning a real null value.
- @immutable
- class _DropdownRouteResult<T> {
- const _DropdownRouteResult(this.result);
- final T? result;
- @override
- bool operator ==(Object other) {
- return other is _DropdownRouteResult<T> && other.result == result;
- }
- @override
- int get hashCode => result.hashCode;
- }
- class _MenuLimits {
- const _MenuLimits(this.top, this.bottom, this.height, this.scrollOffset);
- final double top;
- final double bottom;
- final double height;
- final double scrollOffset;
- }
- class _DropdownRoute<T> extends PopupRoute<_DropdownRouteResult<T>> {
- _DropdownRoute({
- required this.items,
- required this.padding,
- required this.buttonRect,
- required this.selectedIndex,
- required this.isNoSelectedItem,
- this.selectedItemHighlightColor,
- this.elevation = 8,
- required this.capturedThemes,
- required this.style,
- required this.barrierDismissible,
- this.barrierColor,
- this.barrierLabel,
- required this.enableFeedback,
- required this.itemHeight,
- this.itemWidth,
- this.menuMaxHeight,
- this.dropdownDecoration,
- this.dropdownPadding,
- this.dropdownScrollPadding,
- this.scrollbarRadius,
- this.scrollbarThickness,
- this.scrollbarAlwaysShow,
- required this.offset,
- required this.showAboveButton,
- this.customItemsHeights,
- this.searchController,
- this.searchInnerWidget,
- this.searchMatchFn,
- }) : itemHeights =
- customItemsHeights ?? List<double>.filled(items.length, itemHeight);
- final List<_MenuItem<T>> items;
- final EdgeInsetsGeometry padding;
- final ValueNotifier<Rect?> buttonRect;
- final int selectedIndex;
- final bool isNoSelectedItem;
- final Color? selectedItemHighlightColor;
- final int elevation;
- final CapturedThemes capturedThemes;
- final TextStyle style;
- final bool enableFeedback;
- final double itemHeight;
- final double? itemWidth;
- final double? menuMaxHeight;
- final BoxDecoration? dropdownDecoration;
- final EdgeInsetsGeometry? dropdownPadding;
- final EdgeInsetsGeometry? dropdownScrollPadding;
- final Radius? scrollbarRadius;
- final double? scrollbarThickness;
- final bool? scrollbarAlwaysShow;
- final Offset offset;
- final bool showAboveButton;
- final List<double>? customItemsHeights;
- final TextEditingController? searchController;
- final Widget? searchInnerWidget;
- final _SearchMatchFn? searchMatchFn;
- final List<double> itemHeights;
- ScrollController? scrollController;
- @override
- Duration get transitionDuration => _kDropdownMenuDuration;
- @override
- final bool barrierDismissible;
- @override
- final Color? barrierColor;
- @override
- final String? barrierLabel;
- @override
- Widget buildPage(BuildContext context, Animation<double> animation,
- Animation<double> secondaryAnimation) {
- return LayoutBuilder(
- builder: (BuildContext context, BoxConstraints constraints) {
- return ValueListenableBuilder<Rect?>(
- valueListenable: buttonRect,
- builder: (context, rect, _) {
- return _DropdownRoutePage<T>(
- route: this,
- constraints: constraints,
- items: items,
- padding: padding,
- buttonRect: rect!,
- selectedIndex: selectedIndex,
- elevation: elevation,
- capturedThemes: capturedThemes,
- style: style,
- enableFeedback: enableFeedback,
- dropdownDecoration: dropdownDecoration,
- dropdownPadding: dropdownPadding,
- dropdownScrollPadding: dropdownScrollPadding,
- menuMaxHeight: menuMaxHeight,
- itemHeight: itemHeight,
- itemWidth: itemWidth,
- scrollbarRadius: scrollbarRadius,
- scrollbarThickness: scrollbarThickness,
- scrollbarAlwaysShow: scrollbarAlwaysShow,
- offset: offset,
- customItemsHeights: customItemsHeights,
- searchController: searchController,
- searchInnerWidget: searchInnerWidget,
- searchMatchFn: searchMatchFn,
- );
- },
- );
- },
- );
- }
- void _dismiss() {
- if (isActive) {
- navigator?.removeRoute(this);
- }
- }
- double getItemOffset(int index, double paddingTop) {
- double offset = paddingTop;
- if (items.isNotEmpty && index > 0) {
- assert(items.length == itemHeights.length);
- offset += itemHeights
- .sublist(0, index)
- .reduce((double total, double height) => total + height);
- }
- return offset;
- }
- // Returns the vertical extent of the menu and the initial scrollOffset
- // for the ListView that contains the menu items. The vertical center of the
- // selected item is aligned with the button's vertical center, as far as
- // that's possible given availableHeight.
- _MenuLimits getMenuLimits(
- Rect buttonRect, double availableHeight, int index) {
- double computedMaxHeight = availableHeight - 2.0 * itemHeight;
- if (menuMaxHeight != null) {
- computedMaxHeight = math.min(computedMaxHeight, menuMaxHeight!);
- }
- final double buttonTop = buttonRect.top;
- final double buttonBottom = math.min(buttonRect.bottom, availableHeight);
- final double selectedItemOffset = getItemOffset(
- index,
- dropdownPadding != null
- ? dropdownPadding!.resolve(null).top
- : kMaterialListPadding.top,
- );
- // If the button is placed on the bottom or top of the screen, its top or
- // bottom may be less than [_kMenuItemHeight] from the edge of the screen.
- // In this case, we want to change the menu limits to align with the top
- // or bottom edge of the button.
- final double topLimit = math.min(itemHeight, buttonTop);
- final double bottomLimit = math.max(availableHeight, buttonBottom);
- double menuTop =
- showAboveButton ? buttonTop - offset.dy : buttonBottom - offset.dy;
- double preferredMenuHeight =
- dropdownPadding?.vertical ?? kMaterialListPadding.vertical;
- if (items.isNotEmpty) {
- preferredMenuHeight +=
- itemHeights.reduce((double total, double height) => total + height);
- }
- // If there are too many elements in the menu, we need to shrink it down
- // so it is at most the computedMaxHeight.
- final double menuHeight = math.min(computedMaxHeight, preferredMenuHeight);
- double menuBottom = menuTop + menuHeight;
- // If the computed top or bottom of the menu are outside of the range
- // specified, we need to bring them into range. If the item height is larger
- // than the button height and the button is at the very bottom or top of the
- // screen, the menu will be aligned with the bottom or top of the button
- // respectively.
- if (menuTop < topLimit) {
- menuTop = math.min(buttonTop, topLimit);
- menuBottom = menuTop + menuHeight;
- }
- if (menuBottom > bottomLimit) {
- menuBottom = math.max(buttonBottom, bottomLimit);
- menuTop = menuBottom - menuHeight;
- }
- if (menuBottom - itemHeights[selectedIndex] / 2.0 <
- buttonBottom - buttonRect.height / 2.0) {
- menuBottom = math.max(buttonBottom, bottomLimit);
- menuTop = menuBottom - menuHeight;
- }
- double scrollOffset = 0;
- // If all of the menu items will not fit within availableHeight then
- // compute the scroll offset that will line the selected menu item up
- // with the select item. This is only done when the menu is first
- // shown - subsequently we leave the scroll offset where the user left
- // it. This scroll offset is only accurate for fixed height menu items
- // (the default).
- if (preferredMenuHeight > computedMaxHeight) {
- // The offset should be zero if the selected item is in view at the beginning
- // of the menu. Otherwise, the scroll offset should center the item if possible.
- scrollOffset = math.max(
- 0.0,
- selectedItemOffset -
- (menuHeight / 2) +
- (itemHeights[selectedIndex] / 2));
- // If the selected item's scroll offset is greater than the maximum scroll offset,
- // set it instead to the maximum allowed scroll offset.
- scrollOffset = math.min(scrollOffset, preferredMenuHeight - menuHeight);
- }
- assert((menuBottom - menuTop - menuHeight).abs() < precisionErrorTolerance);
- return _MenuLimits(menuTop, menuBottom, menuHeight, scrollOffset);
- }
- }
- class _DropdownRoutePage<T> extends StatelessWidget {
- const _DropdownRoutePage({
- key,
- required this.route,
- required this.constraints,
- this.items,
- required this.padding,
- required this.buttonRect,
- required this.selectedIndex,
- this.elevation = 8,
- required this.capturedThemes,
- this.style,
- required this.enableFeedback,
- this.dropdownDecoration,
- this.dropdownPadding,
- this.dropdownScrollPadding,
- this.menuMaxHeight,
- required this.itemHeight,
- this.itemWidth,
- this.scrollbarRadius,
- this.scrollbarThickness,
- this.scrollbarAlwaysShow,
- required this.offset,
- this.customItemsHeights,
- this.searchController,
- this.searchInnerWidget,
- this.searchMatchFn,
- });
- final _DropdownRoute<T> route;
- final BoxConstraints constraints;
- final List<_MenuItem<T>>? items;
- final EdgeInsetsGeometry padding;
- final Rect buttonRect;
- final int selectedIndex;
- final int elevation;
- final CapturedThemes capturedThemes;
- final TextStyle? style;
- final bool enableFeedback;
- final BoxDecoration? dropdownDecoration;
- final EdgeInsetsGeometry? dropdownPadding;
- final EdgeInsetsGeometry? dropdownScrollPadding;
- final double? menuMaxHeight;
- final double itemHeight;
- final double? itemWidth;
- final Radius? scrollbarRadius;
- final double? scrollbarThickness;
- final bool? scrollbarAlwaysShow;
- final Offset offset;
- final List<double>? customItemsHeights;
- final TextEditingController? searchController;
- final Widget? searchInnerWidget;
- final _SearchMatchFn? searchMatchFn;
- @override
- Widget build(BuildContext context) {
- assert(debugCheckHasDirectionality(context));
- // Computing the initialScrollOffset now, before the items have been laid
- // out. This only works if the item heights are effectively fixed, i.e. either
- // DropdownButton.itemHeight is specified or DropdownButton.itemHeight is null
- // and all of the items' intrinsic heights are less than kMinInteractiveDimension.
- // Otherwise the initialScrollOffset is just a rough approximation based on
- // treating the items as if their heights were all equal to kMinInteractiveDimension.
- if (route.scrollController == null) {
- final _MenuLimits menuLimits =
- route.getMenuLimits(buttonRect, constraints.maxHeight, selectedIndex);
- route.scrollController =
- ScrollController(initialScrollOffset: menuLimits.scrollOffset);
- }
- final TextDirection? textDirection = Directionality.maybeOf(context);
- final Widget menu = _DropdownMenu<T>(
- route: route,
- padding: padding.resolve(textDirection),
- buttonRect: buttonRect,
- constraints: constraints,
- enableFeedback: enableFeedback,
- itemHeight: itemHeight,
- dropdownDecoration: dropdownDecoration,
- dropdownPadding: dropdownPadding,
- dropdownScrollPadding: dropdownScrollPadding,
- scrollbarRadius: scrollbarRadius,
- scrollbarThickness: scrollbarThickness,
- scrollbarAlwaysShow: scrollbarAlwaysShow,
- offset: offset,
- customItemsHeights: customItemsHeights,
- searchController: searchController,
- searchInnerWidget: searchInnerWidget,
- searchMatchFn: searchMatchFn,
- );
- return MediaQuery.removePadding(
- context: context,
- removeTop: true,
- removeBottom: true,
- removeLeft: true,
- removeRight: true,
- child: Builder(
- builder: (BuildContext context) {
- return CustomSingleChildLayout(
- delegate: _DropdownMenuRouteLayout<T>(
- buttonRect: buttonRect,
- route: route,
- textDirection: textDirection,
- itemHeight: itemHeight,
- itemWidth: itemWidth,
- offset: offset,
- ),
- child: capturedThemes.wrap(menu),
- );
- },
- ),
- );
- }
- }
- // This widget enables _DropdownRoute to look up the sizes of
- // each menu item. These sizes are used to compute the offset of the selected
- // item so that _DropdownRoutePage can align the vertical center of the
- // selected item lines up with the vertical center of the dropdown button,
- // as closely as possible.
- class _MenuItem<T> extends SingleChildRenderObjectWidget {
- const _MenuItem({
- key,
- required this.onLayout,
- required this.item,
- }) : super(child: item);
- final ValueChanged<Size> onLayout;
- final DropdownMenuItem<T>? item;
- @override
- RenderObject createRenderObject(BuildContext context) {
- return _RenderMenuItem(onLayout);
- }
- @override
- void updateRenderObject(
- BuildContext context, covariant _RenderMenuItem renderObject) {
- renderObject.onLayout = onLayout;
- }
- }
- class _RenderMenuItem extends RenderProxyBox {
- _RenderMenuItem(this.onLayout, [RenderBox? child]) : super(child);
- ValueChanged<Size> onLayout;
- @override
- void performLayout() {
- super.performLayout();
- onLayout(size);
- }
- }
- // The container widget for a menu item created by a [DropdownButton]. It
- // provides the default configuration for [DropdownMenuItem]s, as well as a
- // [DropdownButton]'s hint and disabledHint widgets.
- class _DropdownMenuItemContainer extends StatelessWidget {
- /// Creates an item for a dropdown menu.
- ///
- /// The [child] argument is required.
- const _DropdownMenuItemContainer({
- Key? key,
- this.alignment = AlignmentDirectional.centerStart,
- required this.child,
- }) : super(key: key);
- /// The widget below this widget in the tree.
- ///
- /// Typically a [Text] widget.
- final Widget child;
- /// Defines how the item is positioned within the container.
- ///
- /// This property must not be null. It defaults to [AlignmentDirectional.centerStart].
- ///
- /// See also:
- ///
- /// * [Alignment], a class with convenient constants typically used to
- /// specify an [AlignmentGeometry].
- /// * [AlignmentDirectional], like [Alignment] for specifying alignments
- /// relative to text direction.
- final AlignmentGeometry alignment;
- @override
- Widget build(BuildContext context) {
- return Container(
- constraints: const BoxConstraints(minHeight: _kMenuItemHeight),
- alignment: alignment,
- child: child,
- );
- }
- }
- /// A Material Design button for selecting from a list of items.
- ///
- /// A dropdown button lets the user select from a number of items. The button
- /// shows the currently selected item as well as an arrow that opens a menu for
- /// selecting another item.
- ///
- /// One ancestor must be a [Material] widget and typically this is
- /// provided by the app's [Scaffold].
- ///
- /// The type `T` is the type of the [value] that each dropdown item represents.
- /// All the entries in a given menu must represent values with consistent types.
- /// Typically, an enum is used. Each [DropdownMenuItem] in [items] must be
- /// specialized with that same type argument.
- ///
- /// The [onChanged] callback should update a state variable that defines the
- /// dropdown's value. It should also call [State.setState] to rebuild the
- /// dropdown with the new value.
- ///
- /// {@tool dartpad}
- /// This sample shows a `DropdownButton` with a large arrow icon,
- /// purple text style, and bold purple underline, whose value is one of "One",
- /// "Two", "Free", or "Four".
- ///
- /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/dropdown_button.png)
- ///
- /// ** See code in examples/api/lib/material/dropdown/dropdown_button.0.dart **
- /// {@end-tool}
- ///
- /// If the [onChanged] callback is null or the list of [items] is null
- /// then the dropdown button will be disabled, i.e. its arrow will be
- /// displayed in grey and it will not respond to input. A disabled button
- /// will display the [disabledHint] widget if it is non-null. However, if
- /// [disabledHint] is null and [hint] is non-null, the [hint] widget will
- /// instead be displayed.
- ///
- /// Requires one of its ancestors to be a [Material] widget.
- ///
- /// See also:
- ///
- /// * [DropdownButtonFormField2], which integrates with the [Form] widget.
- /// * [DropdownMenuItem], the class used to represent the [items].
- /// * [DropdownButtonHideUnderline], which prevents its descendant dropdown buttons
- /// from displaying their underlines.
- /// * [ElevatedButton], [TextButton], ordinary buttons that trigger a single action.
- /// * <https://material.io/design/components/menus.html#dropdown-menu>
- class DropdownButton2<T> extends StatefulWidget {
- /// Creates a DropdownButton2
- /// It's customizable DropdownButton with steady dropdown menu and many other features.
- ///
- /// The [items] must have distinct values. If [value] isn't null then it
- /// must be equal to one of the [DropdownMenuItem] values. If [items] or
- /// [onChanged] is null, the button will be disabled, the down arrow
- /// will be greyed out.
- ///
- /// If [value] is null and the button is enabled, [hint] will be displayed
- /// if it is non-null.
- ///
- /// If [value] is null and the button is disabled, [disabledHint] will be displayed
- /// if it is non-null. If [disabledHint] is null, then [hint] will be displayed
- /// if it is non-null.
- DropdownButton2({
- key,
- required this.items,
- this.selectedItemBuilder,
- this.value,
- this.hint,
- this.disabledHint,
- this.onChanged,
- this.onMenuStateChange,
- this.dropdownElevation = 8,
- this.style,
- this.underline,
- this.icon,
- this.iconOnClick,
- this.iconDisabledColor,
- this.iconEnabledColor,
- this.iconSize = 24.0,
- this.isDense = false,
- this.isExpanded = false,
- this.itemHeight = kMinInteractiveDimension,
- this.focusColor,
- this.focusNode,
- this.autofocus = false,
- this.dropdownMaxHeight,
- this.enableFeedback,
- this.alignment = AlignmentDirectional.centerStart,
- this.buttonHeight,
- this.buttonWidth,
- this.buttonPadding,
- this.buttonDecoration,
- this.buttonElevation,
- this.itemPadding,
- this.dropdownWidth,
- this.dropdownPadding,
- this.dropdownScrollPadding,
- this.dropdownDecoration,
- this.selectedItemHighlightColor,
- this.scrollbarRadius,
- this.scrollbarThickness,
- this.scrollbarAlwaysShow,
- this.offset,
- this.customButton,
- this.customItemsHeights,
- this.openWithLongPress = false,
- this.dropdownOverButton = false,
- this.dropdownFullScreen = false,
- this.barrierDismissible = true,
- this.barrierColor,
- this.barrierLabel,
- this.searchController,
- this.searchInnerWidget,
- this.searchMatchFn,
- // When adding new arguments, consider adding similar arguments to
- // DropdownButtonFormField.
- }) : assert(
- items == null ||
- items.isEmpty ||
- value == null ||
- items.where((DropdownMenuItem<T> item) {
- return item.value == value;
- }).length ==
- 1,
- "There should be exactly one item with [DropdownButton]'s value: "
- '$value. \n'
- 'Either zero or 2 or more [DropdownMenuItem]s were detected '
- 'with the same value',
- ),
- assert(
- customItemsHeights == null ||
- items == null ||
- items.isEmpty ||
- customItemsHeights.length == items.length,
- "customItemsHeights list should have the same length of items list",
- ),
- formFieldCallBack = null;
- DropdownButton2._formField({
- keyy,
- required this.items,
- this.selectedItemBuilder,
- this.value,
- this.hint,
- this.disabledHint,
- required this.onChanged,
- this.onMenuStateChange,
- this.dropdownElevation = 8,
- this.style,
- this.underline,
- this.icon,
- this.iconOnClick,
- this.iconDisabledColor,
- this.iconEnabledColor,
- this.iconSize = 24.0,
- this.isDense = false,
- this.isExpanded = false,
- this.itemHeight = kMinInteractiveDimension,
- this.focusColor,
- this.focusNode,
- this.autofocus = false,
- this.dropdownMaxHeight,
- this.enableFeedback,
- this.alignment = AlignmentDirectional.centerStart,
- this.buttonHeight,
- this.buttonWidth,
- this.buttonPadding,
- this.buttonDecoration,
- this.buttonElevation,
- this.itemPadding,
- this.dropdownWidth,
- this.dropdownPadding,
- this.dropdownScrollPadding,
- this.dropdownDecoration,
- this.selectedItemHighlightColor,
- this.scrollbarRadius,
- this.scrollbarThickness,
- this.scrollbarAlwaysShow,
- this.offset,
- this.customButton,
- this.customItemsHeights,
- this.openWithLongPress = false,
- this.dropdownOverButton = false,
- this.dropdownFullScreen = false,
- this.barrierDismissible = true,
- this.barrierColor,
- this.barrierLabel,
- this.searchController,
- this.searchInnerWidget,
- this.searchMatchFn,
- this.formFieldCallBack,
- }) : assert(
- items == null ||
- items.isEmpty ||
- value == null ||
- items.where((DropdownMenuItem<T> item) {
- return item.value == value;
- }).length ==
- 1,
- "There should be exactly one item with [DropdownButtonFormField]'s value: "
- '$value. \n'
- 'Either zero or 2 or more [DropdownMenuItem]s were detected '
- 'with the same value',
- );
- // Parameters added By Me
- /// The height of the button.
- final double? buttonHeight;
- /// The width of the button
- final double? buttonWidth;
- /// The inner padding of the Button
- final EdgeInsetsGeometry? buttonPadding;
- /// The decoration of the Button
- final BoxDecoration? buttonDecoration;
- /// The elevation of the Button
- final int? buttonElevation;
- /// The padding of menu items
- final EdgeInsetsGeometry? itemPadding;
- /// The width of the dropdown menu
- final double? dropdownWidth;
- /// The inner padding of the dropdown menu
- final EdgeInsetsGeometry? dropdownPadding;
- /// The inner padding of the dropdown menu including the scrollbar
- final EdgeInsetsGeometry? dropdownScrollPadding;
- /// The decoration of the dropdown menu
- final BoxDecoration? dropdownDecoration;
- /// The highlight color of the current selected item
- final Color? selectedItemHighlightColor;
- /// The radius of the scrollbar's corners
- final Radius? scrollbarRadius;
- /// The thickness of the scrollbar
- final double? scrollbarThickness;
- /// Always show the scrollbar even when a scroll is not underway
- final bool? scrollbarAlwaysShow;
- /// Changes the position of the dropdown menu
- final Offset? offset;
- /// Uses custom widget like icon,image,etc.. instead of the default button
- final Widget? customButton;
- /// Uses different predefined heights for the menu items (useful for adding dividers)
- final List<double>? customItemsHeights;
- /// Opens the dropdown menu on long-pressing instead of tapping
- final bool openWithLongPress;
- /// Opens the dropdown menu over the button instead of below it
- final bool dropdownOverButton;
- /// Opens the dropdown menu in fullscreen mode (Above AppBar & TabBar)
- final bool dropdownFullScreen;
- /// Shows different icon when dropdown menu open
- final Widget? iconOnClick;
- /// Called when the dropdown menu is opened or closed.
- final _OnMenuStateChangeFn? onMenuStateChange;
- /// Whether you can dismiss this route by tapping the modal barrier.
- final bool barrierDismissible;
- /// The color to use for the modal barrier. If this is null, the barrier will
- /// be transparent.
- final Color? barrierColor;
- /// The semantic label used for a dismissible barrier.
- ///
- /// If the barrier is dismissible, this label will be read out if
- /// accessibility tools (like VoiceOver on iOS) focus on the barrier.
- final String? barrierLabel;
- /// The TextEditingController used for searchable dropdowns. If this is null,
- /// then it'll perform as a normal dropdown without searching feature.
- final TextEditingController? searchController;
- /// The widget to use for searchable dropdowns, such as search bar.
- /// It will be shown at the top of the dropdown menu.
- final Widget? searchInnerWidget;
- /// The match function used for searchable dropdowns. If this is null,
- /// then _defaultSearchMatchFn will be used.
- ///
- /// _defaultSearchMatchFn = (item, searchValue) =>
- /// item.value.toString().toLowerCase().contains(searchValue.toLowerCase());
- final _SearchMatchFn? searchMatchFn;
- /// The list of items the user can select.
- ///
- /// If the [onChanged] callback is null or the list of items is null
- /// then the dropdown button will be disabled, i.e. its arrow will be
- /// displayed in grey and it will not respond to input.
- final List<DropdownMenuItem<T>>? items;
- /// The value of the currently selected [DropdownMenuItem].
- ///
- /// If [value] is null and the button is enabled, [hint] will be displayed
- /// if it is non-null.
- ///
- /// If [value] is null and the button is disabled, [disabledHint] will be displayed
- /// if it is non-null. If [disabledHint] is null, then [hint] will be displayed
- /// if it is non-null.
- final T? value;
- /// A placeholder widget that is displayed by the dropdown button.
- ///
- /// If [value] is null and the dropdown is enabled ([items] and [onChanged] are non-null),
- /// this widget is displayed as a placeholder for the dropdown button's value.
- ///
- /// If [value] is null and the dropdown is disabled and [disabledHint] is null,
- /// this widget is used as the placeholder.
- final Widget? hint;
- /// A preferred placeholder widget that is displayed when the dropdown is disabled.
- ///
- /// If [value] is null, the dropdown is disabled ([items] or [onChanged] is null),
- /// this widget is displayed as a placeholder for the dropdown button's value.
- final Widget? disabledHint;
- /// {@template flutter.material.dropdownButton.onChanged}
- /// Called when the user selects an item.
- ///
- /// If the [onChanged] callback is null or the list of [DropdownButton2.items]
- /// is null then the dropdown button will be disabled, i.e. its arrow will be
- /// displayed in grey and it will not respond to input. A disabled button
- /// will display the [DropdownButton2.disabledHint] widget if it is non-null.
- /// If [DropdownButton2.disabledHint] is also null but [DropdownButton2.hint] is
- /// non-null, [DropdownButton2.hint] will instead be displayed.
- /// {@endtemplate}
- final ValueChanged<T?>? onChanged;
- /// A builder to customize the dropdown buttons corresponding to the
- /// [DropdownMenuItem]s in [items].
- ///
- /// When a [DropdownMenuItem] is selected, the widget that will be displayed
- /// from the list corresponds to the [DropdownMenuItem] of the same index
- /// in [items].
- ///
- /// {@tool dartpad}
- /// This sample shows a `DropdownButton` with a button with [Text] that
- /// corresponds to but is unique from [DropdownMenuItem].
- ///
- /// ** See code in examples/api/lib/material/dropdown/dropdown_button.selected_item_builder.0.dart **
- /// {@end-tool}
- ///
- /// If this callback is null, the [DropdownMenuItem] from [items]
- /// that matches [value] will be displayed.
- final DropdownButtonBuilder? selectedItemBuilder;
- /// The z-coordinate at which to place the menu when open.
- ///
- /// The following elevations have defined shadows: 1, 2, 3, 4, 6, 8, 9, 12,
- /// 16, and 24. See [kElevationToShadow].
- ///
- /// Defaults to 8, the appropriate elevation for dropdown buttons.
- final int dropdownElevation;
- /// The text style to use for text in the dropdown button and the dropdown
- /// menu that appears when you tap the button.
- ///
- /// To use a separate text style for selected item when it's displayed within
- /// the dropdown button, consider using [selectedItemBuilder].
- ///
- /// {@tool dartpad}
- /// This sample shows a `DropdownButton` with a dropdown button text style
- /// that is different than its menu items.
- ///
- /// ** See code in examples/api/lib/material/dropdown/dropdown_button.style.0.dart **
- /// {@end-tool}
- ///
- /// Defaults to the [TextTheme.titleMedium] value of the current
- /// [ThemeData.textTheme] of the current [Theme].
- final TextStyle? style;
- /// The widget to use for drawing the drop-down button's underline.
- ///
- /// Defaults to a 0.0 width bottom border with color 0xFFBDBDBD.
- final Widget? underline;
- /// The widget to use for the drop-down button's icon.
- ///
- /// Defaults to an [Icon] with the [Icons.arrow_drop_down] glyph.
- final Widget? icon;
- /// The color of any [Icon] descendant of [icon] if this button is disabled,
- /// i.e. if [onChanged] is null.
- ///
- /// Defaults to [MaterialColor.shade400] of [Colors.grey] when the theme's
- /// [ThemeData.brightness] is [Brightness.light] and to
- /// [Colors.white10] when it is [Brightness.dark]
- final Color? iconDisabledColor;
- /// The color of any [Icon] descendant of [icon] if this button is enabled,
- /// i.e. if [onChanged] is defined.
- ///
- /// Defaults to [MaterialColor.shade700] of [Colors.grey] when the theme's
- /// [ThemeData.brightness] is [Brightness.light] and to
- /// [Colors.white70] when it is [Brightness.dark]
- final Color? iconEnabledColor;
- /// The size to use for the drop-down button's icon.
- ///
- /// Defaults to 24.0.
- final double iconSize;
- /// Reduce the button's height.
- ///
- /// By default this button's height is the same as its menu items' heights.
- /// If isDense is true, the button's height is reduced by about half. This
- /// can be useful when the button is embedded in a container that adds
- /// its own decorations, like [InputDecorator].
- final bool isDense;
- /// Set the dropdown's inner contents to horizontally fill its parent.
- ///
- /// By default this button's inner width is the minimum size of its contents.
- /// If [isExpanded] is true, the inner width is expanded to fill its
- /// surrounding container.
- final bool isExpanded;
- /// The default value is [kMinInteractiveDimension]
- final double itemHeight;
- /// The color for the button's [Material] when it has the input focus.
- final Color? focusColor;
- /// {@macro flutter.widgets.Focus.focusNode}
- final FocusNode? focusNode;
- /// {@macro flutter.widgets.Focus.autofocus}
- final bool autofocus;
- /// The maximum height of the menu.
- ///
- /// The maximum height of the menu must be at least one row shorter than
- /// the height of the app's view. This ensures that a tappable area
- /// outside of the simple menu is present so the user can dismiss the menu.
- ///
- /// If this property is set above the maximum allowable height threshold
- /// mentioned above, then the menu defaults to being padded at the top
- /// and bottom of the menu by at one menu item's height.
- final double? dropdownMaxHeight;
- /// Whether detected gestures should provide acoustic and/or haptic feedback.
- ///
- /// For example, on Android a tap will produce a clicking sound and a
- /// long-press will produce a short vibration, when feedback is enabled.
- ///
- /// By default, platform-specific feedback is enabled.
- ///
- /// See also:
- ///
- /// * [Feedback] for providing platform-specific feedback to certain actions.
- final bool? enableFeedback;
- /// Defines how the hint or the selected item is positioned within the button.
- ///
- /// This property must not be null. It defaults to [AlignmentDirectional.centerStart].
- ///
- /// See also:
- ///
- /// * [Alignment], a class with convenient constants typically used to
- /// specify an [AlignmentGeometry].
- /// * [AlignmentDirectional], like [Alignment] for specifying alignments
- /// relative to text direction.
- final AlignmentGeometry alignment;
- /// Called when the dropdown menu is opened or closed in case of using
- /// DropdownButtonFormField2 to update the FormField's focus.
- final _OnMenuStateChangeFn? formFieldCallBack;
- @override
- State<DropdownButton2<T>> createState() => DropdownButton2State<T>();
- }
- class DropdownButton2State<T> extends State<DropdownButton2<T>>
- with WidgetsBindingObserver {
- int? _selectedIndex;
- _DropdownRoute<T>? _dropdownRoute;
- Orientation? _lastOrientation;
- FocusNode? _internalNode;
- FocusNode? get focusNode => widget.focusNode ?? _internalNode;
- bool _hasPrimaryFocus = false;
- late Map<Type, Action<Intent>> _actionMap;
- bool _isMenuOpen = false;
- // Using ValueNotifier for the Rect of DropdownButton so the dropdown menu listen and
- // update its position if DropdownButton's position has changed, as when keyboard open.
- final _rect = ValueNotifier<Rect?>(null);
- // Only used if needed to create _internalNode.
- FocusNode _createFocusNode() {
- return FocusNode(debugLabel: '${widget.runtimeType}');
- }
- @override
- void initState() {
- super.initState();
- WidgetsBinding.instance?.addObserver(this);
- _updateSelectedIndex();
- if (widget.focusNode == null) {
- _internalNode ??= _createFocusNode();
- }
- _actionMap = <Type, Action<Intent>>{
- ActivateIntent: CallbackAction<ActivateIntent>(
- onInvoke: (ActivateIntent intent) => _handleTap(),
- ),
- ButtonActivateIntent: CallbackAction<ButtonActivateIntent>(
- onInvoke: (ButtonActivateIntent intent) => _handleTap(),
- ),
- };
- focusNode!.addListener(_handleFocusChanged);
- }
- @override
- void dispose() {
- WidgetsBinding.instance?.removeObserver(this);
- _removeDropdownRoute();
- focusNode!.removeListener(_handleFocusChanged);
- _internalNode?.dispose();
- super.dispose();
- }
- void _removeDropdownRoute() {
- _dropdownRoute?._dismiss();
- _dropdownRoute = null;
- _lastOrientation = null;
- }
- void _handleFocusChanged() {
- if (_hasPrimaryFocus != focusNode!.hasPrimaryFocus) {
- setState(() {
- _hasPrimaryFocus = focusNode!.hasPrimaryFocus;
- });
- }
- }
- @override
- void didUpdateWidget(DropdownButton2<T> oldWidget) {
- super.didUpdateWidget(oldWidget);
- if (widget.focusNode != oldWidget.focusNode) {
- oldWidget.focusNode?.removeListener(_handleFocusChanged);
- if (widget.focusNode == null) {
- _internalNode ??= _createFocusNode();
- }
- _hasPrimaryFocus = focusNode!.hasPrimaryFocus;
- focusNode!.addListener(_handleFocusChanged);
- }
- _updateSelectedIndex();
- }
- void _updateSelectedIndex() {
- if (widget.items == null ||
- widget.items!.isEmpty ||
- (widget.value == null &&
- widget.items!
- .where((DropdownMenuItem<T> item) =>
- item.enabled && item.value == widget.value)
- .isEmpty)) {
- _selectedIndex = null;
- return;
- }
- assert(widget.items!
- .where((DropdownMenuItem<T> item) => item.value == widget.value)
- .length ==
- 1);
- for (int itemIndex = 0; itemIndex < widget.items!.length; itemIndex++) {
- if (widget.items![itemIndex].value == widget.value) {
- _selectedIndex = itemIndex;
- return;
- }
- }
- }
- @override
- void didChangeMetrics() {
- //This fix the bug of calling didChangeMetrics() on iOS when app starts
- if (_rect.value == null) return;
- final _newRect = _getRect();
- //This avoid unnecessary rebuilds if _rect position hasn't changed
- if (_rect.value!.top == _newRect.top) return;
- _rect.value = _newRect;
- }
- TextStyle? get _textStyle =>
- widget.style ?? Theme.of(context).textTheme.subtitle1;
- Rect _getRect() {
- final TextDirection? textDirection = Directionality.maybeOf(context);
- const EdgeInsetsGeometry menuMargin = EdgeInsets.zero;
- final NavigatorState navigator =
- Navigator.of(context, rootNavigator: widget.dropdownFullScreen);
- final RenderBox itemBox = context.findRenderObject()! as RenderBox;
- final Rect itemRect = itemBox.localToGlobal(Offset.zero,
- ancestor: navigator.context.findRenderObject()) &
- itemBox.size;
- return menuMargin.resolve(textDirection).inflateRect(itemRect);
- }
- void _handleTap() {
- final TextDirection? textDirection = Directionality.maybeOf(context);
- final List<_MenuItem<T>> menuItems = <_MenuItem<T>>[
- for (int index = 0; index < widget.items!.length; index += 1)
- _MenuItem<T>(
- item: widget.items![index],
- onLayout: (Size size) {
- // If [_dropdownRoute] is null and onLayout is called, this means
- // that performLayout was called on a _DropdownRoute that has not
- // left the widget tree but is already on its way out.
- //
- // Since onLayout is used primarily to collect the desired heights
- // of each menu item before laying them out, not having the _DropdownRoute
- // collect each item's height to lay out is fine since the route is
- // already on its way out.
- if (_dropdownRoute == null) return;
- _dropdownRoute!.itemHeights[index] = size.height;
- },
- ),
- ];
- final NavigatorState navigator =
- Navigator.of(context, rootNavigator: widget.dropdownFullScreen);
- assert(_dropdownRoute == null);
- _rect.value = _getRect();
- _dropdownRoute = _DropdownRoute<T>(
- items: menuItems,
- buttonRect: _rect,
- padding: widget.itemPadding ?? _kMenuItemPadding.resolve(textDirection),
- selectedIndex: _selectedIndex ?? 0,
- isNoSelectedItem: _selectedIndex == null,
- selectedItemHighlightColor: widget.selectedItemHighlightColor,
- elevation: widget.dropdownElevation,
- capturedThemes:
- InheritedTheme.capture(from: context, to: navigator.context),
- style: _textStyle!,
- barrierDismissible: widget.barrierDismissible,
- barrierColor: widget.barrierColor,
- barrierLabel: widget.barrierLabel ??
- MaterialLocalizations.of(context).modalBarrierDismissLabel,
- enableFeedback: widget.enableFeedback ?? true,
- itemHeight: widget.itemHeight,
- itemWidth: widget.dropdownWidth,
- menuMaxHeight: widget.dropdownMaxHeight,
- dropdownDecoration: widget.dropdownDecoration,
- dropdownPadding: widget.dropdownPadding,
- dropdownScrollPadding: widget.dropdownScrollPadding,
- scrollbarRadius: widget.scrollbarRadius,
- scrollbarThickness: widget.scrollbarThickness,
- scrollbarAlwaysShow: widget.scrollbarAlwaysShow,
- offset: widget.offset ?? const Offset(0, 0),
- showAboveButton: widget.dropdownOverButton,
- customItemsHeights: widget.customItemsHeights,
- searchController: widget.searchController,
- searchInnerWidget: widget.searchInnerWidget,
- searchMatchFn: widget.searchMatchFn,
- );
- _isMenuOpen = true;
- focusNode?.requestFocus();
- navigator
- .push(_dropdownRoute!)
- .then<void>((_DropdownRouteResult<T>? newValue) {
- _removeDropdownRoute();
- _isMenuOpen = false;
- widget.onMenuStateChange?.call(false);
- widget.formFieldCallBack?.call(false);
- if (!mounted || newValue == null) return;
- widget.onChanged?.call(newValue.result);
- });
- widget.onMenuStateChange?.call(true);
- widget.formFieldCallBack?.call(true);
- }
- // This expose the _handleTap() to Allow opening the button programmatically using GlobalKey.
- // Also, DropdownButton2State should be public as we need typed access to it through key.
- void callTap() => _handleTap();
- // When isDense is true, reduce the height of this button from _kMenuItemHeight to
- // _kDenseButtonHeight, but don't make it smaller than the text that it contains.
- // Similarly, we don't reduce the height of the button so much that its icon
- // would be clipped.
- double get _denseButtonHeight {
- final double textScaleFactor = MediaQuery.of(context).textScaleFactor;
- final double fontSize = _textStyle!.fontSize ??
- Theme.of(context).textTheme.subtitle1!.fontSize!;
- final double scaledFontSize = textScaleFactor * fontSize;
- return math.max(
- scaledFontSize, math.max(widget.iconSize, _kDenseButtonHeight));
- }
- Color get _iconColor {
- // These colors are not defined in the Material Design spec.
- if (_enabled) {
- if (widget.iconEnabledColor != null) return widget.iconEnabledColor!;
- switch (Theme.of(context).brightness) {
- case Brightness.light:
- return Colors.grey.shade700;
- case Brightness.dark:
- return Colors.white70;
- }
- } else {
- if (widget.iconDisabledColor != null) return widget.iconDisabledColor!;
- switch (Theme.of(context).brightness) {
- case Brightness.light:
- return Colors.grey.shade400;
- case Brightness.dark:
- return Colors.white10;
- }
- }
- }
- bool get _enabled =>
- widget.items != null &&
- widget.items!.isNotEmpty &&
- widget.onChanged != null;
- Orientation _getOrientation(BuildContext context) {
- Orientation? result = MediaQuery.maybeOf(context)?.orientation;
- if (result == null) {
- // If there's no MediaQuery, then use the window aspect to determine
- // orientation.
- final Size size =
- WidgetsBinding.instance?.window.physicalSize ?? const Size(0, 0);
- result = size.width > size.height
- ? Orientation.landscape
- : Orientation.portrait;
- }
- return result;
- }
- @override
- Widget build(BuildContext context) {
- assert(debugCheckHasMaterial(context));
- assert(debugCheckHasMaterialLocalizations(context));
- final Orientation newOrientation = _getOrientation(context);
- _lastOrientation ??= newOrientation;
- if (newOrientation != _lastOrientation) {
- _removeDropdownRoute();
- _lastOrientation = newOrientation;
- }
- // The width of the button and the menu are defined by the widest
- // item and the width of the hint.
- // We should explicitly type the items list to be a list of <Widget>,
- // otherwise, no explicit type adding items maybe trigger a crash/failure
- // when hint and selectedItemBuilder are provided.
- final List<Widget> items = widget.selectedItemBuilder == null
- ? (widget.items != null ? List<Widget>.of(widget.items!) : <Widget>[])
- : List<Widget>.of(widget.selectedItemBuilder!(context));
- int? hintIndex;
- if (widget.hint != null || (!_enabled && widget.disabledHint != null)) {
- final Widget displayedHint =
- _enabled ? widget.hint! : widget.disabledHint ?? widget.hint!;
- hintIndex = items.length;
- items.add(DefaultTextStyle(
- style: _textStyle!.copyWith(color: Theme.of(context).hintColor),
- child: IgnorePointer(
- ignoringSemantics: false,
- child: _DropdownMenuItemContainer(
- alignment: widget.alignment,
- child: displayedHint,
- ),
- ),
- ));
- }
- final EdgeInsetsGeometry padding = ButtonTheme.of(context).alignedDropdown
- ? _kAlignedButtonPadding
- : _kUnalignedButtonPadding;
- // If value is null (then _selectedIndex is null) then we
- // display the hint or nothing at all.
- final Widget innerItemsWidget;
- if (items.isEmpty) {
- innerItemsWidget = const SizedBox.shrink();
- } else {
- innerItemsWidget = IndexedStack(
- index: _selectedIndex ?? hintIndex,
- alignment: widget.alignment,
- children: widget.isDense
- ? items
- : items.map((Widget item) {
- return SizedBox(height: widget.itemHeight, child: item);
- }).toList(),
- );
- }
- const Icon defaultIcon = Icon(Icons.arrow_drop_down);
- Widget result = DefaultTextStyle(
- style: _enabled
- ? _textStyle!
- : _textStyle!.copyWith(color: Theme.of(context).disabledColor),
- child: widget.customButton ??
- Container(
- decoration: widget.buttonDecoration?.copyWith(
- boxShadow: widget.buttonDecoration!.boxShadow ??
- kElevationToShadow[widget.buttonElevation ?? 0],
- ),
- padding: widget.buttonPadding ??
- padding.resolve(Directionality.of(context)),
- height: widget.buttonHeight ??
- (widget.isDense ? _denseButtonHeight : null),
- width: widget.buttonWidth,
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- mainAxisSize: MainAxisSize.min,
- children: <Widget>[
- if (widget.isExpanded)
- Expanded(child: innerItemsWidget)
- else
- innerItemsWidget,
- IconTheme(
- data: IconThemeData(
- color: _iconColor,
- size: widget.iconSize,
- ),
- child: widget.iconOnClick != null
- ? _isMenuOpen
- ? widget.iconOnClick!
- : widget.icon!
- : widget.icon ?? defaultIcon,
- ),
- ],
- ),
- ),
- );
- if (!DropdownButtonHideUnderline.at(context)) {
- final double bottom = widget.isDense ? 0.0 : 8.0;
- result = Stack(
- children: <Widget>[
- result,
- Positioned(
- left: 0.0,
- right: 0.0,
- bottom: bottom,
- child: widget.underline ??
- Container(
- height: 1.0,
- decoration: const BoxDecoration(
- border: Border(
- bottom: BorderSide(
- color: Color(0xFFBDBDBD),
- width: 0.0,
- ),
- ),
- ),
- ),
- ),
- ],
- );
- }
- final MouseCursor effectiveMouseCursor =
- MaterialStateProperty.resolveAs<MouseCursor>(
- MaterialStateMouseCursor.clickable,
- <MaterialState>{
- if (!_enabled) MaterialState.disabled,
- },
- );
- return Semantics(
- button: true,
- child: Actions(
- actions: _actionMap,
- child: InkWell(
- mouseCursor: effectiveMouseCursor,
- onTap: _enabled && !widget.openWithLongPress ? _handleTap : null,
- onLongPress: _enabled && widget.openWithLongPress ? _handleTap : null,
- canRequestFocus: _enabled,
- focusNode: focusNode,
- autofocus: widget.autofocus,
- focusColor: widget.buttonDecoration?.color ??
- widget.focusColor ??
- Theme.of(context).focusColor,
- enableFeedback: false,
- child: result,
- borderRadius: widget.buttonDecoration?.borderRadius
- ?.resolve(Directionality.of(context)),
- ),
- ),
- );
- }
- }
- /// A [FormField] that contains a [DropdownButton2].
- ///
- /// This is a convenience widget that wraps a [DropdownButton2] widget in a
- /// [FormField].
- ///
- /// A [Form] ancestor is not required. The [Form] simply makes it easier to
- /// save, reset, or validate multiple fields at once. To use without a [Form],
- /// pass a [GlobalKey] to the constructor and use [GlobalKey.currentState] to
- /// save or reset the form field.
- ///
- /// See also:
- ///
- /// * [DropdownButton2], which is the underlying text field without the [Form]
- /// integration.
- class DropdownButtonFormField2<T> extends FormField<T> {
- /// Creates a [DropdownButton2] widget that is a [FormField], wrapped in an
- /// [InputDecorator].
- ///
- /// For a description of the `onSaved`, `validator`, or `autovalidateMode`
- /// parameters, see [FormField]. For the rest (other than [decoration]), see
- /// [DropdownButton2].
- ///
- /// The `items`, `elevation`, `iconSize`, `isDense`, `isExpanded`,
- /// `autofocus`, and `decoration` parameters must not be null.
- DropdownButtonFormField2({
- key,
- required List<DropdownMenuItem<T>>? items,
- DropdownButtonBuilder? selectedItemBuilder,
- T? value,
- Widget? hint,
- Widget? disabledHint,
- this.onChanged,
- int dropdownElevation = 8,
- TextStyle? style,
- Widget? icon,
- Widget? iconOnClick,
- Color? iconDisabledColor,
- Color? iconEnabledColor,
- double iconSize = 24.0,
- bool isDense = true,
- bool isExpanded = false,
- double itemHeight = kMinInteractiveDimension,
- Color? focusColor,
- FocusNode? focusNode,
- bool autofocus = false,
- InputDecoration? decoration,
- onSaved,
- validator,
- AutovalidateMode? autovalidateMode,
- double? dropdownMaxHeight,
- bool? enableFeedback,
- AlignmentGeometry alignment = AlignmentDirectional.centerStart,
- double? buttonHeight,
- double? buttonWidth,
- EdgeInsetsGeometry? buttonPadding,
- BoxDecoration? buttonDecoration,
- int? buttonElevation,
- EdgeInsetsGeometry? itemPadding,
- double? dropdownWidth,
- EdgeInsetsGeometry? dropdownPadding,
- EdgeInsetsGeometry? dropdownScrollPadding,
- BoxDecoration? dropdownDecoration,
- Color? selectedItemHighlightColor,
- Radius? scrollbarRadius,
- double? scrollbarThickness,
- bool? scrollbarAlwaysShow,
- Offset? offset,
- Widget? customButton,
- List<double>? customItemsHeights,
- bool openWithLongPress = false,
- bool dropdownOverButton = false,
- bool dropdownFullScreen = false,
- bool barrierDismissible = true,
- Color? barrierColor,
- String? barrierLabel,
- TextEditingController? searchController,
- Widget? searchInnerWidget,
- _SearchMatchFn? searchMatchFn,
- _OnMenuStateChangeFn? onMenuStateChange,
- }) : assert(
- items == null ||
- items.isEmpty ||
- value == null ||
- items.where((DropdownMenuItem<T> item) {
- return item.value == value;
- }).length ==
- 1,
- "There should be exactly one item with [DropdownButton]'s value: "
- '$value. \n'
- 'Either zero or 2 or more [DropdownMenuItem]s were detected '
- 'with the same value',
- ),
- decoration = decoration ?? InputDecoration(focusColor: focusColor),
- super(
- initialValue: value,
- autovalidateMode: autovalidateMode ?? AutovalidateMode.disabled,
- builder: (FormFieldState<T> field) {
- final _DropdownButtonFormFieldState<T> state =
- field as _DropdownButtonFormFieldState<T>;
- final InputDecoration decorationArg =
- decoration ?? InputDecoration(focusColor: focusColor);
- final InputDecoration effectiveDecoration =
- decorationArg.applyDefaults(
- Theme.of(field.context).inputDecorationTheme,
- );
- final bool showSelectedItem = items != null &&
- items
- .where(
- (DropdownMenuItem<T> item) => item.value == state.value)
- .isNotEmpty;
- bool isHintOrDisabledHintAvailable() {
- final bool isDropdownDisabled =
- onChanged == null || (items == null || items.isEmpty);
- if (isDropdownDisabled) {
- return hint != null || disabledHint != null;
- } else {
- return hint != null;
- }
- }
- final bool isEmpty =
- !showSelectedItem && !isHintOrDisabledHintAvailable();
- bool hasFocus = false;
- // An unFocusable Focus widget so that this widget can detect if its
- // descendants have focus or not.
- return Focus(
- canRequestFocus: false,
- skipTraversal: true,
- child: StatefulBuilder(
- builder: (BuildContext context, StateSetter setState) {
- return InputDecorator(
- decoration: effectiveDecoration.copyWith(
- errorText: field.errorText),
- isEmpty: isEmpty,
- isFocused: hasFocus,
- textAlignVertical: TextAlignVertical.bottom,
- child: DropdownButtonHideUnderline(
- child: DropdownButton2._formField(
- items: items,
- selectedItemBuilder: selectedItemBuilder,
- value: state.value,
- hint: hint,
- disabledHint: disabledHint,
- onChanged: onChanged == null ? null : state.didChange,
- dropdownElevation: dropdownElevation,
- style: style,
- icon: icon,
- iconOnClick: iconOnClick,
- iconDisabledColor: iconDisabledColor,
- iconEnabledColor: iconEnabledColor,
- iconSize: iconSize,
- isDense: isDense,
- isExpanded: isExpanded,
- itemHeight: itemHeight,
- focusColor: focusColor,
- focusNode: focusNode,
- autofocus: autofocus,
- dropdownMaxHeight: dropdownMaxHeight,
- enableFeedback: enableFeedback,
- alignment: alignment,
- buttonHeight: buttonHeight,
- buttonWidth: buttonWidth,
- buttonPadding: buttonPadding,
- buttonDecoration: buttonDecoration,
- buttonElevation: buttonElevation,
- itemPadding: itemPadding,
- dropdownWidth: dropdownWidth,
- dropdownPadding: dropdownPadding,
- dropdownScrollPadding: dropdownScrollPadding,
- dropdownDecoration: dropdownDecoration,
- selectedItemHighlightColor: selectedItemHighlightColor,
- scrollbarRadius: scrollbarRadius,
- scrollbarThickness: scrollbarThickness,
- scrollbarAlwaysShow: scrollbarAlwaysShow,
- offset: offset,
- customButton: customButton,
- customItemsHeights: customItemsHeights,
- openWithLongPress: openWithLongPress,
- dropdownOverButton: dropdownOverButton,
- dropdownFullScreen: dropdownFullScreen,
- onMenuStateChange: onMenuStateChange,
- barrierDismissible: barrierDismissible,
- barrierColor: barrierColor,
- barrierLabel: barrierLabel,
- searchController: searchController,
- searchInnerWidget: searchInnerWidget,
- searchMatchFn: searchMatchFn,
- formFieldCallBack: (isOpen) {
- hasFocus = isOpen;
- setState(() {});
- },
- ),
- ),
- );
- },
- ),
- );
- },
- );
- /// {@macro flutter.material.dropdownButton.onChanged}
- final ValueChanged<T?>? onChanged;
- /// The decoration to show around the dropdown button form field.
- ///
- /// By default, draws a horizontal line under the dropdown button field but
- /// can be configured to show an icon, label, hint text, and error text.
- ///
- /// If not specified, an [InputDecorator] with the `focusColor` set to the
- /// supplied `focusColor` (if any) will be used.
- final InputDecoration decoration;
- @override
- FormFieldState<T> createState() => _DropdownButtonFormFieldState<T>();
- }
- class _DropdownButtonFormFieldState<T> extends FormFieldState<T> {
- @override
- void didChange(T? value) {
- super.didChange(value);
- final DropdownButtonFormField2<T> dropdownButtonFormField =
- widget as DropdownButtonFormField2<T>;
- assert(dropdownButtonFormField.onChanged != null);
- dropdownButtonFormField.onChanged!(value);
- }
- @override
- void didUpdateWidget(DropdownButtonFormField2<T> oldWidget) {
- super.didUpdateWidget(oldWidget);
- if (oldWidget.initialValue != widget.initialValue) {
- setValue(widget.initialValue);
- }
- }
- }
|