popup_window.dart 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. import 'package:flutter/foundation.dart';
  2. import 'package:flutter/material.dart';
  3. import 'package:liftmanager/routers/fluro_navigator.dart';
  4. ///create by elileo on 2018/12/21
  5. ///https://github.com/elileo1/flutter_travel_friends/blob/master/lib/widget/PopupWindow.dart
  6. ///
  7. /// weilu update: 去除了IntrinsicWidth限制,添加了默认蒙版
  8. const Duration _kWindowDuration = const Duration(milliseconds: 0);
  9. const double _kWindowCloseIntervalEnd = 2.0 / 3.0;
  10. const double _kWindowMaxWidth = 240.0;
  11. const double _kWindowMinWidth = 48.0;
  12. const double _kWindowVerticalPadding = 0.0;
  13. const double _kWindowScreenPadding = 0.0;
  14. ///弹窗方法
  15. Future<T> showPopupWindow<T>({
  16. @required BuildContext context,
  17. RelativeRect position,
  18. @required Widget child,
  19. double elevation: 8.0,
  20. String semanticLabel,
  21. bool fullWidth,
  22. bool isShowBg = false,
  23. }) {
  24. assert(context != null);
  25. String label = semanticLabel;
  26. switch (defaultTargetPlatform) {
  27. case TargetPlatform.iOS:
  28. label = semanticLabel;
  29. break;
  30. case TargetPlatform.android:
  31. case TargetPlatform.fuchsia:
  32. label = semanticLabel ?? MaterialLocalizations.of(context)?.popupMenuLabel;
  33. }
  34. return Navigator.push(context,
  35. new _PopupWindowRoute(
  36. context: context,
  37. position: position,
  38. child: child,
  39. elevation: elevation,
  40. semanticLabel: label,
  41. theme: Theme.of(context, shadowThemeOnly: true),
  42. barrierLabel:
  43. MaterialLocalizations.of(context).modalBarrierDismissLabel,
  44. fullWidth: fullWidth,
  45. isShowBg: isShowBg
  46. ));
  47. }
  48. ///自定义弹窗路由:参照_PopupMenuRoute修改的
  49. class _PopupWindowRoute<T> extends PopupRoute<T> {
  50. _PopupWindowRoute({
  51. @required BuildContext context,
  52. RouteSettings settings,
  53. this.child,
  54. this.position,
  55. this.elevation: 8.0,
  56. this.theme,
  57. this.barrierLabel,
  58. this.semanticLabel,
  59. this.fullWidth,
  60. this.isShowBg,
  61. }) : super(settings: settings) {
  62. assert(child != null);
  63. }
  64. final Widget child;
  65. final RelativeRect position;
  66. double elevation;
  67. final ThemeData theme;
  68. final String semanticLabel;
  69. final bool fullWidth;
  70. final bool isShowBg;
  71. @override
  72. Color get barrierColor => null;
  73. @override
  74. bool get barrierDismissible => true;
  75. @override
  76. final String barrierLabel;
  77. @override
  78. Duration get transitionDuration => _kWindowDuration;
  79. @override
  80. Animation<double> createAnimation() {
  81. return new CurvedAnimation(
  82. parent: super.createAnimation(),
  83. curve: Curves.linear,
  84. reverseCurve: const Interval(0.0, _kWindowCloseIntervalEnd));
  85. }
  86. @override
  87. Widget buildPage(BuildContext context, Animation<double> animation,
  88. Animation<double> secondaryAnimation) {
  89. Widget win = new _PopupWindow<T>(
  90. route: this,
  91. semanticLabel: semanticLabel,
  92. fullWidth: fullWidth,
  93. );
  94. if (theme != null) {
  95. win = new Theme(data: theme, child: win);
  96. }
  97. return new MediaQuery.removePadding(
  98. context: context,
  99. removeTop: true,
  100. removeBottom: true,
  101. removeLeft: true,
  102. removeRight: true,
  103. child: new Builder(
  104. builder: (BuildContext context) {
  105. return Material(
  106. type: MaterialType.transparency,
  107. child: GestureDetector(
  108. onTap: () => NavigatorUtils.goBack(context),
  109. child: Container(
  110. width: double.infinity,
  111. height: double.infinity,
  112. color: isShowBg ? const Color(0x99000000) : null,
  113. child: new CustomSingleChildLayout(
  114. delegate: new _PopupWindowLayoutDelegate(
  115. position, null, Directionality.of(context)),
  116. child: win,
  117. ),
  118. ),
  119. ),
  120. );
  121. },
  122. ),
  123. );
  124. }
  125. }
  126. ///自定义弹窗控件:对自定义的弹窗内容进行再包装,添加长宽、动画等约束条件
  127. class _PopupWindow<T> extends StatelessWidget {
  128. const _PopupWindow({
  129. Key key,
  130. this.route,
  131. this.semanticLabel,
  132. this.fullWidth: false,
  133. }) : super(key: key);
  134. final _PopupWindowRoute<T> route;
  135. final String semanticLabel;
  136. final bool fullWidth;
  137. @override
  138. Widget build(BuildContext context) {
  139. final double length = 10.0;
  140. final double unit = 1.0 /
  141. (length + 1.5); // 1.0 for the width and 0.5 for the last item's fade.
  142. final CurveTween opacity =
  143. new CurveTween(curve: const Interval(0.0, 1.0 / 3.0));
  144. final CurveTween width = new CurveTween(curve: new Interval(0.0, unit));
  145. final CurveTween height = new CurveTween(curve: new Interval(0.0, unit * length));
  146. final Widget child = new ConstrainedBox(
  147. constraints: new BoxConstraints(
  148. minWidth: fullWidth ? double.infinity : _kWindowMinWidth,
  149. maxWidth: fullWidth ? double.infinity : _kWindowMaxWidth,
  150. ),
  151. child: new SingleChildScrollView(
  152. padding:
  153. const EdgeInsets.symmetric(vertical: _kWindowVerticalPadding),
  154. child: route.child,
  155. )
  156. );
  157. return new AnimatedBuilder(
  158. animation: route.animation,
  159. builder: (BuildContext context, Widget child) {
  160. return new Opacity(
  161. opacity: opacity.evaluate(route.animation),
  162. child: new Material(
  163. type: route.elevation == 0 ? MaterialType.transparency : MaterialType.card,
  164. elevation: route.elevation,
  165. child: new Align(
  166. alignment: AlignmentDirectional.topEnd,
  167. widthFactor: width.evaluate(route.animation),
  168. heightFactor: height.evaluate(route.animation),
  169. child: new Semantics(
  170. scopesRoute: true,
  171. namesRoute: true,
  172. explicitChildNodes: true,
  173. label: semanticLabel,
  174. child: child,
  175. ),
  176. ),
  177. ),
  178. );
  179. },
  180. child: child,
  181. );
  182. }
  183. }
  184. ///自定义委托内容:子控件大小及其位置计算
  185. class _PopupWindowLayoutDelegate extends SingleChildLayoutDelegate {
  186. _PopupWindowLayoutDelegate(
  187. this.position, this.selectedItemOffset, this.textDirection);
  188. final RelativeRect position;
  189. final double selectedItemOffset;
  190. final TextDirection textDirection;
  191. @override
  192. BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
  193. // The menu can be at most the size of the overlay minus 8.0 pixels in each
  194. // direction.
  195. return new BoxConstraints.loose(constraints.biggest -
  196. const Offset(_kWindowScreenPadding * 2.0, _kWindowScreenPadding * 2.0));
  197. }
  198. @override
  199. Offset getPositionForChild(Size size, Size childSize) {
  200. // size: The size of the overlay.
  201. // childSize: The size of the menu, when fully open, as determined by
  202. // getConstraintsForChild.
  203. // Find the ideal vertical position.
  204. double y;
  205. if (selectedItemOffset == null) {
  206. y = position.top;
  207. } else {
  208. y = position.top +
  209. (size.height - position.top - position.bottom) / 2.0 -
  210. selectedItemOffset;
  211. }
  212. // Find the ideal horizontal position.
  213. double x;
  214. if (position.left > position.right) {
  215. // Menu button is closer to the right edge, so grow to the left, aligned to the right edge.
  216. x = size.width - position.right - childSize.width;
  217. } else if (position.left < position.right) {
  218. // Menu button is closer to the left edge, so grow to the right, aligned to the left edge.
  219. x = position.left;
  220. } else {
  221. // Menu button is equidistant from both edges, so grow in reading direction.
  222. assert(textDirection != null);
  223. switch (textDirection) {
  224. case TextDirection.rtl:
  225. x = size.width - position.right - childSize.width;
  226. break;
  227. case TextDirection.ltr:
  228. x = position.left;
  229. break;
  230. }
  231. }
  232. // Avoid going outside an area defined as the rectangle 8.0 pixels from the
  233. // edge of the screen in every direction.
  234. if (x < _kWindowScreenPadding)
  235. x = _kWindowScreenPadding;
  236. else if (x + childSize.width > size.width - _kWindowScreenPadding)
  237. x = size.width - childSize.width - _kWindowScreenPadding;
  238. if (y < _kWindowScreenPadding)
  239. y = _kWindowScreenPadding;
  240. else if (y + childSize.height > size.height - _kWindowScreenPadding)
  241. y = size.height - childSize.height - _kWindowScreenPadding;
  242. return new Offset(x, y);
  243. }
  244. @override
  245. bool shouldRelayout(_PopupWindowLayoutDelegate oldDelegate) {
  246. return position != oldDelegate.position;
  247. }
  248. }