У меня была похожая ситуация. Мне нужно было реализовать раскрывающийся диалог. Я использовал источники DropdownButton и изменил их.
Мой виджет:
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
const double _hintWidgetHeight = 34;
const double _hintWidgetWidth = 140;
class MyHintWidget extends StatefulWidget {
const MyHintWidget({
Key key,
@required this.title,
@required this.style,
@required this.child,
}) : super(key: key);
final String title;
final TextStyle style;
final Widget child;
@override
_MyHintState createState() => _MyHintState();
}
class _MyHintState extends State<MyHintWidget> with WidgetsBindingObserver {
static const EdgeInsets _hintItemPadding = EdgeInsets.symmetric(horizontal: 16.0);
_HintRoute route;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_removeRoute();
super.dispose();
}
@override
void didChangeMetrics() {
_removeRoute();
}
void _removeRoute() {
route?._dismiss();
route = null;
}
void _handleTap() {
final RenderBox itemBox = context.findRenderObject() as RenderBox;
final Rect itemRect = itemBox.localToGlobal(Offset.zero) & itemBox.size;
final TextDirection textDirection = Directionality.of(context);
route = _HintRoute(
title: widget.title,
style: widget.style,
buttonRect: _hintItemPadding.resolve(textDirection).inflateRect(itemRect),
padding: _hintItemPadding.resolve(textDirection),
theme: ThemeData(
brightness: Brightness.light,
),
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
);
Navigator.push(context, route).then<void>((_) {
route = null;
});
}
@override
Widget build(BuildContext context) {
return Semantics(
button: true,
child: GestureDetector(
onTap: _handleTap,
behavior: HitTestBehavior.opaque,
child: widget.child,
),
);
}
}
class _HintRoute extends PopupRoute<void> {
_HintRoute({
@required this.title,
@required this.style,
this.padding,
this.buttonRect,
this.theme,
this.barrierLabel,
});
final String title;
final TextStyle style;
final EdgeInsetsGeometry padding;
final Rect buttonRect;
final ThemeData theme;
@override
Duration get transitionDuration => const Duration(milliseconds: 300);
@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 _HintRoutePage(
title: title,
style: style,
route: this,
constraints: constraints,
padding: padding,
buttonRect: buttonRect,
theme: theme,
);
});
}
void _dismiss() => navigator?.removeRoute(this);
}
class _HintRoutePage extends StatelessWidget {
const _HintRoutePage({
Key key,
@required this.title,
@required this.style,
this.route,
this.constraints,
this.padding,
this.buttonRect,
this.theme,
}) : super(key: key);
final String title;
final TextStyle style;
final _HintRoute route;
final BoxConstraints constraints;
final EdgeInsetsGeometry padding;
final Rect buttonRect;
final ThemeData theme;
@override
Widget build(BuildContext context) {
assert(debugCheckHasDirectionality(context));
final double availableHeight = constraints.maxHeight;
final double maxHintHeight = availableHeight - 2.0 * _hintWidgetHeight;
final double buttonTop = buttonRect.top;
final double buttonBottom = math.min(buttonRect.bottom, availableHeight);
final double topLimit = math.min(_hintWidgetHeight, buttonTop);
final double bottomLimit = math.max(availableHeight - _hintWidgetHeight, buttonBottom);
final double selectedItemOffset = _hintWidgetHeight + kMaterialListPadding.top;
double hintTop =
(buttonTop - selectedItemOffset) - (_hintWidgetHeight * 2 - buttonRect.height) / 3;
final double preferredHintHeight = _hintWidgetHeight + kMaterialListPadding.vertical;
final double hintHeight = math.min(maxHintHeight, preferredHintHeight);
double hintBottom = hintTop + hintHeight;
if (hintTop < topLimit) hintTop = math.min(buttonTop, topLimit);
if (hintBottom > bottomLimit) {
hintBottom = math.max(buttonBottom, bottomLimit);
hintTop = hintBottom - hintHeight;
}
final TextDirection textDirection = Directionality.of(context);
Widget hint = _HintWidget(
title: title,
style: style,
route: route,
padding: padding.resolve(textDirection),
);
if (theme != null) hint = Theme(data: theme, child: hint);
return MediaQuery.removePadding(
context: context,
removeTop: true,
removeBottom: true,
removeLeft: true,
removeRight: true,
child: Builder(
builder: (BuildContext context) {
return CustomSingleChildLayout(
delegate: _HintRouteLayout(
buttonRect: buttonRect,
hintTop: hintTop,
hintHeight: hintHeight,
textDirection: textDirection,
),
child: hint,
);
},
),
);
}
}
class _HintRouteLayout extends SingleChildLayoutDelegate {
_HintRouteLayout({
@required this.buttonRect,
@required this.hintTop,
@required this.hintHeight,
@required this.textDirection,
});
final Rect buttonRect;
final double hintTop;
final double hintHeight;
final TextDirection textDirection;
@override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
final double maxHeight = math.max(0.0, constraints.maxHeight - 2 * _hintWidgetHeight);
const double width = _hintWidgetWidth;
return BoxConstraints(
minWidth: width,
maxWidth: width,
minHeight: 0.0,
maxHeight: maxHeight,
);
}
@override
Offset getPositionForChild(Size size, Size childSize) {
assert(() {
final Rect container = Offset.zero & size;
if (container.intersect(buttonRect) == buttonRect) {
assert(hintTop >= 0.0);
assert(hintTop + hintHeight <= size.height);
}
return true;
}());
assert(textDirection != null);
double left;
if (buttonRect.left > size.width - childSize.width / 1.1) {
left = size.width - childSize.width;
} else if (buttonRect.left < childSize.width / 5) {
left = 0;
} else {
left = buttonRect.left - (buttonRect.right - buttonRect.left) / 3;
}
return Offset(left, hintTop);
}
@override
bool shouldRelayout(_HintRouteLayout oldDelegate) {
return buttonRect != oldDelegate.buttonRect ||
hintTop != oldDelegate.hintTop ||
hintHeight != oldDelegate.hintHeight ||
textDirection != oldDelegate.textDirection;
}
}
class _HintWidget extends StatefulWidget {
const _HintWidget({
Key key,
@required this.title,
@required this.style,
this.padding,
this.route,
}) : super(key: key);
final String title;
final TextStyle style;
final _HintRoute route;
final EdgeInsets padding;
@override
_HintState createState() => _HintState();
}
class _HintState extends State<_HintWidget> {
CurvedAnimation _fadeOpacity;
CurvedAnimation _resize;
@override
void initState() {
super.initState();
_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) {
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
final _HintRoute route = widget.route;
const double unit = 0.5 / 1.5;
CurvedAnimation opacity;
final double start = (0.5 + 1 * unit).clamp(0.0, 1.0) as double;
final double end = (start + 1.5 * unit).clamp(0.0, 1.0) as double;
opacity = CurvedAnimation(parent: route.animation, curve: Interval(start, end));
return FadeTransition(
opacity: _fadeOpacity,
child: CustomPaint(
painter: _HintPainter(
color: Theme.of(context).canvasColor,
elevation: 8,
resize: _resize,
),
child: Semantics(
scopesRoute: true,
namesRoute: true,
explicitChildNodes: true,
label: localizations.popupMenuLabel,
child: Material(
type: MaterialType.transparency,
child: Padding(
padding: kMaterialListPadding,
child: FadeTransition(
opacity: opacity,
child: InkWell(
onTap: () => Navigator.pop(context),
child: Container(
padding: widget.padding,
child: Text(
widget.title,
style: widget.style,
textAlign: TextAlign.center,
),
),
),
),
),
),
),
),
);
}
}
class _HintPainter extends CustomPainter {
_HintPainter({
this.color,
this.elevation,
this.resize,
}) : _painter = BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(6.0),
boxShadow: kElevationToShadow[elevation],
).createBoxPainter(),
super(repaint: resize);
final Color color;
final int elevation;
final Animation<double> resize;
final BoxPainter _painter;
@override
void paint(Canvas canvas, Size size) {
final double selectedItemOffset = _hintWidgetHeight + kMaterialListPadding.top;
final Tween<double> top = Tween<double>(
begin: selectedItemOffset.clamp(0.0, size.height - _hintWidgetHeight) as double,
end: 0.0,
);
final Tween<double> bottom = Tween<double>(
begin: (top.begin + _hintWidgetHeight).clamp(_hintWidgetHeight, size.height) as double,
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(_HintPainter oldPainter) {
return oldPainter.color != color ||
oldPainter.elevation != elevation ||
oldPainter.resize != resize;
}
}
Итак, он используется:
return MyHintWidget(
title: 'some text',
style: //text style[![enter image description here][1]][1],
child: //child,
);
Результат