Hibok
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

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