1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768 |
- // Copyright 2014 The Flutter Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style license that can be
- // found in the LICENSE file.
- 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';
- import 'package:flyinsonolite/infrastructure/scale.dart';
- const Duration _kDropdownMenuDuration = Duration(milliseconds: 300);
- double get _kMenuItemHeight => kMinInteractiveDimension.s;
- double get _kDenseButtonHeight => 24.s;
- EdgeInsets get _kMenuItemPadding => EdgeInsets.symmetric(horizontal: 16.s);
- EdgeInsetsGeometry get _kAlignedButtonPadding =>
- EdgeInsetsDirectional.only(start: 16.s, end: 4.s);
- const EdgeInsets _kUnalignedButtonPadding = EdgeInsets.zero;
- const EdgeInsets _kAlignedMenuMargin = EdgeInsets.zero;
- EdgeInsetsGeometry get _kUnalignedMenuMargin =>
- EdgeInsetsDirectional.only(start: 16.s, end: 24.s);
- /// A builder to customize dropdown buttons.
- ///
- /// Used by [CustomDropdownButton.selectedItemBuilder].
- typedef DropdownButtonBuilder = List<Widget> Function(BuildContext context);
- class _DropdownMenuPainter extends CustomPainter {
- _DropdownMenuPainter({
- this.color,
- this.elevation,
- this.selectedIndex,
- this.borderRadius,
- required this.resize,
- required this.getSelectedItemOffset,
- }) : _painter = 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:
- 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 BorderRadius? borderRadius;
- final Animation<double> resize;
- final ValueGetter<double> getSelectedItemOffset;
- final BoxPainter _painter;
- @override
- void paint(Canvas canvas, Size size) {
- final double selectedItemOffset = getSelectedItemOffset();
- final Tween<double> top = Tween<double>(
- begin: clampDouble(selectedItemOffset, 0.0,
- math.max(size.height - _kMenuItemHeight, 0.0)),
- end: 0.0,
- );
- final Tween<double> bottom = Tween<double>(
- begin: clampDouble(top.begin! + _kMenuItemHeight,
- math.min(_kMenuItemHeight, 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.borderRadius != borderRadius ||
- oldPainter.resize != resize;
- }
- }
- // The widget that is the button wrapping the menu items.
- class _DropdownMenuItemButton<T> extends StatefulWidget {
- const _DropdownMenuItemButton({
- super.key,
- this.padding,
- required this.route,
- required this.buttonRect,
- required this.constraints,
- required this.itemIndex,
- required this.enableFeedback,
- });
- final _DropdownRoute<T> route;
- final EdgeInsets? padding;
- final Rect buttonRect;
- final BoxConstraints constraints;
- final int itemIndex;
- final bool enableFeedback;
- @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 CustomDropdownMenuItem<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 CustomDropdownMenuItem<T> dropdownMenuItem =
- widget.route.items[widget.itemIndex].item!;
- final CurvedAnimation opacity;
- final double unit = 0.5 / (widget.route.items.length + 1.5);
- if (widget.itemIndex == widget.route.selectedIndex) {
- opacity = CurvedAnimation(
- parent: widget.route.animation!, curve: const Threshold(0.0));
- } else {
- 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);
- opacity = CurvedAnimation(
- parent: widget.route.animation!, curve: Interval(start, end));
- }
- Widget child = Container(
- padding: widget.padding,
- height: widget.route.itemHeight,
- child: widget.route.items[widget.itemIndex],
- );
- // An [InkWell] is added to the item only if it is enabled
- if (dropdownMenuItem.enabled) {
- child = InkWell(
- autofocus: widget.itemIndex == widget.route.selectedIndex,
- enableFeedback: widget.enableFeedback,
- onTap: _handleOnTap,
- onFocusChange: _handleFocusChange,
- 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({
- super.key,
- this.padding,
- required this.route,
- required this.buttonRect,
- required this.constraints,
- this.dropdownColor,
- required this.enableFeedback,
- this.borderRadius,
- });
- final _DropdownRoute<T> route;
- final EdgeInsets? padding;
- final Rect buttonRect;
- final BoxConstraints constraints;
- final Color? dropdownColor;
- final bool enableFeedback;
- final BorderRadius? borderRadius;
- @override
- _DropdownMenuState<T> createState() => _DropdownMenuState<T>();
- }
- class _DropdownMenuState<T> extends State<_DropdownMenu<T>> {
- late CurvedAnimation _fadeOpacity;
- late CurvedAnimation _resize;
- @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),
- );
- }
- @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;
- final List<Widget> children = <Widget>[
- for (int itemIndex = 0; itemIndex < route.items.length; ++itemIndex)
- _DropdownMenuItemButton<T>(
- route: widget.route,
- padding: widget.padding,
- buttonRect: widget.buttonRect,
- constraints: widget.constraints,
- itemIndex: itemIndex,
- enableFeedback: widget.enableFeedback,
- ),
- ];
- return FadeTransition(
- opacity: _fadeOpacity,
- child: CustomPaint(
- painter: _DropdownMenuPainter(
- color: widget.dropdownColor ?? Theme.of(context).canvasColor,
- elevation: route.elevation,
- selectedIndex: route.selectedIndex,
- resize: _resize,
- borderRadius: widget.borderRadius,
- // This offset is passed as a callback, not a value, because it must
- // be retrieved at paint time (after layout), not at build time.
- getSelectedItemOffset: () => route.getItemOffset(route.selectedIndex),
- ),
- child: Semantics(
- scopesRoute: true,
- namesRoute: true,
- explicitChildNodes: true,
- label: localizations.popupMenuLabel,
- child: ClipRRect(
- borderRadius: widget.borderRadius ?? BorderRadius.zero,
- clipBehavior:
- widget.borderRadius != null ? Clip.antiAlias : Clip.none,
- child: Material(
- type: MaterialType.transparency,
- textStyle: route.style,
- 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(
- thumbVisibility: true,
- child: ListView(
- // Ensure this always inherits the PrimaryScrollController
- primary: true,
- padding: kMaterialListPadding,
- shrinkWrap: true,
- children: children,
- ),
- ),
- ),
- ),
- ),
- ),
- ),
- ),
- );
- }
- }
- class _DropdownMenuRouteLayout<T> extends SingleChildLayoutDelegate {
- _DropdownMenuRouteLayout({
- required this.buttonRect,
- required this.route,
- required this.textDirection,
- });
- final Rect buttonRect;
- final _DropdownRoute<T> route;
- final TextDirection? textDirection;
- @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 * _kMenuItemHeight);
- 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 = 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, 0.0, size.width) - childSize.width;
- break;
- case TextDirection.ltr:
- left = clampDouble(buttonRect.left, 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,
- this.elevation = 8,
- required this.capturedThemes,
- required this.style,
- this.barrierLabel,
- this.itemHeight,
- this.dropdownColor,
- this.menuMaxHeight,
- required this.enableFeedback,
- this.borderRadius,
- }) : assert(style != null),
- itemHeights = List<double>.filled(
- items.length, itemHeight ?? kMinInteractiveDimension.s);
- final List<_MenuItem<T>> items;
- final EdgeInsetsGeometry padding;
- final Rect buttonRect;
- final int selectedIndex;
- final int elevation;
- final CapturedThemes capturedThemes;
- final TextStyle style;
- final double? itemHeight;
- final Color? dropdownColor;
- final double? menuMaxHeight;
- final bool enableFeedback;
- final BorderRadius? borderRadius;
- final List<double> itemHeights;
- ScrollController? scrollController;
- @override
- Duration get transitionDuration => _kDropdownMenuDuration;
- @override
- bool get barrierDismissible => true;
- @override
- Color? get barrierColor => null;
- @override
- final String? barrierLabel;
- @override
- Widget buildPage(BuildContext context, Animation<double> animation,
- Animation<double> secondaryAnimation) {
- return LayoutBuilder(
- builder: (BuildContext context, BoxConstraints constraints) {
- return _DropdownRoutePage<T>(
- route: this,
- constraints: constraints,
- items: items,
- padding: padding,
- buttonRect: buttonRect,
- selectedIndex: selectedIndex,
- elevation: elevation,
- capturedThemes: capturedThemes,
- style: style,
- dropdownColor: dropdownColor,
- enableFeedback: enableFeedback,
- borderRadius: borderRadius,
- );
- },
- );
- }
- void _dismiss() {
- if (isActive) {
- navigator?.removeRoute(this);
- }
- }
- double getItemOffset(int index) {
- double offset = kMaterialListPadding.top;
- 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 * _kMenuItemHeight;
- 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);
- // 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(_kMenuItemHeight, buttonTop);
- final double bottomLimit =
- math.max(availableHeight - _kMenuItemHeight, buttonBottom);
- double menuTop = (buttonTop - selectedItemOffset) -
- (itemHeights[selectedIndex] - buttonRect.height) / 2.0;
- double preferredMenuHeight = 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 = buttonBottom -
- buttonRect.height / 2.0 +
- itemHeights[selectedIndex] / 2.0;
- 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 - (buttonTop - menuTop));
- // 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({
- super.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.dropdownColor,
- required this.enableFeedback,
- this.borderRadius,
- });
- 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 Color? dropdownColor;
- final bool enableFeedback;
- final BorderRadius? borderRadius;
- @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,
- dropdownColor: dropdownColor,
- enableFeedback: enableFeedback,
- borderRadius: borderRadius,
- );
- 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,
- ),
- 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({
- super.key,
- required this.onLayout,
- required this.item,
- }) : assert(onLayout != null),
- super(child: item);
- final ValueChanged<Size> onLayout;
- final CustomDropdownMenuItem<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])
- : assert(onLayout != null),
- 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({
- super.key,
- this.alignment = AlignmentDirectional.centerStart,
- required this.child,
- }) : assert(child != null);
- /// 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: BoxConstraints(minHeight: _kMenuItemHeight),
- alignment: alignment,
- child: child,
- );
- }
- }
- /// An item in a menu created by a [CustomDropdownButton].
- ///
- /// The type `T` is the type of the value the entry represents. All the entries
- /// in a given menu must represent values with consistent types.
- class CustomDropdownMenuItem<T> extends _DropdownMenuItemContainer {
- /// Creates an item for a dropdown menu.
- ///
- /// The [child] argument is required.
- const CustomDropdownMenuItem({
- super.key,
- this.onTap,
- this.value,
- this.enabled = true,
- super.alignment,
- required super.child,
- }) : assert(child != null);
- /// Called when the dropdown menu item is tapped.
- final VoidCallback? onTap;
- /// The value to return if the user selects this menu item.
- ///
- /// Eventually returned in a call to [CustomDropdownButton.onChanged].
- final T? value;
- /// Whether or not a user can select this menu item.
- ///
- /// Defaults to `true`.
- final bool enabled;
- }
- /// An inherited widget that causes any descendant [CustomDropdownButton]
- /// widgets to not include their regular underline.
- ///
- /// This is used by [DataTable] to remove the underline from any
- /// [CustomDropdownButton] widgets placed within material data tables, as
- /// required by the Material Design specification.
- class CustomDropdownButtonHideUnderline extends InheritedWidget {
- /// Creates a [CustomDropdownButtonHideUnderline]. A non-null [child] must
- /// be given.
- const CustomDropdownButtonHideUnderline({
- super.key,
- required super.child,
- }) : assert(child != null);
- /// Returns whether the underline of [CustomDropdownButton] widgets should
- /// be hidden.
- static bool at(BuildContext context) {
- return context.dependOnInheritedWidgetOfExactType<
- CustomDropdownButtonHideUnderline>() !=
- null;
- }
- @override
- bool updateShouldNotify(CustomDropdownButtonHideUnderline oldWidget) => false;
- }
- /// 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.
- ///
- /// {@youtube 560 315 https://www.youtube.com/watch?v=ZzQ_PWrFihg}
- ///
- /// 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 [CustomDropdownMenuItem] 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 [CustomDropdownButton] 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.
- ///
- /// {@youtube 560 315 https://www.youtube.com/watch?v=ZzQ_PWrFihg}
- ///
- /// See also:
- ///
- /// * [DropdownButtonFormField], which integrates with the [Form] widget.
- /// * [CustomDropdownMenuItem], the class used to represent the [items].
- /// * [CustomDropdownButtonHideUnderline], which prevents its descendant dropdown buttons
- /// from displaying their underlines.
- /// * [CustomElevatedButton], [CustomTextButton], ordinary buttons that trigger a single action.
- /// * <https://material.io/design/components/menus.html#dropdown-menu>
- class CustomDropdownButton<T> extends StatefulWidget {
- /// Creates a dropdown button.
- ///
- /// The [items] must have distinct values. If [value] isn't null then it
- /// must be equal to one of the [CustomDropdownMenuItem] 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.
- ///
- /// The [elevation] and [iconSize] arguments must not be null (they both have
- /// defaults, so do not need to be specified). The boolean [isDense] and
- /// [isExpanded] arguments must not be null.
- ///
- /// The [autofocus] argument must not be null.
- ///
- /// The [dropdownColor] argument specifies the background color of the
- /// dropdown when it is open. If it is null, the current theme's
- /// [ThemeData.canvasColor] will be used instead.
- CustomDropdownButton({
- super.key,
- required this.items,
- this.selectedItemBuilder,
- this.value,
- this.hint,
- this.disabledHint,
- required this.onChanged,
- this.onTap,
- this.elevation = 8,
- this.style,
- this.underline,
- this.icon,
- 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.dropdownColor,
- this.menuMaxHeight,
- this.enableFeedback,
- this.alignment = AlignmentDirectional.centerStart,
- this.borderRadius,
- // When adding new arguments, consider adding similar arguments to
- // DropdownButtonFormField.
- }) : assert(
- items == null ||
- items.isEmpty ||
- value == null ||
- items.where((CustomDropdownMenuItem<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(elevation != null),
- assert(iconSize != null),
- assert(isDense != null),
- assert(isExpanded != null),
- assert(autofocus != null),
- _inputDecoration = null,
- _isEmpty = false,
- _isFocused = false;
- CustomDropdownButton._formField({
- super.key,
- required this.items,
- this.selectedItemBuilder,
- this.value,
- this.hint,
- this.disabledHint,
- required this.onChanged,
- this.onTap,
- this.elevation = 8,
- this.style,
- this.underline,
- this.icon,
- 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.dropdownColor,
- this.menuMaxHeight,
- this.enableFeedback,
- this.alignment = AlignmentDirectional.centerStart,
- this.borderRadius,
- required InputDecoration inputDecoration,
- required bool isEmpty,
- required bool isFocused,
- }) : assert(
- items == null ||
- items.isEmpty ||
- value == null ||
- items.where((CustomDropdownMenuItem<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',
- ),
- assert(elevation != null),
- assert(iconSize != null),
- assert(isDense != null),
- assert(isExpanded != null),
- assert(autofocus != null),
- assert(isEmpty != null),
- assert(isFocused != null),
- _inputDecoration = inputDecoration,
- _isEmpty = isEmpty,
- _isFocused = isFocused;
- /// 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<CustomDropdownMenuItem<T>>? items;
- /// The value of the currently selected [CustomDropdownMenuItem].
- ///
- /// 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 [CustomDropdownButton.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 [CustomDropdownButton.disabledHint] widget if it is non-null.
- /// If [CustomDropdownButton.disabledHint] is also null but [CustomDropdownButton.hint] is
- /// non-null, [CustomDropdownButton.hint] will instead be displayed.
- /// {@endtemplate}
- final ValueChanged<T?>? onChanged;
- /// Called when the dropdown button is tapped.
- ///
- /// This is distinct from [onChanged], which is called when the user
- /// selects an item from the dropdown.
- ///
- /// The callback will not be invoked if the dropdown button is disabled.
- final VoidCallback? onTap;
- /// A builder to customize the dropdown buttons corresponding to the
- /// [CustomDropdownMenuItem]s in [items].
- ///
- /// When a [CustomDropdownMenuItem] is selected, the widget that will be displayed
- /// from the list corresponds to the [CustomDropdownMenuItem] 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 [CustomDropdownMenuItem].
- ///
- /// ** See code in examples/api/lib/material/dropdown/dropdown_button.selected_item_builder.0.dart **
- /// {@end-tool}
- ///
- /// If this callback is null, the [CustomDropdownMenuItem] 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 elevation;
- /// 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 down arrow icon button.
- ///
- /// 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;
- /// If null, then the menu item heights will vary according to each menu item's
- /// intrinsic height.
- ///
- /// The default value is [kMinInteractiveDimension], which is also the minimum
- /// height for menu items.
- ///
- /// If this value is null and there isn't enough vertical room for the menu,
- /// then the menu's initial scroll offset may not align the selected item with
- /// the dropdown button. That's because, in this case, the initial scroll
- /// offset is computed as if all of the menu item heights were
- /// [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 background color of the dropdown.
- ///
- /// If it is not provided, the theme's [ThemeData.canvasColor] will be used
- /// instead.
- final Color? dropdownColor;
- /// 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? menuMaxHeight;
- /// 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;
- /// Defines the corner radii of the menu's rounded rectangle shape.
- final BorderRadius? borderRadius;
- final InputDecoration? _inputDecoration;
- final bool _isEmpty;
- final bool _isFocused;
- @override
- State<CustomDropdownButton<T>> createState() =>
- _CustomDropdownButtonState<T>();
- }
- class _CustomDropdownButtonState<T> extends State<CustomDropdownButton<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;
- // Only used if needed to create _internalNode.
- FocusNode _createFocusNode() {
- return FocusNode(debugLabel: '${widget.runtimeType}');
- }
- @override
- void initState() {
- super.initState();
- _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(CustomDropdownButton<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((CustomDropdownMenuItem<T> item) =>
- item.enabled && item.value == widget.value)
- .isEmpty)) {
- _selectedIndex = null;
- return;
- }
- assert(widget.items!
- .where(
- (CustomDropdownMenuItem<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;
- }
- }
- }
- TextStyle? get _textStyle =>
- widget.style ?? Theme.of(context).textTheme.titleMedium;
- void _handleTap() {
- final TextDirection? textDirection = Directionality.maybeOf(context);
- final EdgeInsetsGeometry menuMargin =
- ButtonTheme.of(context).alignedDropdown
- ? _kAlignedMenuMargin
- : _kUnalignedMenuMargin;
- 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);
- assert(_dropdownRoute == null);
- final RenderBox itemBox = context.findRenderObject()! as RenderBox;
- final Rect itemRect = itemBox.localToGlobal(Offset.zero,
- ancestor: navigator.context.findRenderObject()) &
- itemBox.size;
- _dropdownRoute = _DropdownRoute<T>(
- items: menuItems,
- buttonRect: menuMargin.resolve(textDirection).inflateRect(itemRect),
- padding: _kMenuItemPadding.resolve(textDirection),
- selectedIndex: _selectedIndex ?? 0,
- elevation: widget.elevation,
- capturedThemes:
- InheritedTheme.capture(from: context, to: navigator.context),
- style: _textStyle!,
- barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
- itemHeight: widget.itemHeight,
- dropdownColor: widget.dropdownColor,
- menuMaxHeight: widget.menuMaxHeight,
- enableFeedback: widget.enableFeedback ?? true,
- borderRadius: widget.borderRadius,
- );
- focusNode?.requestFocus();
- navigator
- .push(_dropdownRoute!)
- .then<void>((_DropdownRouteResult<T>? newValue) {
- _removeDropdownRoute();
- if (!mounted || newValue == null) {
- return;
- }
- widget.onChanged?.call(newValue.result);
- });
- widget.onTap?.call();
- }
- // 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.titleMedium!.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;
- 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 widget.itemHeight != null
- ? SizedBox(height: widget.itemHeight, child: item)
- : Column(
- mainAxisSize: MainAxisSize.min,
- children: <Widget>[item]);
- }).toList(),
- );
- }
- const Icon defaultIcon = Icon(Icons.arrow_drop_down);
- Widget result = DefaultTextStyle(
- style: _enabled
- ? _textStyle!
- : _textStyle!.copyWith(color: Theme.of(context).disabledColor),
- child: Container(
- padding: padding.resolve(Directionality.of(context)),
- height: widget.isDense ? _denseButtonHeight : null,
- 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.icon ?? defaultIcon,
- ),
- ],
- ),
- ),
- );
- if (!CustomDropdownButtonHideUnderline.at(context)) {
- final double bottom =
- (widget.isDense || widget.itemHeight == null) ? 0 : 8.s;
- result = Stack(
- children: <Widget>[
- result,
- Positioned(
- left: 0.0,
- right: 0.0,
- bottom: bottom,
- child: widget.underline ??
- Container(
- height: 1.s,
- 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,
- },
- );
- if (widget._inputDecoration != null) {
- result = InputDecorator(
- decoration: widget._inputDecoration!,
- isEmpty: widget._isEmpty,
- isFocused: widget._isFocused,
- child: result,
- );
- }
- return Semantics(
- button: true,
- child: Actions(
- actions: _actionMap,
- child: InkWell(
- mouseCursor: effectiveMouseCursor,
- onTap: _enabled ? _handleTap : null,
- canRequestFocus: _enabled,
- borderRadius: widget.borderRadius,
- focusNode: focusNode,
- autofocus: widget.autofocus,
- focusColor: widget.focusColor ?? Theme.of(context).focusColor,
- enableFeedback: false,
- child: result,
- ),
- ),
- );
- }
- }
- /// A [FormField] that contains a [CustomDropdownButton].
- ///
- /// This is a convenience widget that wraps a [CustomDropdownButton] 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:
- ///
- /// * [CustomDropdownButton], which is the underlying text field without the [Form]
- /// integration.
- class DropdownButtonFormField<T> extends FormField<T> {
- /// Creates a [CustomDropdownButton] 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
- /// [CustomDropdownButton].
- ///
- /// The `items`, `elevation`, `iconSize`, `isDense`, `isExpanded`,
- /// `autofocus`, and `decoration` parameters must not be null.
- DropdownButtonFormField({
- super.key,
- required List<CustomDropdownMenuItem<T>>? items,
- DropdownButtonBuilder? selectedItemBuilder,
- T? value,
- Widget? hint,
- Widget? disabledHint,
- required this.onChanged,
- VoidCallback? onTap,
- int elevation = 8,
- TextStyle? style,
- Widget? icon,
- Color? iconDisabledColor,
- Color? iconEnabledColor,
- double iconSize = 24.0,
- bool isDense = true,
- bool isExpanded = false,
- double? itemHeight,
- Color? focusColor,
- FocusNode? focusNode,
- bool autofocus = false,
- Color? dropdownColor,
- InputDecoration? decoration,
- super.onSaved,
- super.validator,
- AutovalidateMode? autovalidateMode,
- double? menuMaxHeight,
- bool? enableFeedback,
- AlignmentGeometry alignment = AlignmentDirectional.centerStart,
- BorderRadius? borderRadius,
- // When adding new arguments, consider adding similar arguments to
- // DropdownButton.
- }) : assert(
- items == null ||
- items.isEmpty ||
- value == null ||
- items.where((CustomDropdownMenuItem<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(elevation != null),
- assert(iconSize != null),
- assert(isDense != null),
- assert(isExpanded != null),
- assert(autofocus != null),
- 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((CustomDropdownMenuItem<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();
- // An unfocusable Focus widget so that this widget can detect if its
- // descendants have focus or not.
- return Focus(
- canRequestFocus: false,
- skipTraversal: true,
- child: Builder(builder: (BuildContext context) {
- return CustomDropdownButtonHideUnderline(
- child: CustomDropdownButton<T>._formField(
- items: items,
- selectedItemBuilder: selectedItemBuilder,
- value: state.value,
- hint: hint,
- disabledHint: disabledHint,
- onChanged: onChanged == null ? null : state.didChange,
- onTap: onTap,
- elevation: elevation,
- style: style,
- icon: icon,
- iconDisabledColor: iconDisabledColor,
- iconEnabledColor: iconEnabledColor,
- iconSize: iconSize,
- isDense: isDense,
- isExpanded: isExpanded,
- itemHeight: itemHeight,
- focusColor: focusColor,
- focusNode: focusNode,
- autofocus: autofocus,
- dropdownColor: dropdownColor,
- menuMaxHeight: menuMaxHeight,
- enableFeedback: enableFeedback,
- alignment: alignment,
- borderRadius: borderRadius,
- inputDecoration: effectiveDecoration.copyWith(
- errorText: field.errorText),
- isEmpty: isEmpty,
- isFocused: Focus.of(context).hasFocus,
- ),
- );
- }),
- );
- },
- );
- /// {@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 DropdownButtonFormField<T> dropdownButtonFormField =
- widget as DropdownButtonFormField<T>;
- assert(dropdownButtonFormField.onChanged != null);
- dropdownButtonFormField.onChanged!(value);
- }
- @override
- void didUpdateWidget(DropdownButtonFormField<T> oldWidget) {
- super.didUpdateWidget(oldWidget);
- if (oldWidget.initialValue != widget.initialValue) {
- setValue(widget.initialValue);
- }
- }
- }
|