// 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 'dart:ui' show window; import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'button_theme.dart'; import 'colors.dart'; import 'constants.dart'; import 'debug.dart'; import 'icons.dart'; import 'ink_well.dart'; import 'input_decorator.dart'; import 'material.dart'; import 'material_localizations.dart'; import 'scrollbar.dart'; import 'shadows.dart'; import 'theme.dart'; const Duration _kDropdownMenuDuration = Duration(milliseconds: 300); const double _kMenuItemHeight = kMinInteractiveDimension; const double _kDenseButtonHeight = 24.0; const EdgeInsets _kMenuItemPadding = EdgeInsets.symmetric(horizontal: 16.0); const EdgeInsetsGeometry _kAlignedButtonPadding = EdgeInsetsDirectional.only(start: 16.0, end: 4.0); const EdgeInsets _kUnalignedButtonPadding = EdgeInsets.zero; const EdgeInsets _kAlignedMenuMargin = EdgeInsets.zero; const EdgeInsetsGeometry _kUnalignedMenuMargin = EdgeInsetsDirectional.only(start: 16.0, end: 24.0); typedef DropdownButtonBuilder = List Function(BuildContext context); class _DropdownMenuPainter extends CustomPainter { _DropdownMenuPainter({ this.color, this.elevation, this.selectedIndex, this.resize, 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.circular(2.0), boxShadow: kElevationToShadow[elevation], ).createBoxPainter(), super(repaint: resize); final Color color; final int elevation; final int selectedIndex; final Animation resize; final ValueGetter getSelectedItemOffset; final BoxPainter _painter; @override void paint(Canvas canvas, Size size) { final double selectedItemOffset = getSelectedItemOffset(); final Tween top = Tween( begin: selectedItemOffset.clamp(0.0, size.height - _kMenuItemHeight), end: 0.0, ); final Tween bottom = Tween( begin: (top.begin + _kMenuItemHeight).clamp(_kMenuItemHeight, 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.resize != resize; } } // Do not use the platform-specific default scroll configuration. // Dropdown menus should never overscroll or display an overscroll indicator. class _DropdownScrollBehavior extends ScrollBehavior { const _DropdownScrollBehavior(); @override TargetPlatform getPlatform(BuildContext context) => Theme.of(context).platform; @override Widget buildViewportChrome(BuildContext context, Widget child, AxisDirection axisDirection) => child; @override ScrollPhysics getScrollPhysics(BuildContext context) => const ClampingScrollPhysics(); } // The widget that is the button wrapping the menu items. class _DropdownMenuItemButton extends StatefulWidget { const _DropdownMenuItemButton({ Key key, @required this.padding, @required this.route, @required this.buttonRect, @required this.constraints, @required this.itemIndex, }) : super(key: key); final _DropdownRoute route; final EdgeInsets padding; final Rect buttonRect; final BoxConstraints constraints; final int itemIndex; @override _DropdownMenuItemButtonState createState() => _DropdownMenuItemButtonState(); } class _DropdownMenuItemButtonState extends State<_DropdownMenuItemButton> { void _handleFocusChange(bool focused) { 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() { Navigator.pop( context, _DropdownRouteResult(widget.route.items[widget.itemIndex].item.value), ); } static final Map _webShortcuts ={ LogicalKeySet(LogicalKeyboardKey.enter): const Intent(SelectAction.key), }; @override Widget build(BuildContext context) { 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 = (0.5 + (widget.itemIndex + 1) * unit).clamp(0.0, 1.0); final double end = (start + 1.5 * unit).clamp(0.0, 1.0); opacity = CurvedAnimation(parent: widget.route.animation, curve: Interval(start, end)); } Widget child = FadeTransition( opacity: opacity, child: InkWell( autofocus: widget.itemIndex == widget.route.selectedIndex, child: Container( padding: widget.padding, child: widget.route.items[widget.itemIndex], ), onTap: _handleOnTap, onFocusChange: _handleFocusChange, ), ); if (kIsWeb) { // On the web, enter doesn't select things, *except* in a