dropdown_button2.dart 73 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101
  1. /*
  2. * Created by AHMED ELSAYED on 30 Nov 2021.
  3. * email: ahmedelsaayid@gmail.com
  4. * Edits made on original source code by Flutter.
  5. * Copyright 2014 The Flutter Authors. All rights reserved.
  6. */
  7. import 'dart:math' as math;
  8. import 'package:flutter/foundation.dart';
  9. import 'package:flutter/material.dart';
  10. import 'package:flutter/rendering.dart';
  11. import 'package:flutter/services.dart';
  12. const Duration _kDropdownMenuDuration = Duration(milliseconds: 300);
  13. const double _kMenuItemHeight = kMinInteractiveDimension;
  14. const double _kDenseButtonHeight = 24.0;
  15. const EdgeInsets _kMenuItemPadding = EdgeInsets.symmetric(horizontal: 16.0);
  16. const EdgeInsetsGeometry _kAlignedButtonPadding =
  17. EdgeInsetsDirectional.only(start: 16.0, end: 4.0);
  18. const EdgeInsets _kUnalignedButtonPadding = EdgeInsets.zero;
  19. typedef _OnMenuStateChangeFn = void Function(bool isOpen);
  20. typedef _SearchMatchFn = bool Function(
  21. DropdownMenuItem item,
  22. String searchValue,
  23. );
  24. double clampDouble(double min, double max, double input) {
  25. if (input < min) {
  26. return min;
  27. } else if (input > max) {
  28. return max;
  29. }
  30. return input;
  31. }
  32. _SearchMatchFn _defaultSearchMatchFn = (item, searchValue) =>
  33. item.value.toString().toLowerCase().contains(searchValue.toLowerCase());
  34. class _DropdownMenuPainter extends CustomPainter {
  35. _DropdownMenuPainter({
  36. this.color,
  37. this.elevation,
  38. this.selectedIndex,
  39. required this.resize,
  40. required this.itemHeight,
  41. this.dropdownDecoration,
  42. }) : _painter = dropdownDecoration
  43. ?.copyWith(
  44. color: dropdownDecoration.color ?? color,
  45. boxShadow: dropdownDecoration.boxShadow ??
  46. kElevationToShadow[elevation],
  47. )
  48. .createBoxPainter() ??
  49. BoxDecoration(
  50. // If you add an image here, you must provide a real
  51. // configuration in the paint() function and you must provide some sort
  52. // of onChanged callback here.
  53. color: color,
  54. borderRadius: const BorderRadius.all(Radius.circular(2.0)),
  55. boxShadow: kElevationToShadow[elevation],
  56. ).createBoxPainter(),
  57. super(repaint: resize);
  58. final Color? color;
  59. final int? elevation;
  60. final int? selectedIndex;
  61. final Animation<double> resize;
  62. final double itemHeight;
  63. final BoxDecoration? dropdownDecoration;
  64. final BoxPainter _painter;
  65. @override
  66. void paint(Canvas canvas, Size size) {
  67. final Tween<double> top = Tween<double>(
  68. //Begin at 0.0 instead of selectedItemOffset so that the menu open animation
  69. //always start from top to bottom instead of starting from the selected item
  70. begin: 0.0,
  71. end: 0.0,
  72. );
  73. final Tween<double> bottom = Tween<double>(
  74. begin: clampDouble(top.begin! + itemHeight,
  75. math.min(itemHeight, size.height), size.height),
  76. end: size.height,
  77. );
  78. final Rect rect = Rect.fromLTRB(
  79. 0.0, top.evaluate(resize), size.width, bottom.evaluate(resize));
  80. _painter.paint(canvas, rect.topLeft, ImageConfiguration(size: rect.size));
  81. }
  82. @override
  83. bool shouldRepaint(_DropdownMenuPainter oldPainter) {
  84. return oldPainter.color != color ||
  85. oldPainter.elevation != elevation ||
  86. oldPainter.selectedIndex != selectedIndex ||
  87. oldPainter.dropdownDecoration != dropdownDecoration ||
  88. oldPainter.itemHeight != itemHeight ||
  89. oldPainter.resize != resize;
  90. }
  91. }
  92. // The widget that is the button wrapping the menu items.
  93. class _DropdownMenuItemButton<T> extends StatefulWidget {
  94. const _DropdownMenuItemButton({
  95. key,
  96. this.padding,
  97. required this.route,
  98. required this.buttonRect,
  99. required this.constraints,
  100. required this.itemIndex,
  101. required this.enableFeedback,
  102. this.customItemsHeights,
  103. this.customItemsHeight,
  104. });
  105. final _DropdownRoute<T> route;
  106. final EdgeInsets? padding;
  107. final Rect buttonRect;
  108. final BoxConstraints constraints;
  109. final int itemIndex;
  110. final bool enableFeedback;
  111. final List<double>? customItemsHeights;
  112. final double? customItemsHeight;
  113. @override
  114. _DropdownMenuItemButtonState<T> createState() =>
  115. _DropdownMenuItemButtonState<T>();
  116. }
  117. class _DropdownMenuItemButtonState<T>
  118. extends State<_DropdownMenuItemButton<T>> {
  119. void _handleFocusChange(bool focused) {
  120. final bool inTraditionalMode;
  121. switch (FocusManager.instance.highlightMode) {
  122. case FocusHighlightMode.touch:
  123. inTraditionalMode = false;
  124. break;
  125. case FocusHighlightMode.traditional:
  126. inTraditionalMode = true;
  127. break;
  128. }
  129. if (focused && inTraditionalMode) {
  130. final _MenuLimits menuLimits = widget.route.getMenuLimits(
  131. widget.buttonRect,
  132. widget.constraints.maxHeight,
  133. widget.itemIndex,
  134. );
  135. widget.route.scrollController!.animateTo(
  136. menuLimits.scrollOffset,
  137. curve: Curves.easeInOut,
  138. duration: const Duration(milliseconds: 100),
  139. );
  140. }
  141. }
  142. void _handleOnTap() {
  143. final DropdownMenuItem<T> dropdownMenuItem =
  144. widget.route.items[widget.itemIndex].item!;
  145. dropdownMenuItem.onTap?.call();
  146. Navigator.pop(
  147. context,
  148. _DropdownRouteResult<T>(dropdownMenuItem.value),
  149. );
  150. }
  151. static const Map<ShortcutActivator, Intent> _webShortcuts =
  152. <ShortcutActivator, Intent>{
  153. // On the web, up/down don't change focus, *except* in a <select>
  154. // element, which is what a dropdown emulates.
  155. SingleActivator(LogicalKeyboardKey.arrowDown):
  156. DirectionalFocusIntent(TraversalDirection.down),
  157. SingleActivator(LogicalKeyboardKey.arrowUp):
  158. DirectionalFocusIntent(TraversalDirection.up),
  159. };
  160. @override
  161. Widget build(BuildContext context) {
  162. final DropdownMenuItem<T> dropdownMenuItem =
  163. widget.route.items[widget.itemIndex].item!;
  164. final double unit = 0.5 / (widget.route.items.length + 1.5);
  165. final double start =
  166. clampDouble(0.5 + (widget.itemIndex + 1) * unit, 0.0, 1.0);
  167. final double end = clampDouble(start + 1.5 * unit, 0.0, 1.0);
  168. final CurvedAnimation opacity = CurvedAnimation(
  169. parent: widget.route.animation!, curve: Interval(start, end));
  170. Widget child = Container(
  171. padding: widget.padding,
  172. height: widget.customItemsHeights == null
  173. ? widget.route.itemHeight
  174. : widget.customItemsHeights![widget.itemIndex],
  175. child: widget.route.items[widget.itemIndex],
  176. );
  177. // An [InkWell] is added to the item only if it is enabled
  178. // isNoSelectedItem to avoid first item highlight when no item selected
  179. if (dropdownMenuItem.enabled) {
  180. final _isSelectedItem = !widget.route.isNoSelectedItem &&
  181. widget.itemIndex == widget.route.selectedIndex;
  182. child = InkWell(
  183. autofocus: _isSelectedItem,
  184. enableFeedback: widget.enableFeedback,
  185. onTap: _handleOnTap,
  186. onFocusChange: _handleFocusChange,
  187. child: Container(
  188. color:
  189. _isSelectedItem ? widget.route.selectedItemHighlightColor : null,
  190. child: child,
  191. ),
  192. );
  193. }
  194. child = FadeTransition(opacity: opacity, child: child);
  195. if (kIsWeb && dropdownMenuItem.enabled) {
  196. child = Shortcuts(
  197. shortcuts: _webShortcuts,
  198. child: child,
  199. );
  200. }
  201. return child;
  202. }
  203. }
  204. class _DropdownMenu<T> extends StatefulWidget {
  205. const _DropdownMenu({
  206. key,
  207. this.padding,
  208. required this.route,
  209. required this.buttonRect,
  210. required this.constraints,
  211. required this.enableFeedback,
  212. required this.itemHeight,
  213. this.dropdownDecoration,
  214. this.dropdownPadding,
  215. this.dropdownScrollPadding,
  216. this.scrollbarRadius,
  217. this.scrollbarThickness,
  218. this.scrollbarAlwaysShow,
  219. required this.offset,
  220. this.customItemsHeights,
  221. this.searchController,
  222. this.searchInnerWidget,
  223. this.searchMatchFn,
  224. });
  225. final _DropdownRoute<T> route;
  226. final EdgeInsets? padding;
  227. final Rect buttonRect;
  228. final BoxConstraints constraints;
  229. final bool enableFeedback;
  230. final double itemHeight;
  231. final BoxDecoration? dropdownDecoration;
  232. final EdgeInsetsGeometry? dropdownPadding;
  233. final EdgeInsetsGeometry? dropdownScrollPadding;
  234. final Radius? scrollbarRadius;
  235. final double? scrollbarThickness;
  236. final bool? scrollbarAlwaysShow;
  237. final Offset offset;
  238. final List<double>? customItemsHeights;
  239. final TextEditingController? searchController;
  240. final Widget? searchInnerWidget;
  241. final _SearchMatchFn? searchMatchFn;
  242. @override
  243. _DropdownMenuState<T> createState() => _DropdownMenuState<T>();
  244. }
  245. class _DropdownMenuState<T> extends State<_DropdownMenu<T>> {
  246. late CurvedAnimation _fadeOpacity;
  247. late CurvedAnimation _resize;
  248. late List<Widget> _children;
  249. late _SearchMatchFn _searchMatchFn;
  250. @override
  251. void initState() {
  252. super.initState();
  253. // We need to hold these animations as state because of their curve
  254. // direction. When the route's animation reverses, if we were to recreate
  255. // the CurvedAnimation objects in build, we'd lose
  256. // CurvedAnimation._curveDirection.
  257. _fadeOpacity = CurvedAnimation(
  258. parent: widget.route.animation!,
  259. curve: const Interval(0.0, 0.25),
  260. reverseCurve: const Interval(0.75, 1.0),
  261. );
  262. _resize = CurvedAnimation(
  263. parent: widget.route.animation!,
  264. curve: const Interval(0.25, 0.5),
  265. reverseCurve: const Threshold(0.0),
  266. );
  267. //If searchController is null, then it'll perform as a normal dropdown
  268. //and search functions will not be executed.
  269. if (widget.searchController == null) {
  270. _children = <Widget>[
  271. for (int index = 0; index < widget.route.items.length; ++index)
  272. _DropdownMenuItemButton<T>(
  273. route: widget.route,
  274. padding: widget.padding,
  275. buttonRect: widget.buttonRect,
  276. constraints: widget.constraints,
  277. itemIndex: index,
  278. enableFeedback: widget.enableFeedback,
  279. customItemsHeights: widget.customItemsHeights,
  280. ),
  281. ];
  282. } else {
  283. _searchMatchFn = widget.searchMatchFn ?? _defaultSearchMatchFn;
  284. _children = _getSearchItems();
  285. // Add listener to searchController (if it's used) to update the shown items.
  286. widget.searchController?.addListener(_updateSearchItems);
  287. }
  288. }
  289. void _updateSearchItems() {
  290. _children = _getSearchItems();
  291. setState(() {});
  292. }
  293. List<Widget> _getSearchItems() {
  294. return <Widget>[
  295. for (int index = 0; index < widget.route.items.length; ++index)
  296. if (_searchMatchFn(
  297. widget.route.items[index].item!, widget.searchController!.text))
  298. _DropdownMenuItemButton<T>(
  299. route: widget.route,
  300. padding: widget.padding,
  301. buttonRect: widget.buttonRect,
  302. constraints: widget.constraints,
  303. itemIndex: index,
  304. enableFeedback: widget.enableFeedback,
  305. customItemsHeights: widget.customItemsHeights,
  306. ),
  307. ];
  308. }
  309. @override
  310. void dispose() {
  311. _fadeOpacity.dispose();
  312. _resize.dispose();
  313. widget.searchController?.removeListener(_updateSearchItems);
  314. super.dispose();
  315. }
  316. @override
  317. Widget build(BuildContext context) {
  318. // The menu is shown in three stages (unit timing in brackets):
  319. // [0s - 0.25s] - Fade in a rect-sized menu container with the selected item.
  320. // [0.25s - 0.5s] - Grow the otherwise empty menu container from the center
  321. // until it's big enough for as many items as we're going to show.
  322. // [0.5s - 1.0s] Fade in the remaining visible items from top to bottom.
  323. //
  324. // When the menu is dismissed we just fade the entire thing out
  325. // in the first 0.25s.
  326. assert(debugCheckHasMaterialLocalizations(context));
  327. final MaterialLocalizations localizations =
  328. MaterialLocalizations.of(context);
  329. final _DropdownRoute<T> route = widget.route;
  330. return FadeTransition(
  331. opacity: _fadeOpacity,
  332. child: CustomPaint(
  333. painter: _DropdownMenuPainter(
  334. color: Theme.of(context).canvasColor,
  335. elevation: route.elevation,
  336. selectedIndex: route.selectedIndex,
  337. resize: _resize,
  338. itemHeight: widget.itemHeight,
  339. dropdownDecoration: widget.dropdownDecoration,
  340. ),
  341. child: Semantics(
  342. scopesRoute: true,
  343. namesRoute: true,
  344. explicitChildNodes: true,
  345. label: localizations.popupMenuLabel,
  346. child: ClipRRect(
  347. //Prevent scrollbar, ripple effect & items from going beyond border boundaries when scrolling.
  348. clipBehavior: widget.dropdownDecoration?.borderRadius != null
  349. ? Clip.antiAlias
  350. : Clip.none,
  351. borderRadius: widget.dropdownDecoration?.borderRadius
  352. ?.resolve(Directionality.of(context)) ??
  353. BorderRadius.zero,
  354. child: Material(
  355. type: MaterialType.transparency,
  356. textStyle: route.style,
  357. child: Column(
  358. mainAxisSize: MainAxisSize.min,
  359. children: [
  360. if (widget.searchInnerWidget != null)
  361. widget.searchInnerWidget!,
  362. Flexible(
  363. child: Padding(
  364. padding: widget.dropdownScrollPadding ?? EdgeInsets.zero,
  365. child: ScrollConfiguration(
  366. // Dropdown menus should never overscroll or display an overscroll indicator.
  367. // Scrollbars are built-in below.
  368. // Platform must use Theme and ScrollPhysics must be Clamping.
  369. behavior: ScrollConfiguration.of(context).copyWith(
  370. scrollbars: false,
  371. overscroll: false,
  372. physics: const ClampingScrollPhysics(),
  373. platform: Theme.of(context).platform,
  374. ),
  375. child: PrimaryScrollController(
  376. controller: widget.route.scrollController!,
  377. child: Scrollbar(
  378. radius: widget.scrollbarRadius,
  379. thickness: widget.scrollbarThickness,
  380. isAlwaysShown: widget.scrollbarAlwaysShow,
  381. child: ListView(
  382. // Ensure this always inherits the PrimaryScrollController
  383. primary: true,
  384. padding: widget.dropdownPadding ??
  385. kMaterialListPadding,
  386. shrinkWrap: true,
  387. children: _children,
  388. ),
  389. ),
  390. ),
  391. ),
  392. ),
  393. ),
  394. ],
  395. ),
  396. ),
  397. ),
  398. ),
  399. ),
  400. );
  401. }
  402. }
  403. class _DropdownMenuRouteLayout<T> extends SingleChildLayoutDelegate {
  404. _DropdownMenuRouteLayout({
  405. required this.buttonRect,
  406. required this.route,
  407. required this.textDirection,
  408. required this.itemHeight,
  409. this.itemWidth,
  410. required this.offset,
  411. });
  412. final Rect buttonRect;
  413. final _DropdownRoute<T> route;
  414. final TextDirection? textDirection;
  415. final double itemHeight;
  416. final double? itemWidth;
  417. final Offset offset;
  418. @override
  419. BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
  420. // The maximum height of a simple menu should be one or more rows less than
  421. // the view height. This ensures a tappable area outside of the simple menu
  422. // with which to dismiss the menu.
  423. // -- https://material.io/design/components/menus.html#usage
  424. double maxHeight = math.max(0.0, constraints.maxHeight - 2 * itemHeight);
  425. if (route.menuMaxHeight != null && route.menuMaxHeight! <= maxHeight) {
  426. maxHeight = route.menuMaxHeight!;
  427. }
  428. // The width of a menu should be at most the view width. This ensures that
  429. // the menu does not extend past the left and right edges of the screen.
  430. final double width =
  431. itemWidth ?? math.min(constraints.maxWidth, buttonRect.width);
  432. return BoxConstraints(
  433. minWidth: width,
  434. maxWidth: width,
  435. maxHeight: maxHeight,
  436. );
  437. }
  438. @override
  439. Offset getPositionForChild(Size size, Size childSize) {
  440. final _MenuLimits menuLimits =
  441. route.getMenuLimits(buttonRect, size.height, route.selectedIndex);
  442. assert(() {
  443. final Rect container = Offset.zero & size;
  444. if (container.intersect(buttonRect) == buttonRect) {
  445. // If the button was entirely on-screen, then verify
  446. // that the menu is also on-screen.
  447. // If the button was a bit off-screen, then, oh well.
  448. assert(menuLimits.top >= 0.0);
  449. assert(menuLimits.top + menuLimits.height <= size.height);
  450. }
  451. return true;
  452. }());
  453. assert(textDirection != null);
  454. final double left;
  455. switch (textDirection!) {
  456. case TextDirection.rtl:
  457. left = clampDouble(buttonRect.right + offset.dx, 0.0, size.width) -
  458. childSize.width;
  459. break;
  460. case TextDirection.ltr:
  461. left = clampDouble(
  462. buttonRect.left + offset.dx, 0.0, size.width - childSize.width);
  463. break;
  464. }
  465. return Offset(left, menuLimits.top);
  466. }
  467. @override
  468. bool shouldRelayout(_DropdownMenuRouteLayout<T> oldDelegate) {
  469. return buttonRect != oldDelegate.buttonRect ||
  470. textDirection != oldDelegate.textDirection;
  471. }
  472. }
  473. // We box the return value so that the return value can be null. Otherwise,
  474. // canceling the route (which returns null) would get confused with actually
  475. // returning a real null value.
  476. @immutable
  477. class _DropdownRouteResult<T> {
  478. const _DropdownRouteResult(this.result);
  479. final T? result;
  480. @override
  481. bool operator ==(Object other) {
  482. return other is _DropdownRouteResult<T> && other.result == result;
  483. }
  484. @override
  485. int get hashCode => result.hashCode;
  486. }
  487. class _MenuLimits {
  488. const _MenuLimits(this.top, this.bottom, this.height, this.scrollOffset);
  489. final double top;
  490. final double bottom;
  491. final double height;
  492. final double scrollOffset;
  493. }
  494. class _DropdownRoute<T> extends PopupRoute<_DropdownRouteResult<T>> {
  495. _DropdownRoute({
  496. required this.items,
  497. required this.padding,
  498. required this.buttonRect,
  499. required this.selectedIndex,
  500. required this.isNoSelectedItem,
  501. this.selectedItemHighlightColor,
  502. this.elevation = 8,
  503. required this.capturedThemes,
  504. required this.style,
  505. required this.barrierDismissible,
  506. this.barrierColor,
  507. this.barrierLabel,
  508. required this.enableFeedback,
  509. required this.itemHeight,
  510. this.itemWidth,
  511. this.menuMaxHeight,
  512. this.dropdownDecoration,
  513. this.dropdownPadding,
  514. this.dropdownScrollPadding,
  515. this.scrollbarRadius,
  516. this.scrollbarThickness,
  517. this.scrollbarAlwaysShow,
  518. required this.offset,
  519. required this.showAboveButton,
  520. this.customItemsHeights,
  521. this.searchController,
  522. this.searchInnerWidget,
  523. this.searchMatchFn,
  524. }) : itemHeights =
  525. customItemsHeights ?? List<double>.filled(items.length, itemHeight);
  526. final List<_MenuItem<T>> items;
  527. final EdgeInsetsGeometry padding;
  528. final ValueNotifier<Rect?> buttonRect;
  529. final int selectedIndex;
  530. final bool isNoSelectedItem;
  531. final Color? selectedItemHighlightColor;
  532. final int elevation;
  533. final CapturedThemes capturedThemes;
  534. final TextStyle style;
  535. final bool enableFeedback;
  536. final double itemHeight;
  537. final double? itemWidth;
  538. final double? menuMaxHeight;
  539. final BoxDecoration? dropdownDecoration;
  540. final EdgeInsetsGeometry? dropdownPadding;
  541. final EdgeInsetsGeometry? dropdownScrollPadding;
  542. final Radius? scrollbarRadius;
  543. final double? scrollbarThickness;
  544. final bool? scrollbarAlwaysShow;
  545. final Offset offset;
  546. final bool showAboveButton;
  547. final List<double>? customItemsHeights;
  548. final TextEditingController? searchController;
  549. final Widget? searchInnerWidget;
  550. final _SearchMatchFn? searchMatchFn;
  551. final List<double> itemHeights;
  552. ScrollController? scrollController;
  553. @override
  554. Duration get transitionDuration => _kDropdownMenuDuration;
  555. @override
  556. final bool barrierDismissible;
  557. @override
  558. final Color? barrierColor;
  559. @override
  560. final String? barrierLabel;
  561. @override
  562. Widget buildPage(BuildContext context, Animation<double> animation,
  563. Animation<double> secondaryAnimation) {
  564. return LayoutBuilder(
  565. builder: (BuildContext context, BoxConstraints constraints) {
  566. return ValueListenableBuilder<Rect?>(
  567. valueListenable: buttonRect,
  568. builder: (context, rect, _) {
  569. return _DropdownRoutePage<T>(
  570. route: this,
  571. constraints: constraints,
  572. items: items,
  573. padding: padding,
  574. buttonRect: rect!,
  575. selectedIndex: selectedIndex,
  576. elevation: elevation,
  577. capturedThemes: capturedThemes,
  578. style: style,
  579. enableFeedback: enableFeedback,
  580. dropdownDecoration: dropdownDecoration,
  581. dropdownPadding: dropdownPadding,
  582. dropdownScrollPadding: dropdownScrollPadding,
  583. menuMaxHeight: menuMaxHeight,
  584. itemHeight: itemHeight,
  585. itemWidth: itemWidth,
  586. scrollbarRadius: scrollbarRadius,
  587. scrollbarThickness: scrollbarThickness,
  588. scrollbarAlwaysShow: scrollbarAlwaysShow,
  589. offset: offset,
  590. customItemsHeights: customItemsHeights,
  591. searchController: searchController,
  592. searchInnerWidget: searchInnerWidget,
  593. searchMatchFn: searchMatchFn,
  594. );
  595. },
  596. );
  597. },
  598. );
  599. }
  600. void _dismiss() {
  601. if (isActive) {
  602. navigator?.removeRoute(this);
  603. }
  604. }
  605. double getItemOffset(int index, double paddingTop) {
  606. double offset = paddingTop;
  607. if (items.isNotEmpty && index > 0) {
  608. assert(items.length == itemHeights.length);
  609. offset += itemHeights
  610. .sublist(0, index)
  611. .reduce((double total, double height) => total + height);
  612. }
  613. return offset;
  614. }
  615. // Returns the vertical extent of the menu and the initial scrollOffset
  616. // for the ListView that contains the menu items. The vertical center of the
  617. // selected item is aligned with the button's vertical center, as far as
  618. // that's possible given availableHeight.
  619. _MenuLimits getMenuLimits(
  620. Rect buttonRect, double availableHeight, int index) {
  621. double computedMaxHeight = availableHeight - 2.0 * itemHeight;
  622. if (menuMaxHeight != null) {
  623. computedMaxHeight = math.min(computedMaxHeight, menuMaxHeight!);
  624. }
  625. final double buttonTop = buttonRect.top;
  626. final double buttonBottom = math.min(buttonRect.bottom, availableHeight);
  627. final double selectedItemOffset = getItemOffset(
  628. index,
  629. dropdownPadding != null
  630. ? dropdownPadding!.resolve(null).top
  631. : kMaterialListPadding.top,
  632. );
  633. // If the button is placed on the bottom or top of the screen, its top or
  634. // bottom may be less than [_kMenuItemHeight] from the edge of the screen.
  635. // In this case, we want to change the menu limits to align with the top
  636. // or bottom edge of the button.
  637. final double topLimit = math.min(itemHeight, buttonTop);
  638. final double bottomLimit = math.max(availableHeight, buttonBottom);
  639. double menuTop =
  640. showAboveButton ? buttonTop - offset.dy : buttonBottom - offset.dy;
  641. double preferredMenuHeight =
  642. dropdownPadding?.vertical ?? kMaterialListPadding.vertical;
  643. if (items.isNotEmpty) {
  644. preferredMenuHeight +=
  645. itemHeights.reduce((double total, double height) => total + height);
  646. }
  647. // If there are too many elements in the menu, we need to shrink it down
  648. // so it is at most the computedMaxHeight.
  649. final double menuHeight = math.min(computedMaxHeight, preferredMenuHeight);
  650. double menuBottom = menuTop + menuHeight;
  651. // If the computed top or bottom of the menu are outside of the range
  652. // specified, we need to bring them into range. If the item height is larger
  653. // than the button height and the button is at the very bottom or top of the
  654. // screen, the menu will be aligned with the bottom or top of the button
  655. // respectively.
  656. if (menuTop < topLimit) {
  657. menuTop = math.min(buttonTop, topLimit);
  658. menuBottom = menuTop + menuHeight;
  659. }
  660. if (menuBottom > bottomLimit) {
  661. menuBottom = math.max(buttonBottom, bottomLimit);
  662. menuTop = menuBottom - menuHeight;
  663. }
  664. if (menuBottom - itemHeights[selectedIndex] / 2.0 <
  665. buttonBottom - buttonRect.height / 2.0) {
  666. menuBottom = math.max(buttonBottom, bottomLimit);
  667. menuTop = menuBottom - menuHeight;
  668. }
  669. double scrollOffset = 0;
  670. // If all of the menu items will not fit within availableHeight then
  671. // compute the scroll offset that will line the selected menu item up
  672. // with the select item. This is only done when the menu is first
  673. // shown - subsequently we leave the scroll offset where the user left
  674. // it. This scroll offset is only accurate for fixed height menu items
  675. // (the default).
  676. if (preferredMenuHeight > computedMaxHeight) {
  677. // The offset should be zero if the selected item is in view at the beginning
  678. // of the menu. Otherwise, the scroll offset should center the item if possible.
  679. scrollOffset = math.max(
  680. 0.0,
  681. selectedItemOffset -
  682. (menuHeight / 2) +
  683. (itemHeights[selectedIndex] / 2));
  684. // If the selected item's scroll offset is greater than the maximum scroll offset,
  685. // set it instead to the maximum allowed scroll offset.
  686. scrollOffset = math.min(scrollOffset, preferredMenuHeight - menuHeight);
  687. }
  688. assert((menuBottom - menuTop - menuHeight).abs() < precisionErrorTolerance);
  689. return _MenuLimits(menuTop, menuBottom, menuHeight, scrollOffset);
  690. }
  691. }
  692. class _DropdownRoutePage<T> extends StatelessWidget {
  693. const _DropdownRoutePage({
  694. key,
  695. required this.route,
  696. required this.constraints,
  697. this.items,
  698. required this.padding,
  699. required this.buttonRect,
  700. required this.selectedIndex,
  701. this.elevation = 8,
  702. required this.capturedThemes,
  703. this.style,
  704. required this.enableFeedback,
  705. this.dropdownDecoration,
  706. this.dropdownPadding,
  707. this.dropdownScrollPadding,
  708. this.menuMaxHeight,
  709. required this.itemHeight,
  710. this.itemWidth,
  711. this.scrollbarRadius,
  712. this.scrollbarThickness,
  713. this.scrollbarAlwaysShow,
  714. required this.offset,
  715. this.customItemsHeights,
  716. this.searchController,
  717. this.searchInnerWidget,
  718. this.searchMatchFn,
  719. });
  720. final _DropdownRoute<T> route;
  721. final BoxConstraints constraints;
  722. final List<_MenuItem<T>>? items;
  723. final EdgeInsetsGeometry padding;
  724. final Rect buttonRect;
  725. final int selectedIndex;
  726. final int elevation;
  727. final CapturedThemes capturedThemes;
  728. final TextStyle? style;
  729. final bool enableFeedback;
  730. final BoxDecoration? dropdownDecoration;
  731. final EdgeInsetsGeometry? dropdownPadding;
  732. final EdgeInsetsGeometry? dropdownScrollPadding;
  733. final double? menuMaxHeight;
  734. final double itemHeight;
  735. final double? itemWidth;
  736. final Radius? scrollbarRadius;
  737. final double? scrollbarThickness;
  738. final bool? scrollbarAlwaysShow;
  739. final Offset offset;
  740. final List<double>? customItemsHeights;
  741. final TextEditingController? searchController;
  742. final Widget? searchInnerWidget;
  743. final _SearchMatchFn? searchMatchFn;
  744. @override
  745. Widget build(BuildContext context) {
  746. assert(debugCheckHasDirectionality(context));
  747. // Computing the initialScrollOffset now, before the items have been laid
  748. // out. This only works if the item heights are effectively fixed, i.e. either
  749. // DropdownButton.itemHeight is specified or DropdownButton.itemHeight is null
  750. // and all of the items' intrinsic heights are less than kMinInteractiveDimension.
  751. // Otherwise the initialScrollOffset is just a rough approximation based on
  752. // treating the items as if their heights were all equal to kMinInteractiveDimension.
  753. if (route.scrollController == null) {
  754. final _MenuLimits menuLimits =
  755. route.getMenuLimits(buttonRect, constraints.maxHeight, selectedIndex);
  756. route.scrollController =
  757. ScrollController(initialScrollOffset: menuLimits.scrollOffset);
  758. }
  759. final TextDirection? textDirection = Directionality.maybeOf(context);
  760. final Widget menu = _DropdownMenu<T>(
  761. route: route,
  762. padding: padding.resolve(textDirection),
  763. buttonRect: buttonRect,
  764. constraints: constraints,
  765. enableFeedback: enableFeedback,
  766. itemHeight: itemHeight,
  767. dropdownDecoration: dropdownDecoration,
  768. dropdownPadding: dropdownPadding,
  769. dropdownScrollPadding: dropdownScrollPadding,
  770. scrollbarRadius: scrollbarRadius,
  771. scrollbarThickness: scrollbarThickness,
  772. scrollbarAlwaysShow: scrollbarAlwaysShow,
  773. offset: offset,
  774. customItemsHeights: customItemsHeights,
  775. searchController: searchController,
  776. searchInnerWidget: searchInnerWidget,
  777. searchMatchFn: searchMatchFn,
  778. );
  779. return MediaQuery.removePadding(
  780. context: context,
  781. removeTop: true,
  782. removeBottom: true,
  783. removeLeft: true,
  784. removeRight: true,
  785. child: Builder(
  786. builder: (BuildContext context) {
  787. return CustomSingleChildLayout(
  788. delegate: _DropdownMenuRouteLayout<T>(
  789. buttonRect: buttonRect,
  790. route: route,
  791. textDirection: textDirection,
  792. itemHeight: itemHeight,
  793. itemWidth: itemWidth,
  794. offset: offset,
  795. ),
  796. child: capturedThemes.wrap(menu),
  797. );
  798. },
  799. ),
  800. );
  801. }
  802. }
  803. // This widget enables _DropdownRoute to look up the sizes of
  804. // each menu item. These sizes are used to compute the offset of the selected
  805. // item so that _DropdownRoutePage can align the vertical center of the
  806. // selected item lines up with the vertical center of the dropdown button,
  807. // as closely as possible.
  808. class _MenuItem<T> extends SingleChildRenderObjectWidget {
  809. const _MenuItem({
  810. key,
  811. required this.onLayout,
  812. required this.item,
  813. }) : super(child: item);
  814. final ValueChanged<Size> onLayout;
  815. final DropdownMenuItem<T>? item;
  816. @override
  817. RenderObject createRenderObject(BuildContext context) {
  818. return _RenderMenuItem(onLayout);
  819. }
  820. @override
  821. void updateRenderObject(
  822. BuildContext context, covariant _RenderMenuItem renderObject) {
  823. renderObject.onLayout = onLayout;
  824. }
  825. }
  826. class _RenderMenuItem extends RenderProxyBox {
  827. _RenderMenuItem(this.onLayout, [RenderBox? child]) : super(child);
  828. ValueChanged<Size> onLayout;
  829. @override
  830. void performLayout() {
  831. super.performLayout();
  832. onLayout(size);
  833. }
  834. }
  835. // The container widget for a menu item created by a [DropdownButton]. It
  836. // provides the default configuration for [DropdownMenuItem]s, as well as a
  837. // [DropdownButton]'s hint and disabledHint widgets.
  838. class _DropdownMenuItemContainer extends StatelessWidget {
  839. /// Creates an item for a dropdown menu.
  840. ///
  841. /// The [child] argument is required.
  842. const _DropdownMenuItemContainer({
  843. Key? key,
  844. this.alignment = AlignmentDirectional.centerStart,
  845. required this.child,
  846. }) : super(key: key);
  847. /// The widget below this widget in the tree.
  848. ///
  849. /// Typically a [Text] widget.
  850. final Widget child;
  851. /// Defines how the item is positioned within the container.
  852. ///
  853. /// This property must not be null. It defaults to [AlignmentDirectional.centerStart].
  854. ///
  855. /// See also:
  856. ///
  857. /// * [Alignment], a class with convenient constants typically used to
  858. /// specify an [AlignmentGeometry].
  859. /// * [AlignmentDirectional], like [Alignment] for specifying alignments
  860. /// relative to text direction.
  861. final AlignmentGeometry alignment;
  862. @override
  863. Widget build(BuildContext context) {
  864. return Container(
  865. constraints: const BoxConstraints(minHeight: _kMenuItemHeight),
  866. alignment: alignment,
  867. child: child,
  868. );
  869. }
  870. }
  871. /// A Material Design button for selecting from a list of items.
  872. ///
  873. /// A dropdown button lets the user select from a number of items. The button
  874. /// shows the currently selected item as well as an arrow that opens a menu for
  875. /// selecting another item.
  876. ///
  877. /// One ancestor must be a [Material] widget and typically this is
  878. /// provided by the app's [Scaffold].
  879. ///
  880. /// The type `T` is the type of the [value] that each dropdown item represents.
  881. /// All the entries in a given menu must represent values with consistent types.
  882. /// Typically, an enum is used. Each [DropdownMenuItem] in [items] must be
  883. /// specialized with that same type argument.
  884. ///
  885. /// The [onChanged] callback should update a state variable that defines the
  886. /// dropdown's value. It should also call [State.setState] to rebuild the
  887. /// dropdown with the new value.
  888. ///
  889. /// {@tool dartpad}
  890. /// This sample shows a `DropdownButton` with a large arrow icon,
  891. /// purple text style, and bold purple underline, whose value is one of "One",
  892. /// "Two", "Free", or "Four".
  893. ///
  894. /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/dropdown_button.png)
  895. ///
  896. /// ** See code in examples/api/lib/material/dropdown/dropdown_button.0.dart **
  897. /// {@end-tool}
  898. ///
  899. /// If the [onChanged] callback is null or the list of [items] is null
  900. /// then the dropdown button will be disabled, i.e. its arrow will be
  901. /// displayed in grey and it will not respond to input. A disabled button
  902. /// will display the [disabledHint] widget if it is non-null. However, if
  903. /// [disabledHint] is null and [hint] is non-null, the [hint] widget will
  904. /// instead be displayed.
  905. ///
  906. /// Requires one of its ancestors to be a [Material] widget.
  907. ///
  908. /// See also:
  909. ///
  910. /// * [DropdownButtonFormField2], which integrates with the [Form] widget.
  911. /// * [DropdownMenuItem], the class used to represent the [items].
  912. /// * [DropdownButtonHideUnderline], which prevents its descendant dropdown buttons
  913. /// from displaying their underlines.
  914. /// * [ElevatedButton], [TextButton], ordinary buttons that trigger a single action.
  915. /// * <https://material.io/design/components/menus.html#dropdown-menu>
  916. class DropdownButton2<T> extends StatefulWidget {
  917. /// Creates a DropdownButton2
  918. /// It's customizable DropdownButton with steady dropdown menu and many other features.
  919. ///
  920. /// The [items] must have distinct values. If [value] isn't null then it
  921. /// must be equal to one of the [DropdownMenuItem] values. If [items] or
  922. /// [onChanged] is null, the button will be disabled, the down arrow
  923. /// will be greyed out.
  924. ///
  925. /// If [value] is null and the button is enabled, [hint] will be displayed
  926. /// if it is non-null.
  927. ///
  928. /// If [value] is null and the button is disabled, [disabledHint] will be displayed
  929. /// if it is non-null. If [disabledHint] is null, then [hint] will be displayed
  930. /// if it is non-null.
  931. DropdownButton2({
  932. key,
  933. required this.items,
  934. this.selectedItemBuilder,
  935. this.value,
  936. this.hint,
  937. this.disabledHint,
  938. this.onChanged,
  939. this.onMenuStateChange,
  940. this.dropdownElevation = 8,
  941. this.style,
  942. this.underline,
  943. this.icon,
  944. this.iconOnClick,
  945. this.iconDisabledColor,
  946. this.iconEnabledColor,
  947. this.iconSize = 24.0,
  948. this.isDense = false,
  949. this.isExpanded = false,
  950. this.itemHeight = kMinInteractiveDimension,
  951. this.focusColor,
  952. this.focusNode,
  953. this.autofocus = false,
  954. this.dropdownMaxHeight,
  955. this.enableFeedback,
  956. this.alignment = AlignmentDirectional.centerStart,
  957. this.buttonHeight,
  958. this.buttonWidth,
  959. this.buttonPadding,
  960. this.buttonDecoration,
  961. this.buttonElevation,
  962. this.itemPadding,
  963. this.dropdownWidth,
  964. this.dropdownPadding,
  965. this.dropdownScrollPadding,
  966. this.dropdownDecoration,
  967. this.selectedItemHighlightColor,
  968. this.scrollbarRadius,
  969. this.scrollbarThickness,
  970. this.scrollbarAlwaysShow,
  971. this.offset,
  972. this.customButton,
  973. this.customItemsHeights,
  974. this.openWithLongPress = false,
  975. this.dropdownOverButton = false,
  976. this.dropdownFullScreen = false,
  977. this.barrierDismissible = true,
  978. this.barrierColor,
  979. this.barrierLabel,
  980. this.searchController,
  981. this.searchInnerWidget,
  982. this.searchMatchFn,
  983. // When adding new arguments, consider adding similar arguments to
  984. // DropdownButtonFormField.
  985. }) : assert(
  986. items == null ||
  987. items.isEmpty ||
  988. value == null ||
  989. items.where((DropdownMenuItem<T> item) {
  990. return item.value == value;
  991. }).length ==
  992. 1,
  993. "There should be exactly one item with [DropdownButton]'s value: "
  994. '$value. \n'
  995. 'Either zero or 2 or more [DropdownMenuItem]s were detected '
  996. 'with the same value',
  997. ),
  998. assert(
  999. customItemsHeights == null ||
  1000. items == null ||
  1001. items.isEmpty ||
  1002. customItemsHeights.length == items.length,
  1003. "customItemsHeights list should have the same length of items list",
  1004. ),
  1005. formFieldCallBack = null;
  1006. DropdownButton2._formField({
  1007. keyy,
  1008. required this.items,
  1009. this.selectedItemBuilder,
  1010. this.value,
  1011. this.hint,
  1012. this.disabledHint,
  1013. required this.onChanged,
  1014. this.onMenuStateChange,
  1015. this.dropdownElevation = 8,
  1016. this.style,
  1017. this.underline,
  1018. this.icon,
  1019. this.iconOnClick,
  1020. this.iconDisabledColor,
  1021. this.iconEnabledColor,
  1022. this.iconSize = 24.0,
  1023. this.isDense = false,
  1024. this.isExpanded = false,
  1025. this.itemHeight = kMinInteractiveDimension,
  1026. this.focusColor,
  1027. this.focusNode,
  1028. this.autofocus = false,
  1029. this.dropdownMaxHeight,
  1030. this.enableFeedback,
  1031. this.alignment = AlignmentDirectional.centerStart,
  1032. this.buttonHeight,
  1033. this.buttonWidth,
  1034. this.buttonPadding,
  1035. this.buttonDecoration,
  1036. this.buttonElevation,
  1037. this.itemPadding,
  1038. this.dropdownWidth,
  1039. this.dropdownPadding,
  1040. this.dropdownScrollPadding,
  1041. this.dropdownDecoration,
  1042. this.selectedItemHighlightColor,
  1043. this.scrollbarRadius,
  1044. this.scrollbarThickness,
  1045. this.scrollbarAlwaysShow,
  1046. this.offset,
  1047. this.customButton,
  1048. this.customItemsHeights,
  1049. this.openWithLongPress = false,
  1050. this.dropdownOverButton = false,
  1051. this.dropdownFullScreen = false,
  1052. this.barrierDismissible = true,
  1053. this.barrierColor,
  1054. this.barrierLabel,
  1055. this.searchController,
  1056. this.searchInnerWidget,
  1057. this.searchMatchFn,
  1058. this.formFieldCallBack,
  1059. }) : assert(
  1060. items == null ||
  1061. items.isEmpty ||
  1062. value == null ||
  1063. items.where((DropdownMenuItem<T> item) {
  1064. return item.value == value;
  1065. }).length ==
  1066. 1,
  1067. "There should be exactly one item with [DropdownButtonFormField]'s value: "
  1068. '$value. \n'
  1069. 'Either zero or 2 or more [DropdownMenuItem]s were detected '
  1070. 'with the same value',
  1071. );
  1072. // Parameters added By Me
  1073. /// The height of the button.
  1074. final double? buttonHeight;
  1075. /// The width of the button
  1076. final double? buttonWidth;
  1077. /// The inner padding of the Button
  1078. final EdgeInsetsGeometry? buttonPadding;
  1079. /// The decoration of the Button
  1080. final BoxDecoration? buttonDecoration;
  1081. /// The elevation of the Button
  1082. final int? buttonElevation;
  1083. /// The padding of menu items
  1084. final EdgeInsetsGeometry? itemPadding;
  1085. /// The width of the dropdown menu
  1086. final double? dropdownWidth;
  1087. /// The inner padding of the dropdown menu
  1088. final EdgeInsetsGeometry? dropdownPadding;
  1089. /// The inner padding of the dropdown menu including the scrollbar
  1090. final EdgeInsetsGeometry? dropdownScrollPadding;
  1091. /// The decoration of the dropdown menu
  1092. final BoxDecoration? dropdownDecoration;
  1093. /// The highlight color of the current selected item
  1094. final Color? selectedItemHighlightColor;
  1095. /// The radius of the scrollbar's corners
  1096. final Radius? scrollbarRadius;
  1097. /// The thickness of the scrollbar
  1098. final double? scrollbarThickness;
  1099. /// Always show the scrollbar even when a scroll is not underway
  1100. final bool? scrollbarAlwaysShow;
  1101. /// Changes the position of the dropdown menu
  1102. final Offset? offset;
  1103. /// Uses custom widget like icon,image,etc.. instead of the default button
  1104. final Widget? customButton;
  1105. /// Uses different predefined heights for the menu items (useful for adding dividers)
  1106. final List<double>? customItemsHeights;
  1107. /// Opens the dropdown menu on long-pressing instead of tapping
  1108. final bool openWithLongPress;
  1109. /// Opens the dropdown menu over the button instead of below it
  1110. final bool dropdownOverButton;
  1111. /// Opens the dropdown menu in fullscreen mode (Above AppBar & TabBar)
  1112. final bool dropdownFullScreen;
  1113. /// Shows different icon when dropdown menu open
  1114. final Widget? iconOnClick;
  1115. /// Called when the dropdown menu is opened or closed.
  1116. final _OnMenuStateChangeFn? onMenuStateChange;
  1117. /// Whether you can dismiss this route by tapping the modal barrier.
  1118. final bool barrierDismissible;
  1119. /// The color to use for the modal barrier. If this is null, the barrier will
  1120. /// be transparent.
  1121. final Color? barrierColor;
  1122. /// The semantic label used for a dismissible barrier.
  1123. ///
  1124. /// If the barrier is dismissible, this label will be read out if
  1125. /// accessibility tools (like VoiceOver on iOS) focus on the barrier.
  1126. final String? barrierLabel;
  1127. /// The TextEditingController used for searchable dropdowns. If this is null,
  1128. /// then it'll perform as a normal dropdown without searching feature.
  1129. final TextEditingController? searchController;
  1130. /// The widget to use for searchable dropdowns, such as search bar.
  1131. /// It will be shown at the top of the dropdown menu.
  1132. final Widget? searchInnerWidget;
  1133. /// The match function used for searchable dropdowns. If this is null,
  1134. /// then _defaultSearchMatchFn will be used.
  1135. ///
  1136. /// _defaultSearchMatchFn = (item, searchValue) =>
  1137. /// item.value.toString().toLowerCase().contains(searchValue.toLowerCase());
  1138. final _SearchMatchFn? searchMatchFn;
  1139. /// The list of items the user can select.
  1140. ///
  1141. /// If the [onChanged] callback is null or the list of items is null
  1142. /// then the dropdown button will be disabled, i.e. its arrow will be
  1143. /// displayed in grey and it will not respond to input.
  1144. final List<DropdownMenuItem<T>>? items;
  1145. /// The value of the currently selected [DropdownMenuItem].
  1146. ///
  1147. /// If [value] is null and the button is enabled, [hint] will be displayed
  1148. /// if it is non-null.
  1149. ///
  1150. /// If [value] is null and the button is disabled, [disabledHint] will be displayed
  1151. /// if it is non-null. If [disabledHint] is null, then [hint] will be displayed
  1152. /// if it is non-null.
  1153. final T? value;
  1154. /// A placeholder widget that is displayed by the dropdown button.
  1155. ///
  1156. /// If [value] is null and the dropdown is enabled ([items] and [onChanged] are non-null),
  1157. /// this widget is displayed as a placeholder for the dropdown button's value.
  1158. ///
  1159. /// If [value] is null and the dropdown is disabled and [disabledHint] is null,
  1160. /// this widget is used as the placeholder.
  1161. final Widget? hint;
  1162. /// A preferred placeholder widget that is displayed when the dropdown is disabled.
  1163. ///
  1164. /// If [value] is null, the dropdown is disabled ([items] or [onChanged] is null),
  1165. /// this widget is displayed as a placeholder for the dropdown button's value.
  1166. final Widget? disabledHint;
  1167. /// {@template flutter.material.dropdownButton.onChanged}
  1168. /// Called when the user selects an item.
  1169. ///
  1170. /// If the [onChanged] callback is null or the list of [DropdownButton2.items]
  1171. /// is null then the dropdown button will be disabled, i.e. its arrow will be
  1172. /// displayed in grey and it will not respond to input. A disabled button
  1173. /// will display the [DropdownButton2.disabledHint] widget if it is non-null.
  1174. /// If [DropdownButton2.disabledHint] is also null but [DropdownButton2.hint] is
  1175. /// non-null, [DropdownButton2.hint] will instead be displayed.
  1176. /// {@endtemplate}
  1177. final ValueChanged<T?>? onChanged;
  1178. /// A builder to customize the dropdown buttons corresponding to the
  1179. /// [DropdownMenuItem]s in [items].
  1180. ///
  1181. /// When a [DropdownMenuItem] is selected, the widget that will be displayed
  1182. /// from the list corresponds to the [DropdownMenuItem] of the same index
  1183. /// in [items].
  1184. ///
  1185. /// {@tool dartpad}
  1186. /// This sample shows a `DropdownButton` with a button with [Text] that
  1187. /// corresponds to but is unique from [DropdownMenuItem].
  1188. ///
  1189. /// ** See code in examples/api/lib/material/dropdown/dropdown_button.selected_item_builder.0.dart **
  1190. /// {@end-tool}
  1191. ///
  1192. /// If this callback is null, the [DropdownMenuItem] from [items]
  1193. /// that matches [value] will be displayed.
  1194. final DropdownButtonBuilder? selectedItemBuilder;
  1195. /// The z-coordinate at which to place the menu when open.
  1196. ///
  1197. /// The following elevations have defined shadows: 1, 2, 3, 4, 6, 8, 9, 12,
  1198. /// 16, and 24. See [kElevationToShadow].
  1199. ///
  1200. /// Defaults to 8, the appropriate elevation for dropdown buttons.
  1201. final int dropdownElevation;
  1202. /// The text style to use for text in the dropdown button and the dropdown
  1203. /// menu that appears when you tap the button.
  1204. ///
  1205. /// To use a separate text style for selected item when it's displayed within
  1206. /// the dropdown button, consider using [selectedItemBuilder].
  1207. ///
  1208. /// {@tool dartpad}
  1209. /// This sample shows a `DropdownButton` with a dropdown button text style
  1210. /// that is different than its menu items.
  1211. ///
  1212. /// ** See code in examples/api/lib/material/dropdown/dropdown_button.style.0.dart **
  1213. /// {@end-tool}
  1214. ///
  1215. /// Defaults to the [TextTheme.titleMedium] value of the current
  1216. /// [ThemeData.textTheme] of the current [Theme].
  1217. final TextStyle? style;
  1218. /// The widget to use for drawing the drop-down button's underline.
  1219. ///
  1220. /// Defaults to a 0.0 width bottom border with color 0xFFBDBDBD.
  1221. final Widget? underline;
  1222. /// The widget to use for the drop-down button's icon.
  1223. ///
  1224. /// Defaults to an [Icon] with the [Icons.arrow_drop_down] glyph.
  1225. final Widget? icon;
  1226. /// The color of any [Icon] descendant of [icon] if this button is disabled,
  1227. /// i.e. if [onChanged] is null.
  1228. ///
  1229. /// Defaults to [MaterialColor.shade400] of [Colors.grey] when the theme's
  1230. /// [ThemeData.brightness] is [Brightness.light] and to
  1231. /// [Colors.white10] when it is [Brightness.dark]
  1232. final Color? iconDisabledColor;
  1233. /// The color of any [Icon] descendant of [icon] if this button is enabled,
  1234. /// i.e. if [onChanged] is defined.
  1235. ///
  1236. /// Defaults to [MaterialColor.shade700] of [Colors.grey] when the theme's
  1237. /// [ThemeData.brightness] is [Brightness.light] and to
  1238. /// [Colors.white70] when it is [Brightness.dark]
  1239. final Color? iconEnabledColor;
  1240. /// The size to use for the drop-down button's icon.
  1241. ///
  1242. /// Defaults to 24.0.
  1243. final double iconSize;
  1244. /// Reduce the button's height.
  1245. ///
  1246. /// By default this button's height is the same as its menu items' heights.
  1247. /// If isDense is true, the button's height is reduced by about half. This
  1248. /// can be useful when the button is embedded in a container that adds
  1249. /// its own decorations, like [InputDecorator].
  1250. final bool isDense;
  1251. /// Set the dropdown's inner contents to horizontally fill its parent.
  1252. ///
  1253. /// By default this button's inner width is the minimum size of its contents.
  1254. /// If [isExpanded] is true, the inner width is expanded to fill its
  1255. /// surrounding container.
  1256. final bool isExpanded;
  1257. /// The default value is [kMinInteractiveDimension]
  1258. final double itemHeight;
  1259. /// The color for the button's [Material] when it has the input focus.
  1260. final Color? focusColor;
  1261. /// {@macro flutter.widgets.Focus.focusNode}
  1262. final FocusNode? focusNode;
  1263. /// {@macro flutter.widgets.Focus.autofocus}
  1264. final bool autofocus;
  1265. /// The maximum height of the menu.
  1266. ///
  1267. /// The maximum height of the menu must be at least one row shorter than
  1268. /// the height of the app's view. This ensures that a tappable area
  1269. /// outside of the simple menu is present so the user can dismiss the menu.
  1270. ///
  1271. /// If this property is set above the maximum allowable height threshold
  1272. /// mentioned above, then the menu defaults to being padded at the top
  1273. /// and bottom of the menu by at one menu item's height.
  1274. final double? dropdownMaxHeight;
  1275. /// Whether detected gestures should provide acoustic and/or haptic feedback.
  1276. ///
  1277. /// For example, on Android a tap will produce a clicking sound and a
  1278. /// long-press will produce a short vibration, when feedback is enabled.
  1279. ///
  1280. /// By default, platform-specific feedback is enabled.
  1281. ///
  1282. /// See also:
  1283. ///
  1284. /// * [Feedback] for providing platform-specific feedback to certain actions.
  1285. final bool? enableFeedback;
  1286. /// Defines how the hint or the selected item is positioned within the button.
  1287. ///
  1288. /// This property must not be null. It defaults to [AlignmentDirectional.centerStart].
  1289. ///
  1290. /// See also:
  1291. ///
  1292. /// * [Alignment], a class with convenient constants typically used to
  1293. /// specify an [AlignmentGeometry].
  1294. /// * [AlignmentDirectional], like [Alignment] for specifying alignments
  1295. /// relative to text direction.
  1296. final AlignmentGeometry alignment;
  1297. /// Called when the dropdown menu is opened or closed in case of using
  1298. /// DropdownButtonFormField2 to update the FormField's focus.
  1299. final _OnMenuStateChangeFn? formFieldCallBack;
  1300. @override
  1301. State<DropdownButton2<T>> createState() => DropdownButton2State<T>();
  1302. }
  1303. class DropdownButton2State<T> extends State<DropdownButton2<T>>
  1304. with WidgetsBindingObserver {
  1305. int? _selectedIndex;
  1306. _DropdownRoute<T>? _dropdownRoute;
  1307. Orientation? _lastOrientation;
  1308. FocusNode? _internalNode;
  1309. FocusNode? get focusNode => widget.focusNode ?? _internalNode;
  1310. bool _hasPrimaryFocus = false;
  1311. late Map<Type, Action<Intent>> _actionMap;
  1312. bool _isMenuOpen = false;
  1313. // Using ValueNotifier for the Rect of DropdownButton so the dropdown menu listen and
  1314. // update its position if DropdownButton's position has changed, as when keyboard open.
  1315. final _rect = ValueNotifier<Rect?>(null);
  1316. // Only used if needed to create _internalNode.
  1317. FocusNode _createFocusNode() {
  1318. return FocusNode(debugLabel: '${widget.runtimeType}');
  1319. }
  1320. @override
  1321. void initState() {
  1322. super.initState();
  1323. WidgetsBinding.instance?.addObserver(this);
  1324. _updateSelectedIndex();
  1325. if (widget.focusNode == null) {
  1326. _internalNode ??= _createFocusNode();
  1327. }
  1328. _actionMap = <Type, Action<Intent>>{
  1329. ActivateIntent: CallbackAction<ActivateIntent>(
  1330. onInvoke: (ActivateIntent intent) => _handleTap(),
  1331. ),
  1332. ButtonActivateIntent: CallbackAction<ButtonActivateIntent>(
  1333. onInvoke: (ButtonActivateIntent intent) => _handleTap(),
  1334. ),
  1335. };
  1336. focusNode!.addListener(_handleFocusChanged);
  1337. }
  1338. @override
  1339. void dispose() {
  1340. WidgetsBinding.instance?.removeObserver(this);
  1341. _removeDropdownRoute();
  1342. focusNode!.removeListener(_handleFocusChanged);
  1343. _internalNode?.dispose();
  1344. super.dispose();
  1345. }
  1346. void _removeDropdownRoute() {
  1347. _dropdownRoute?._dismiss();
  1348. _dropdownRoute = null;
  1349. _lastOrientation = null;
  1350. }
  1351. void _handleFocusChanged() {
  1352. if (_hasPrimaryFocus != focusNode!.hasPrimaryFocus) {
  1353. setState(() {
  1354. _hasPrimaryFocus = focusNode!.hasPrimaryFocus;
  1355. });
  1356. }
  1357. }
  1358. @override
  1359. void didUpdateWidget(DropdownButton2<T> oldWidget) {
  1360. super.didUpdateWidget(oldWidget);
  1361. if (widget.focusNode != oldWidget.focusNode) {
  1362. oldWidget.focusNode?.removeListener(_handleFocusChanged);
  1363. if (widget.focusNode == null) {
  1364. _internalNode ??= _createFocusNode();
  1365. }
  1366. _hasPrimaryFocus = focusNode!.hasPrimaryFocus;
  1367. focusNode!.addListener(_handleFocusChanged);
  1368. }
  1369. _updateSelectedIndex();
  1370. }
  1371. void _updateSelectedIndex() {
  1372. if (widget.items == null ||
  1373. widget.items!.isEmpty ||
  1374. (widget.value == null &&
  1375. widget.items!
  1376. .where((DropdownMenuItem<T> item) =>
  1377. item.enabled && item.value == widget.value)
  1378. .isEmpty)) {
  1379. _selectedIndex = null;
  1380. return;
  1381. }
  1382. assert(widget.items!
  1383. .where((DropdownMenuItem<T> item) => item.value == widget.value)
  1384. .length ==
  1385. 1);
  1386. for (int itemIndex = 0; itemIndex < widget.items!.length; itemIndex++) {
  1387. if (widget.items![itemIndex].value == widget.value) {
  1388. _selectedIndex = itemIndex;
  1389. return;
  1390. }
  1391. }
  1392. }
  1393. @override
  1394. void didChangeMetrics() {
  1395. //This fix the bug of calling didChangeMetrics() on iOS when app starts
  1396. if (_rect.value == null) return;
  1397. final _newRect = _getRect();
  1398. //This avoid unnecessary rebuilds if _rect position hasn't changed
  1399. if (_rect.value!.top == _newRect.top) return;
  1400. _rect.value = _newRect;
  1401. }
  1402. TextStyle? get _textStyle =>
  1403. widget.style ?? Theme.of(context).textTheme.subtitle1;
  1404. Rect _getRect() {
  1405. final TextDirection? textDirection = Directionality.maybeOf(context);
  1406. const EdgeInsetsGeometry menuMargin = EdgeInsets.zero;
  1407. final NavigatorState navigator =
  1408. Navigator.of(context, rootNavigator: widget.dropdownFullScreen);
  1409. final RenderBox itemBox = context.findRenderObject()! as RenderBox;
  1410. final Rect itemRect = itemBox.localToGlobal(Offset.zero,
  1411. ancestor: navigator.context.findRenderObject()) &
  1412. itemBox.size;
  1413. return menuMargin.resolve(textDirection).inflateRect(itemRect);
  1414. }
  1415. void _handleTap() {
  1416. final TextDirection? textDirection = Directionality.maybeOf(context);
  1417. final List<_MenuItem<T>> menuItems = <_MenuItem<T>>[
  1418. for (int index = 0; index < widget.items!.length; index += 1)
  1419. _MenuItem<T>(
  1420. item: widget.items![index],
  1421. onLayout: (Size size) {
  1422. // If [_dropdownRoute] is null and onLayout is called, this means
  1423. // that performLayout was called on a _DropdownRoute that has not
  1424. // left the widget tree but is already on its way out.
  1425. //
  1426. // Since onLayout is used primarily to collect the desired heights
  1427. // of each menu item before laying them out, not having the _DropdownRoute
  1428. // collect each item's height to lay out is fine since the route is
  1429. // already on its way out.
  1430. if (_dropdownRoute == null) return;
  1431. _dropdownRoute!.itemHeights[index] = size.height;
  1432. },
  1433. ),
  1434. ];
  1435. final NavigatorState navigator =
  1436. Navigator.of(context, rootNavigator: widget.dropdownFullScreen);
  1437. assert(_dropdownRoute == null);
  1438. _rect.value = _getRect();
  1439. _dropdownRoute = _DropdownRoute<T>(
  1440. items: menuItems,
  1441. buttonRect: _rect,
  1442. padding: widget.itemPadding ?? _kMenuItemPadding.resolve(textDirection),
  1443. selectedIndex: _selectedIndex ?? 0,
  1444. isNoSelectedItem: _selectedIndex == null,
  1445. selectedItemHighlightColor: widget.selectedItemHighlightColor,
  1446. elevation: widget.dropdownElevation,
  1447. capturedThemes:
  1448. InheritedTheme.capture(from: context, to: navigator.context),
  1449. style: _textStyle!,
  1450. barrierDismissible: widget.barrierDismissible,
  1451. barrierColor: widget.barrierColor,
  1452. barrierLabel: widget.barrierLabel ??
  1453. MaterialLocalizations.of(context).modalBarrierDismissLabel,
  1454. enableFeedback: widget.enableFeedback ?? true,
  1455. itemHeight: widget.itemHeight,
  1456. itemWidth: widget.dropdownWidth,
  1457. menuMaxHeight: widget.dropdownMaxHeight,
  1458. dropdownDecoration: widget.dropdownDecoration,
  1459. dropdownPadding: widget.dropdownPadding,
  1460. dropdownScrollPadding: widget.dropdownScrollPadding,
  1461. scrollbarRadius: widget.scrollbarRadius,
  1462. scrollbarThickness: widget.scrollbarThickness,
  1463. scrollbarAlwaysShow: widget.scrollbarAlwaysShow,
  1464. offset: widget.offset ?? const Offset(0, 0),
  1465. showAboveButton: widget.dropdownOverButton,
  1466. customItemsHeights: widget.customItemsHeights,
  1467. searchController: widget.searchController,
  1468. searchInnerWidget: widget.searchInnerWidget,
  1469. searchMatchFn: widget.searchMatchFn,
  1470. );
  1471. _isMenuOpen = true;
  1472. focusNode?.requestFocus();
  1473. navigator
  1474. .push(_dropdownRoute!)
  1475. .then<void>((_DropdownRouteResult<T>? newValue) {
  1476. _removeDropdownRoute();
  1477. _isMenuOpen = false;
  1478. widget.onMenuStateChange?.call(false);
  1479. widget.formFieldCallBack?.call(false);
  1480. if (!mounted || newValue == null) return;
  1481. widget.onChanged?.call(newValue.result);
  1482. });
  1483. widget.onMenuStateChange?.call(true);
  1484. widget.formFieldCallBack?.call(true);
  1485. }
  1486. // This expose the _handleTap() to Allow opening the button programmatically using GlobalKey.
  1487. // Also, DropdownButton2State should be public as we need typed access to it through key.
  1488. void callTap() => _handleTap();
  1489. // When isDense is true, reduce the height of this button from _kMenuItemHeight to
  1490. // _kDenseButtonHeight, but don't make it smaller than the text that it contains.
  1491. // Similarly, we don't reduce the height of the button so much that its icon
  1492. // would be clipped.
  1493. double get _denseButtonHeight {
  1494. final double textScaleFactor = MediaQuery.of(context).textScaleFactor;
  1495. final double fontSize = _textStyle!.fontSize ??
  1496. Theme.of(context).textTheme.subtitle1!.fontSize!;
  1497. final double scaledFontSize = textScaleFactor * fontSize;
  1498. return math.max(
  1499. scaledFontSize, math.max(widget.iconSize, _kDenseButtonHeight));
  1500. }
  1501. Color get _iconColor {
  1502. // These colors are not defined in the Material Design spec.
  1503. if (_enabled) {
  1504. if (widget.iconEnabledColor != null) return widget.iconEnabledColor!;
  1505. switch (Theme.of(context).brightness) {
  1506. case Brightness.light:
  1507. return Colors.grey.shade700;
  1508. case Brightness.dark:
  1509. return Colors.white70;
  1510. }
  1511. } else {
  1512. if (widget.iconDisabledColor != null) return widget.iconDisabledColor!;
  1513. switch (Theme.of(context).brightness) {
  1514. case Brightness.light:
  1515. return Colors.grey.shade400;
  1516. case Brightness.dark:
  1517. return Colors.white10;
  1518. }
  1519. }
  1520. }
  1521. bool get _enabled =>
  1522. widget.items != null &&
  1523. widget.items!.isNotEmpty &&
  1524. widget.onChanged != null;
  1525. Orientation _getOrientation(BuildContext context) {
  1526. Orientation? result = MediaQuery.maybeOf(context)?.orientation;
  1527. if (result == null) {
  1528. // If there's no MediaQuery, then use the window aspect to determine
  1529. // orientation.
  1530. final Size size =
  1531. WidgetsBinding.instance?.window.physicalSize ?? const Size(0, 0);
  1532. result = size.width > size.height
  1533. ? Orientation.landscape
  1534. : Orientation.portrait;
  1535. }
  1536. return result;
  1537. }
  1538. @override
  1539. Widget build(BuildContext context) {
  1540. assert(debugCheckHasMaterial(context));
  1541. assert(debugCheckHasMaterialLocalizations(context));
  1542. final Orientation newOrientation = _getOrientation(context);
  1543. _lastOrientation ??= newOrientation;
  1544. if (newOrientation != _lastOrientation) {
  1545. _removeDropdownRoute();
  1546. _lastOrientation = newOrientation;
  1547. }
  1548. // The width of the button and the menu are defined by the widest
  1549. // item and the width of the hint.
  1550. // We should explicitly type the items list to be a list of <Widget>,
  1551. // otherwise, no explicit type adding items maybe trigger a crash/failure
  1552. // when hint and selectedItemBuilder are provided.
  1553. final List<Widget> items = widget.selectedItemBuilder == null
  1554. ? (widget.items != null ? List<Widget>.of(widget.items!) : <Widget>[])
  1555. : List<Widget>.of(widget.selectedItemBuilder!(context));
  1556. int? hintIndex;
  1557. if (widget.hint != null || (!_enabled && widget.disabledHint != null)) {
  1558. final Widget displayedHint =
  1559. _enabled ? widget.hint! : widget.disabledHint ?? widget.hint!;
  1560. hintIndex = items.length;
  1561. items.add(DefaultTextStyle(
  1562. style: _textStyle!.copyWith(color: Theme.of(context).hintColor),
  1563. child: IgnorePointer(
  1564. ignoringSemantics: false,
  1565. child: _DropdownMenuItemContainer(
  1566. alignment: widget.alignment,
  1567. child: displayedHint,
  1568. ),
  1569. ),
  1570. ));
  1571. }
  1572. final EdgeInsetsGeometry padding = ButtonTheme.of(context).alignedDropdown
  1573. ? _kAlignedButtonPadding
  1574. : _kUnalignedButtonPadding;
  1575. // If value is null (then _selectedIndex is null) then we
  1576. // display the hint or nothing at all.
  1577. final Widget innerItemsWidget;
  1578. if (items.isEmpty) {
  1579. innerItemsWidget = const SizedBox.shrink();
  1580. } else {
  1581. innerItemsWidget = IndexedStack(
  1582. index: _selectedIndex ?? hintIndex,
  1583. alignment: widget.alignment,
  1584. children: widget.isDense
  1585. ? items
  1586. : items.map((Widget item) {
  1587. return SizedBox(height: widget.itemHeight, child: item);
  1588. }).toList(),
  1589. );
  1590. }
  1591. const Icon defaultIcon = Icon(Icons.arrow_drop_down);
  1592. Widget result = DefaultTextStyle(
  1593. style: _enabled
  1594. ? _textStyle!
  1595. : _textStyle!.copyWith(color: Theme.of(context).disabledColor),
  1596. child: widget.customButton ??
  1597. Container(
  1598. decoration: widget.buttonDecoration?.copyWith(
  1599. boxShadow: widget.buttonDecoration!.boxShadow ??
  1600. kElevationToShadow[widget.buttonElevation ?? 0],
  1601. ),
  1602. padding: widget.buttonPadding ??
  1603. padding.resolve(Directionality.of(context)),
  1604. height: widget.buttonHeight ??
  1605. (widget.isDense ? _denseButtonHeight : null),
  1606. width: widget.buttonWidth,
  1607. child: Row(
  1608. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  1609. mainAxisSize: MainAxisSize.min,
  1610. children: <Widget>[
  1611. if (widget.isExpanded)
  1612. Expanded(child: innerItemsWidget)
  1613. else
  1614. innerItemsWidget,
  1615. IconTheme(
  1616. data: IconThemeData(
  1617. color: _iconColor,
  1618. size: widget.iconSize,
  1619. ),
  1620. child: widget.iconOnClick != null
  1621. ? _isMenuOpen
  1622. ? widget.iconOnClick!
  1623. : widget.icon!
  1624. : widget.icon ?? defaultIcon,
  1625. ),
  1626. ],
  1627. ),
  1628. ),
  1629. );
  1630. if (!DropdownButtonHideUnderline.at(context)) {
  1631. final double bottom = widget.isDense ? 0.0 : 8.0;
  1632. result = Stack(
  1633. children: <Widget>[
  1634. result,
  1635. Positioned(
  1636. left: 0.0,
  1637. right: 0.0,
  1638. bottom: bottom,
  1639. child: widget.underline ??
  1640. Container(
  1641. height: 1.0,
  1642. decoration: const BoxDecoration(
  1643. border: Border(
  1644. bottom: BorderSide(
  1645. color: Color(0xFFBDBDBD),
  1646. width: 0.0,
  1647. ),
  1648. ),
  1649. ),
  1650. ),
  1651. ),
  1652. ],
  1653. );
  1654. }
  1655. final MouseCursor effectiveMouseCursor =
  1656. MaterialStateProperty.resolveAs<MouseCursor>(
  1657. MaterialStateMouseCursor.clickable,
  1658. <MaterialState>{
  1659. if (!_enabled) MaterialState.disabled,
  1660. },
  1661. );
  1662. return Semantics(
  1663. button: true,
  1664. child: Actions(
  1665. actions: _actionMap,
  1666. child: InkWell(
  1667. mouseCursor: effectiveMouseCursor,
  1668. onTap: _enabled && !widget.openWithLongPress ? _handleTap : null,
  1669. onLongPress: _enabled && widget.openWithLongPress ? _handleTap : null,
  1670. canRequestFocus: _enabled,
  1671. focusNode: focusNode,
  1672. autofocus: widget.autofocus,
  1673. focusColor: widget.buttonDecoration?.color ??
  1674. widget.focusColor ??
  1675. Theme.of(context).focusColor,
  1676. enableFeedback: false,
  1677. child: result,
  1678. borderRadius: widget.buttonDecoration?.borderRadius
  1679. ?.resolve(Directionality.of(context)),
  1680. ),
  1681. ),
  1682. );
  1683. }
  1684. }
  1685. /// A [FormField] that contains a [DropdownButton2].
  1686. ///
  1687. /// This is a convenience widget that wraps a [DropdownButton2] widget in a
  1688. /// [FormField].
  1689. ///
  1690. /// A [Form] ancestor is not required. The [Form] simply makes it easier to
  1691. /// save, reset, or validate multiple fields at once. To use without a [Form],
  1692. /// pass a [GlobalKey] to the constructor and use [GlobalKey.currentState] to
  1693. /// save or reset the form field.
  1694. ///
  1695. /// See also:
  1696. ///
  1697. /// * [DropdownButton2], which is the underlying text field without the [Form]
  1698. /// integration.
  1699. class DropdownButtonFormField2<T> extends FormField<T> {
  1700. /// Creates a [DropdownButton2] widget that is a [FormField], wrapped in an
  1701. /// [InputDecorator].
  1702. ///
  1703. /// For a description of the `onSaved`, `validator`, or `autovalidateMode`
  1704. /// parameters, see [FormField]. For the rest (other than [decoration]), see
  1705. /// [DropdownButton2].
  1706. ///
  1707. /// The `items`, `elevation`, `iconSize`, `isDense`, `isExpanded`,
  1708. /// `autofocus`, and `decoration` parameters must not be null.
  1709. DropdownButtonFormField2({
  1710. key,
  1711. required List<DropdownMenuItem<T>>? items,
  1712. DropdownButtonBuilder? selectedItemBuilder,
  1713. T? value,
  1714. Widget? hint,
  1715. Widget? disabledHint,
  1716. this.onChanged,
  1717. int dropdownElevation = 8,
  1718. TextStyle? style,
  1719. Widget? icon,
  1720. Widget? iconOnClick,
  1721. Color? iconDisabledColor,
  1722. Color? iconEnabledColor,
  1723. double iconSize = 24.0,
  1724. bool isDense = true,
  1725. bool isExpanded = false,
  1726. double itemHeight = kMinInteractiveDimension,
  1727. Color? focusColor,
  1728. FocusNode? focusNode,
  1729. bool autofocus = false,
  1730. InputDecoration? decoration,
  1731. onSaved,
  1732. validator,
  1733. AutovalidateMode? autovalidateMode,
  1734. double? dropdownMaxHeight,
  1735. bool? enableFeedback,
  1736. AlignmentGeometry alignment = AlignmentDirectional.centerStart,
  1737. double? buttonHeight,
  1738. double? buttonWidth,
  1739. EdgeInsetsGeometry? buttonPadding,
  1740. BoxDecoration? buttonDecoration,
  1741. int? buttonElevation,
  1742. EdgeInsetsGeometry? itemPadding,
  1743. double? dropdownWidth,
  1744. EdgeInsetsGeometry? dropdownPadding,
  1745. EdgeInsetsGeometry? dropdownScrollPadding,
  1746. BoxDecoration? dropdownDecoration,
  1747. Color? selectedItemHighlightColor,
  1748. Radius? scrollbarRadius,
  1749. double? scrollbarThickness,
  1750. bool? scrollbarAlwaysShow,
  1751. Offset? offset,
  1752. Widget? customButton,
  1753. List<double>? customItemsHeights,
  1754. bool openWithLongPress = false,
  1755. bool dropdownOverButton = false,
  1756. bool dropdownFullScreen = false,
  1757. bool barrierDismissible = true,
  1758. Color? barrierColor,
  1759. String? barrierLabel,
  1760. TextEditingController? searchController,
  1761. Widget? searchInnerWidget,
  1762. _SearchMatchFn? searchMatchFn,
  1763. _OnMenuStateChangeFn? onMenuStateChange,
  1764. }) : assert(
  1765. items == null ||
  1766. items.isEmpty ||
  1767. value == null ||
  1768. items.where((DropdownMenuItem<T> item) {
  1769. return item.value == value;
  1770. }).length ==
  1771. 1,
  1772. "There should be exactly one item with [DropdownButton]'s value: "
  1773. '$value. \n'
  1774. 'Either zero or 2 or more [DropdownMenuItem]s were detected '
  1775. 'with the same value',
  1776. ),
  1777. decoration = decoration ?? InputDecoration(focusColor: focusColor),
  1778. super(
  1779. initialValue: value,
  1780. autovalidateMode: autovalidateMode ?? AutovalidateMode.disabled,
  1781. builder: (FormFieldState<T> field) {
  1782. final _DropdownButtonFormFieldState<T> state =
  1783. field as _DropdownButtonFormFieldState<T>;
  1784. final InputDecoration decorationArg =
  1785. decoration ?? InputDecoration(focusColor: focusColor);
  1786. final InputDecoration effectiveDecoration =
  1787. decorationArg.applyDefaults(
  1788. Theme.of(field.context).inputDecorationTheme,
  1789. );
  1790. final bool showSelectedItem = items != null &&
  1791. items
  1792. .where(
  1793. (DropdownMenuItem<T> item) => item.value == state.value)
  1794. .isNotEmpty;
  1795. bool isHintOrDisabledHintAvailable() {
  1796. final bool isDropdownDisabled =
  1797. onChanged == null || (items == null || items.isEmpty);
  1798. if (isDropdownDisabled) {
  1799. return hint != null || disabledHint != null;
  1800. } else {
  1801. return hint != null;
  1802. }
  1803. }
  1804. final bool isEmpty =
  1805. !showSelectedItem && !isHintOrDisabledHintAvailable();
  1806. bool hasFocus = false;
  1807. // An unFocusable Focus widget so that this widget can detect if its
  1808. // descendants have focus or not.
  1809. return Focus(
  1810. canRequestFocus: false,
  1811. skipTraversal: true,
  1812. child: StatefulBuilder(
  1813. builder: (BuildContext context, StateSetter setState) {
  1814. return InputDecorator(
  1815. decoration: effectiveDecoration.copyWith(
  1816. errorText: field.errorText),
  1817. isEmpty: isEmpty,
  1818. isFocused: hasFocus,
  1819. textAlignVertical: TextAlignVertical.bottom,
  1820. child: DropdownButtonHideUnderline(
  1821. child: DropdownButton2._formField(
  1822. items: items,
  1823. selectedItemBuilder: selectedItemBuilder,
  1824. value: state.value,
  1825. hint: hint,
  1826. disabledHint: disabledHint,
  1827. onChanged: onChanged == null ? null : state.didChange,
  1828. dropdownElevation: dropdownElevation,
  1829. style: style,
  1830. icon: icon,
  1831. iconOnClick: iconOnClick,
  1832. iconDisabledColor: iconDisabledColor,
  1833. iconEnabledColor: iconEnabledColor,
  1834. iconSize: iconSize,
  1835. isDense: isDense,
  1836. isExpanded: isExpanded,
  1837. itemHeight: itemHeight,
  1838. focusColor: focusColor,
  1839. focusNode: focusNode,
  1840. autofocus: autofocus,
  1841. dropdownMaxHeight: dropdownMaxHeight,
  1842. enableFeedback: enableFeedback,
  1843. alignment: alignment,
  1844. buttonHeight: buttonHeight,
  1845. buttonWidth: buttonWidth,
  1846. buttonPadding: buttonPadding,
  1847. buttonDecoration: buttonDecoration,
  1848. buttonElevation: buttonElevation,
  1849. itemPadding: itemPadding,
  1850. dropdownWidth: dropdownWidth,
  1851. dropdownPadding: dropdownPadding,
  1852. dropdownScrollPadding: dropdownScrollPadding,
  1853. dropdownDecoration: dropdownDecoration,
  1854. selectedItemHighlightColor: selectedItemHighlightColor,
  1855. scrollbarRadius: scrollbarRadius,
  1856. scrollbarThickness: scrollbarThickness,
  1857. scrollbarAlwaysShow: scrollbarAlwaysShow,
  1858. offset: offset,
  1859. customButton: customButton,
  1860. customItemsHeights: customItemsHeights,
  1861. openWithLongPress: openWithLongPress,
  1862. dropdownOverButton: dropdownOverButton,
  1863. dropdownFullScreen: dropdownFullScreen,
  1864. onMenuStateChange: onMenuStateChange,
  1865. barrierDismissible: barrierDismissible,
  1866. barrierColor: barrierColor,
  1867. barrierLabel: barrierLabel,
  1868. searchController: searchController,
  1869. searchInnerWidget: searchInnerWidget,
  1870. searchMatchFn: searchMatchFn,
  1871. formFieldCallBack: (isOpen) {
  1872. hasFocus = isOpen;
  1873. setState(() {});
  1874. },
  1875. ),
  1876. ),
  1877. );
  1878. },
  1879. ),
  1880. );
  1881. },
  1882. );
  1883. /// {@macro flutter.material.dropdownButton.onChanged}
  1884. final ValueChanged<T?>? onChanged;
  1885. /// The decoration to show around the dropdown button form field.
  1886. ///
  1887. /// By default, draws a horizontal line under the dropdown button field but
  1888. /// can be configured to show an icon, label, hint text, and error text.
  1889. ///
  1890. /// If not specified, an [InputDecorator] with the `focusColor` set to the
  1891. /// supplied `focusColor` (if any) will be used.
  1892. final InputDecoration decoration;
  1893. @override
  1894. FormFieldState<T> createState() => _DropdownButtonFormFieldState<T>();
  1895. }
  1896. class _DropdownButtonFormFieldState<T> extends FormFieldState<T> {
  1897. @override
  1898. void didChange(T? value) {
  1899. super.didChange(value);
  1900. final DropdownButtonFormField2<T> dropdownButtonFormField =
  1901. widget as DropdownButtonFormField2<T>;
  1902. assert(dropdownButtonFormField.onChanged != null);
  1903. dropdownButtonFormField.onChanged!(value);
  1904. }
  1905. @override
  1906. void didUpdateWidget(DropdownButtonFormField2<T> oldWidget) {
  1907. super.didUpdateWidget(oldWidget);
  1908. if (oldWidget.initialValue != widget.initialValue) {
  1909. setValue(widget.initialValue);
  1910. }
  1911. }
  1912. }