Hibok
No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.
 
 
 
 
 
 

546 líneas
18 KiB

  1. import 'package:flutter/material.dart';
  2. import 'triangle_painter.dart';
  3. const double _kMenuScreenPadding = 8.0;
  4. class WPopupMenu extends StatefulWidget {
  5. WPopupMenu({
  6. Key key,
  7. @required this.onValueChanged,
  8. @required this.actions,
  9. @required this.child,
  10. this.pressType = PressType.longPress,
  11. this.pageMaxChildCount = 3,
  12. this.backgroundColor = Colors.black,
  13. this.menuWidth = 250,
  14. this.menuHeight = 38,
  15. this.onLongPressStart,
  16. this.onLongPressEnd,
  17. });
  18. final ValueChanged<int> onValueChanged;
  19. final List<String> actions;
  20. final Widget child;
  21. final PressType pressType; // 点击方式 长按 还是单击
  22. final int pageMaxChildCount;
  23. final Color backgroundColor;
  24. final double menuWidth;
  25. final double menuHeight;
  26. final Function onLongPressStart;
  27. final Function onLongPressEnd;
  28. @override
  29. _WPopupMenuState createState() => _WPopupMenuState();
  30. }
  31. class _WPopupMenuState extends State<WPopupMenu> {
  32. double width;
  33. double height;
  34. RenderBox button;
  35. RenderBox overlay;
  36. OverlayEntry entry;
  37. @override
  38. void initState() {
  39. super.initState();
  40. WidgetsBinding.instance.addPostFrameCallback((call) {
  41. if(context!=null && context.size!=null){
  42. width = context.size.width;
  43. height = context.size.height;
  44. button = context.findRenderObject();
  45. overlay = Overlay.of(context).context.findRenderObject();
  46. }
  47. });
  48. }
  49. @override
  50. Widget build(BuildContext context) {
  51. return GestureDetector(
  52. child: widget.child,
  53. onTap: () {
  54. if (widget.pressType == PressType.singleClick) {
  55. onTap();
  56. }
  57. },
  58. onLongPress: () {
  59. if (widget.pressType == PressType.longPress) {
  60. onTap();
  61. }
  62. },
  63. onLongPressStart: (LongPressStartDetails details) {
  64. print('onLongPressStart');
  65. if(widget.onLongPressStart!=null)
  66. widget.onLongPressStart();
  67. },
  68. onLongPressEnd: (LongPressEndDetails detail) {
  69. print('onLongPressEnd');
  70. if(widget.onLongPressEnd!=null)
  71. widget.onLongPressEnd();
  72. });
  73. }
  74. void onTap() {
  75. Widget menuWidget = _MenuPopWidget(
  76. context,
  77. height,
  78. width,
  79. widget.actions,
  80. widget.pageMaxChildCount,
  81. widget.backgroundColor,
  82. widget.menuWidth,
  83. widget.menuHeight,
  84. button,
  85. overlay,
  86. (index) {
  87. if (index != -1) widget.onValueChanged(index);
  88. removeOverlay();
  89. },
  90. );
  91. entry = OverlayEntry(builder: (context) {
  92. return menuWidget;
  93. });
  94. Overlay.of(context).insert(entry);
  95. }
  96. void removeOverlay() {
  97. entry.remove();
  98. entry = null;
  99. }
  100. }
  101. enum PressType {
  102. // 长按
  103. longPress,
  104. // 单击
  105. singleClick,
  106. }
  107. class _MenuPopWidget extends StatefulWidget {
  108. final BuildContext btnContext;
  109. final List<String> actions;
  110. final int _pageMaxChildCount;
  111. final Color backgroundColor;
  112. final double menuWidth;
  113. final double menuHeight;
  114. final double _height;
  115. final double _width;
  116. final RenderBox button;
  117. final RenderBox overlay;
  118. final ValueChanged<int> onValueChanged;
  119. _MenuPopWidget(
  120. this.btnContext,
  121. this._height,
  122. this._width,
  123. this.actions,
  124. this._pageMaxChildCount,
  125. this.backgroundColor,
  126. this.menuWidth,
  127. this.menuHeight,
  128. this.button,
  129. this.overlay,
  130. this.onValueChanged,
  131. );
  132. @override
  133. _MenuPopWidgetState createState() => _MenuPopWidgetState();
  134. }
  135. class _MenuPopWidgetState extends State<_MenuPopWidget> {
  136. int _curPage = 0;
  137. final double _arrowWidth = 40;
  138. final double _separatorWidth = 1;
  139. final double _triangleHeight = 10;
  140. RelativeRect position;
  141. @override
  142. void initState() {
  143. super.initState();
  144. position = RelativeRect.fromRect(
  145. Rect.fromPoints(
  146. widget.button.localToGlobal(Offset.zero, ancestor: widget.overlay),
  147. widget.button.localToGlobal(Offset.zero, ancestor: widget.overlay),
  148. ),
  149. Offset.zero & widget.overlay.size,
  150. );
  151. }
  152. @override
  153. Widget build(BuildContext context) {
  154. // 这里计算出来 当前页的 child 一共有多少个
  155. int _curPageChildCount =
  156. (_curPage + 1) * widget._pageMaxChildCount > widget.actions.length
  157. ? widget.actions.length % widget._pageMaxChildCount
  158. : widget._pageMaxChildCount;
  159. double _curArrowWidth = 0;
  160. int _curArrowCount = 0; // 一共几个箭头
  161. if (widget.actions.length > widget._pageMaxChildCount) {
  162. // 数据长度大于 widget._pageMaxChildCount
  163. if (_curPage == 0) {
  164. // 如果是第一页
  165. _curArrowWidth = _arrowWidth;
  166. _curArrowCount = 1;
  167. } else {
  168. // 如果不是第一页 则需要也显示左箭头
  169. _curArrowWidth = _arrowWidth * 2;
  170. _curArrowCount = 2;
  171. }
  172. }
  173. double _curPageWidth = widget.menuWidth +
  174. (_curPageChildCount - 1 + _curArrowCount) * _separatorWidth +
  175. _curArrowWidth;
  176. return GestureDetector(
  177. behavior: HitTestBehavior.opaque,
  178. onTap: () {
  179. widget.onValueChanged(-1);
  180. },
  181. child: MediaQuery.removePadding(
  182. context: context,
  183. removeTop: true,
  184. removeBottom: true,
  185. removeLeft: true,
  186. removeRight: true,
  187. child: Builder(
  188. builder: (BuildContext context) {
  189. var isInverted = (position.top +
  190. (MediaQuery.of(context).size.height -
  191. position.top -
  192. position.bottom) /
  193. 2.0 -
  194. (widget.menuHeight + _triangleHeight)) <
  195. (widget.menuHeight + _triangleHeight) * 2;
  196. return CustomSingleChildLayout(
  197. // 这里计算偏移量
  198. delegate: _PopupMenuRouteLayout(
  199. position,
  200. widget.menuHeight + _triangleHeight,
  201. Directionality.of(widget.btnContext),
  202. widget._width,
  203. widget.menuWidth,
  204. widget._height),
  205. child: SizedBox(
  206. height: widget.menuHeight + _triangleHeight,
  207. width: _curPageWidth,
  208. child: Material(
  209. color: Colors.transparent,
  210. child: Column(
  211. mainAxisSize: MainAxisSize.min,
  212. children: <Widget>[
  213. isInverted
  214. ? CustomPaint(
  215. size: Size(_curPageWidth, _triangleHeight),
  216. painter: TrianglePainter(
  217. color: widget.backgroundColor,
  218. position: position,
  219. isInverted: true,
  220. size: widget.button.size,
  221. screenWidth: MediaQuery.of(context).size.width,
  222. ),
  223. )
  224. : Container(),
  225. Expanded(
  226. child: Stack(
  227. children: <Widget>[
  228. ClipRRect(
  229. borderRadius:
  230. BorderRadius.all(Radius.circular(5)),
  231. child: Container(
  232. color: widget.backgroundColor,
  233. height: widget.menuHeight,
  234. ),
  235. ),
  236. Row(
  237. mainAxisSize: MainAxisSize.min,
  238. children: <Widget>[
  239. // 左箭头:判断是否是第一页,如果是第一页则不显示
  240. _curPage == 0
  241. ? Container(
  242. height: widget.menuHeight,
  243. )
  244. : InkWell(
  245. onTap: () {
  246. setState(() {
  247. _curPage--;
  248. });
  249. },
  250. child: Container(
  251. alignment: Alignment.centerRight,
  252. width: _arrowWidth + 3,
  253. height: widget.menuHeight - 10,
  254. child: Image.asset(
  255. 'assets/images/left_white.png',
  256. fit: BoxFit.none,
  257. ),
  258. ),
  259. ),
  260. // 左箭头:判断是否是第一页,如果是第一页则不显示
  261. _curPage == 0
  262. ? Container(
  263. height: widget.menuHeight,
  264. )
  265. : Container(
  266. width: 1,
  267. height: widget.menuHeight,
  268. color: Colors.grey,
  269. ),
  270. // 中间是ListView
  271. _buildList(_curPageChildCount, _curPageWidth,
  272. _curArrowWidth, _curArrowCount),
  273. // 右箭头:判断是否有箭头,如果有就显示,没有就不显示
  274. _curArrowCount > 0
  275. ? Container(
  276. width: 1,
  277. color: Colors.grey,
  278. height: widget.menuHeight,
  279. )
  280. : Container(
  281. height: widget.menuHeight,
  282. ),
  283. _curArrowCount > 0
  284. ? InkWell(
  285. onTap: () {
  286. if ((_curPage + 1) *
  287. widget._pageMaxChildCount <
  288. widget.actions.length)
  289. setState(() {
  290. _curPage++;
  291. });
  292. },
  293. child: Container(
  294. width: _arrowWidth-4,
  295. height: widget.menuHeight,
  296. child: Image.asset(
  297. (_curPage + 1) *
  298. widget
  299. ._pageMaxChildCount >=
  300. widget.actions.length
  301. ? 'assets/images/right_gray.png'
  302. : 'assets/images/right_white.png',
  303. fit: BoxFit.none,
  304. ),
  305. ),
  306. )
  307. : Container(
  308. height: widget.menuHeight,
  309. ),
  310. ],
  311. ),
  312. ],
  313. ),
  314. ),
  315. isInverted
  316. ? Container()
  317. : CustomPaint(
  318. size: Size(_curPageWidth, _triangleHeight),
  319. painter: TrianglePainter(
  320. color: widget.backgroundColor,
  321. position: position,
  322. size: widget.button.size,
  323. screenWidth: MediaQuery.of(context).size.width,
  324. ),
  325. ),
  326. ],
  327. ),
  328. ),
  329. ),
  330. );
  331. },
  332. ),
  333. ),
  334. );
  335. }
  336. Widget _buildList(int _curPageChildCount, double _curPageWidth,
  337. double _curArrowWidth, int _curArrowCount) {
  338. List<Widget> list = [];
  339. int totalTxtLength = 0;
  340. double minPadding = 12;
  341. double width = widget.menuWidth - _curPageChildCount * minPadding;
  342. for (int k = 0; k < _curPageChildCount; k++) {
  343. totalTxtLength = totalTxtLength +
  344. widget.actions[_curPage * widget._pageMaxChildCount + k].length;
  345. }
  346. for (int k = 0; k < _curPageChildCount; k++) {
  347. String text = widget.actions[_curPage * widget._pageMaxChildCount + k];
  348. list.add(InkWell(
  349. onTap: () {
  350. widget.onValueChanged(_curPage * widget._pageMaxChildCount + k);
  351. },
  352. child: SizedBox(
  353. // width: (_curPageWidth -
  354. // _curArrowWidth -
  355. // (_curPageChildCount - 1 + _curArrowCount) *
  356. // _separatorWidth) /
  357. // _curPageChildCount,
  358. width: (text.length / (totalTxtLength) * (width)) + minPadding,
  359. height: widget.menuHeight,
  360. child: Center(
  361. child: Text(
  362. text,
  363. maxLines: 1,
  364. textScaleFactor: 1.0,
  365. style: TextStyle(color: Colors.white, fontSize: 13),
  366. ),
  367. ),
  368. ),
  369. ));
  370. if (k != _curPageChildCount - 1) {
  371. list.add(Container(
  372. width: 1,
  373. height: widget.menuHeight,
  374. color: Colors.grey.withAlpha(70),
  375. ));
  376. }
  377. }
  378. return Wrap(
  379. children: list,
  380. );
  381. // return ListView.separated(
  382. // shrinkWrap: true,
  383. // physics: NeverScrollableScrollPhysics(),
  384. // scrollDirection: Axis.horizontal,
  385. // itemCount: _curPageChildCount,
  386. // itemBuilder: (BuildContext context, int index) {
  387. // return GestureDetector(
  388. // onTap: () {
  389. // widget.onValueChanged(_curPage * widget._pageMaxChildCount + index);
  390. //
  391. // },
  392. // child: SizedBox(
  393. // width: (_curPageWidth -
  394. // _curArrowWidth -
  395. // (_curPageChildCount - 1 + _curArrowCount) *
  396. // _separatorWidth) /
  397. // _curPageChildCount,
  398. // height: widget.menuHeight,
  399. // child: Center(
  400. // child: Text(
  401. //
  402. // widget.actions[_curPage * widget._pageMaxChildCount + index],
  403. // maxLines: 1,
  404. // style: TextStyle(color: Colors.white, fontSize: 16),
  405. // ),
  406. // ),
  407. // ),
  408. // );
  409. // },
  410. // separatorBuilder: (BuildContext context, int index) {
  411. // return Container(
  412. // width: 1,
  413. // height: widget.menuHeight,
  414. // color: Colors.grey,
  415. // );
  416. // },
  417. // );
  418. }
  419. }
  420. // Positioning of the menu on the screen.
  421. class _PopupMenuRouteLayout extends SingleChildLayoutDelegate {
  422. _PopupMenuRouteLayout(this.position, this.selectedItemOffset,
  423. this.textDirection, this.width, this.menuWidth, this.height);
  424. // Rectangle of underlying button, relative to the overlay's dimensions.
  425. final RelativeRect position;
  426. // The distance from the top of the menu to the middle of selected item.
  427. //
  428. // This will be null if there's no item to position in this way.
  429. final double selectedItemOffset;
  430. // Whether to prefer going to the left or to the right.
  431. final TextDirection textDirection;
  432. final double width;
  433. final double height;
  434. final double menuWidth;
  435. // We put the child wherever position specifies, so long as it will fit within
  436. // the specified parent size padded (inset) by 8. If necessary, we adjust the
  437. // child's position so that it fits.
  438. @override
  439. BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
  440. // The menu can be at most the size of the overlay minus 8.0 pixels in each
  441. // direction.
  442. return BoxConstraints.loose(constraints.biggest -
  443. const Offset(_kMenuScreenPadding * 2.0, _kMenuScreenPadding * 2.0));
  444. }
  445. @override
  446. Offset getPositionForChild(Size size, Size childSize) {
  447. // size: The size of the overlay.
  448. // childSize: The size of the menu, when fully open, as determined by
  449. // getConstraintsForChild.
  450. // Find the ideal vertical position.
  451. double y;
  452. if (selectedItemOffset == null) {
  453. y = position.top;
  454. } else {
  455. y = position.top +
  456. (size.height - position.top - position.bottom) / 2.0 -
  457. selectedItemOffset;
  458. }
  459. // Find the ideal horizontal position.
  460. double x;
  461. // 如果menu 的宽度 小于 child 的宽度,则直接把menu 放在 child 中间
  462. if (childSize.width < width) {
  463. x = position.left + (width - childSize.width) / 2;
  464. } else {
  465. // 如果靠右
  466. if (position.left > size.width - (position.left + width)) {
  467. if (size.width - (position.left + width) >
  468. childSize.width / 2 + _kMenuScreenPadding) {
  469. x = position.left - (childSize.width - width) / 2;
  470. } else {
  471. print('${position.left + width} --- ${size.width} -- $childSize');
  472. x = position.left + width - childSize.width;
  473. }
  474. } else if (position.left < size.width - (position.left + width)) {
  475. if (position.left > childSize.width / 2 + _kMenuScreenPadding) {
  476. x = position.left - (childSize.width - width) / 2;
  477. } else
  478. x = position.left;
  479. } else {
  480. x = position.right - width / 2 - childSize.width / 2;
  481. }
  482. }
  483. if (y < _kMenuScreenPadding)
  484. y = _kMenuScreenPadding;
  485. else if (y + childSize.height > size.height - _kMenuScreenPadding)
  486. y = size.height - childSize.height;
  487. else if (y < childSize.height * 2) {
  488. y = position.top + height;
  489. }
  490. // print(Offset(x, y));
  491. // print('${size} --- ${childSize}');
  492. return Offset(x, y);
  493. }
  494. @override
  495. bool shouldRelayout(_PopupMenuRouteLayout oldDelegate) {
  496. return position != oldDelegate.position;
  497. }
  498. }