Hibok
Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.
 
 
 
 
 
 

552 wiersze
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. return GestureDetector(
  182. behavior: HitTestBehavior.opaque,
  183. onTap: () {
  184. widget.onValueChanged(-1);
  185. },
  186. child: MediaQuery.removePadding(
  187. context: context,
  188. removeTop: true,
  189. removeBottom: true,
  190. removeLeft: true,
  191. removeRight: true,
  192. child: Builder(
  193. builder: (BuildContext context) {
  194. var isInverted = (position.top +
  195. (MediaQuery.of(context).size.height -
  196. position.top -
  197. position.bottom) /
  198. 2.0 -
  199. (widget.menuHeight + _triangleHeight)) <
  200. (widget.menuHeight + _triangleHeight) * 2;
  201. return CustomSingleChildLayout(
  202. // 这里计算偏移量
  203. delegate: _PopupMenuRouteLayout(
  204. position,
  205. widget.menuHeight + _triangleHeight,
  206. Directionality.of(widget.btnContext),
  207. widget._width,
  208. widget.menuWidth,
  209. widget._height),
  210. child: SizedBox(
  211. height: widget.menuHeight + _triangleHeight,
  212. width: _curPageWidth,
  213. child: Material(
  214. color: Colors.transparent,
  215. child: Column(
  216. mainAxisSize: MainAxisSize.min,
  217. children: <Widget>[
  218. isInverted
  219. ? CustomPaint(
  220. size: Size(_curPageWidth, _triangleHeight),
  221. painter: TrianglePainter(
  222. color: widget.backgroundColor,
  223. position: position,
  224. isInverted: true,
  225. size: widget.button.size,
  226. screenWidth: MediaQuery.of(context).size.width,
  227. ),
  228. )
  229. : Container(),
  230. Expanded(
  231. child: Stack(
  232. children: <Widget>[
  233. ClipRRect(
  234. borderRadius:
  235. BorderRadius.all(Radius.circular(5)),
  236. child: Container(
  237. color: widget.backgroundColor,
  238. height: widget.menuHeight,
  239. ),
  240. ),
  241. Row(
  242. mainAxisSize: MainAxisSize.min,
  243. children: <Widget>[
  244. // 左箭头:判断是否是第一页,如果是第一页则不显示
  245. _curPage == 0
  246. ? Container(
  247. height: widget.menuHeight,
  248. )
  249. : InkWell(
  250. onTap: () {
  251. setState(() {
  252. _curPage--;
  253. });
  254. },
  255. child: Container(
  256. alignment: Alignment.centerRight,
  257. width: _arrowWidth + 3,
  258. height: widget.menuHeight - 10,
  259. child: Image.asset(
  260. 'assets/images/left_white.png',
  261. fit: BoxFit.none,
  262. ),
  263. ),
  264. ),
  265. // 左箭头:判断是否是第一页,如果是第一页则不显示
  266. _curPage == 0
  267. ? Container(
  268. height: widget.menuHeight,
  269. )
  270. : Container(
  271. width: 1,
  272. height: widget.menuHeight,
  273. color: Colors.grey,
  274. ),
  275. // 中间是ListView
  276. _buildList(_curPageChildCount, _curPageWidth,
  277. _curArrowWidth, _curArrowCount),
  278. // 右箭头:判断是否有箭头,如果有就显示,没有就不显示
  279. _curArrowCount > 0
  280. ? Container(
  281. width: 1,
  282. color: Colors.grey,
  283. height: widget.menuHeight,
  284. )
  285. : Container(
  286. height: widget.menuHeight,
  287. ),
  288. _curArrowCount > 0
  289. ? InkWell(
  290. onTap: () {
  291. if ((_curPage + 1) *
  292. widget._pageMaxChildCount <
  293. widget.actions.length)
  294. setState(() {
  295. _curPage++;
  296. });
  297. },
  298. child: Container(
  299. width: _arrowWidth-4,
  300. height: widget.menuHeight,
  301. child: Image.asset(
  302. (_curPage + 1) *
  303. widget
  304. ._pageMaxChildCount >=
  305. widget.actions.length
  306. ? 'assets/images/right_gray.png'
  307. : 'assets/images/right_white.png',
  308. fit: BoxFit.none,
  309. ),
  310. ),
  311. )
  312. : Container(
  313. height: widget.menuHeight,
  314. ),
  315. ],
  316. ),
  317. ],
  318. ),
  319. ),
  320. isInverted
  321. ? Container()
  322. : CustomPaint(
  323. size: Size(_curPageWidth, _triangleHeight),
  324. painter: TrianglePainter(
  325. color: widget.backgroundColor,
  326. position: position,
  327. size: widget.button.size,
  328. screenWidth: MediaQuery.of(context).size.width,
  329. ),
  330. ),
  331. ],
  332. ),
  333. ),
  334. ),
  335. );
  336. },
  337. ),
  338. ),
  339. );
  340. }
  341. Widget _buildList(int _curPageChildCount, double _curPageWidth,
  342. double _curArrowWidth, int _curArrowCount) {
  343. List<Widget> list = [];
  344. int totalTxtLength = 0;
  345. double minPadding = 12;
  346. double width = widget.menuWidth - _curPageChildCount * minPadding;
  347. for (int k = 0; k < _curPageChildCount; k++) {
  348. totalTxtLength = totalTxtLength +
  349. widget.actions[_curPage * widget._pageMaxChildCount + k].length;
  350. }
  351. for (int k = 0; k < _curPageChildCount; k++) {
  352. String text = widget.actions[_curPage * widget._pageMaxChildCount + k];
  353. list.add(InkWell(
  354. onTap: () {
  355. widget.onValueChanged(_curPage * widget._pageMaxChildCount + k);
  356. },
  357. child: SizedBox(
  358. // width: (_curPageWidth -
  359. // _curArrowWidth -
  360. // (_curPageChildCount - 1 + _curArrowCount) *
  361. // _separatorWidth) /
  362. // _curPageChildCount,
  363. width: (text.length / (totalTxtLength) * (width)) + minPadding,
  364. height: widget.menuHeight,
  365. child: Center(
  366. child: Text(
  367. text,
  368. maxLines: 1,
  369. textScaleFactor: 1.0,
  370. style: TextStyle(color: Colors.white, fontSize: 13),
  371. ),
  372. ),
  373. ),
  374. ));
  375. if (k != _curPageChildCount - 1) {
  376. list.add(Container(
  377. width: 1,
  378. height: widget.menuHeight,
  379. color: Colors.grey.withAlpha(70),
  380. ));
  381. }
  382. }
  383. return Wrap(
  384. children: list,
  385. );
  386. // return ListView.separated(
  387. // shrinkWrap: true,
  388. // physics: NeverScrollableScrollPhysics(),
  389. // scrollDirection: Axis.horizontal,
  390. // itemCount: _curPageChildCount,
  391. // itemBuilder: (BuildContext context, int index) {
  392. // return GestureDetector(
  393. // onTap: () {
  394. // widget.onValueChanged(_curPage * widget._pageMaxChildCount + index);
  395. //
  396. // },
  397. // child: SizedBox(
  398. // width: (_curPageWidth -
  399. // _curArrowWidth -
  400. // (_curPageChildCount - 1 + _curArrowCount) *
  401. // _separatorWidth) /
  402. // _curPageChildCount,
  403. // height: widget.menuHeight,
  404. // child: Center(
  405. // child: Text(
  406. //
  407. // widget.actions[_curPage * widget._pageMaxChildCount + index],
  408. // maxLines: 1,
  409. // style: TextStyle(color: Colors.white, fontSize: 16),
  410. // ),
  411. // ),
  412. // ),
  413. // );
  414. // },
  415. // separatorBuilder: (BuildContext context, int index) {
  416. // return Container(
  417. // width: 1,
  418. // height: widget.menuHeight,
  419. // color: Colors.grey,
  420. // );
  421. // },
  422. // );
  423. }
  424. }
  425. // Positioning of the menu on the screen.
  426. class _PopupMenuRouteLayout extends SingleChildLayoutDelegate {
  427. _PopupMenuRouteLayout(this.position, this.selectedItemOffset,
  428. this.textDirection, this.width, this.menuWidth, this.height);
  429. // Rectangle of underlying button, relative to the overlay's dimensions.
  430. final RelativeRect position;
  431. // The distance from the top of the menu to the middle of selected item.
  432. //
  433. // This will be null if there's no item to position in this way.
  434. final double selectedItemOffset;
  435. // Whether to prefer going to the left or to the right.
  436. final TextDirection textDirection;
  437. final double width;
  438. final double height;
  439. final double menuWidth;
  440. // We put the child wherever position specifies, so long as it will fit within
  441. // the specified parent size padded (inset) by 8. If necessary, we adjust the
  442. // child's position so that it fits.
  443. @override
  444. BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
  445. // The menu can be at most the size of the overlay minus 8.0 pixels in each
  446. // direction.
  447. return BoxConstraints.loose(constraints.biggest -
  448. const Offset(_kMenuScreenPadding * 2.0, _kMenuScreenPadding * 2.0));
  449. }
  450. @override
  451. Offset getPositionForChild(Size size, Size childSize) {
  452. // size: The size of the overlay.
  453. // childSize: The size of the menu, when fully open, as determined by
  454. // getConstraintsForChild.
  455. // Find the ideal vertical position.
  456. double y;
  457. if (selectedItemOffset == null) {
  458. y = position.top;
  459. } else {
  460. y = position.top +
  461. (size.height - position.top - position.bottom) / 2.0 -
  462. selectedItemOffset;
  463. }
  464. // Find the ideal horizontal position.
  465. double x;
  466. // 如果menu 的宽度 小于 child 的宽度,则直接把menu 放在 child 中间
  467. if (childSize.width < width) {
  468. x = position.left + (width - childSize.width) / 2;
  469. } else {
  470. // 如果靠右
  471. if (position.left > size.width - (position.left + width)) {
  472. if (size.width - (position.left + width) >
  473. childSize.width / 2 + _kMenuScreenPadding) {
  474. x = position.left - (childSize.width - width) / 2;
  475. } else {
  476. print('${position.left + width} --- ${size.width} -- $childSize');
  477. x = position.left + width - childSize.width;
  478. }
  479. } else if (position.left < size.width - (position.left + width)) {
  480. if (position.left > childSize.width / 2 + _kMenuScreenPadding) {
  481. x = position.left - (childSize.width - width) / 2;
  482. } else
  483. x = position.left;
  484. } else {
  485. x = position.right - width / 2 - childSize.width / 2;
  486. }
  487. }
  488. if (y < _kMenuScreenPadding)
  489. y = _kMenuScreenPadding;
  490. else if (y + childSize.height > size.height - _kMenuScreenPadding)
  491. y = size.height - childSize.height;
  492. else if (y < childSize.height * 2) {
  493. y = position.top + height;
  494. }
  495. // print(Offset(x, y));
  496. // print('${size} --- ${childSize}');
  497. return Offset(x, y);
  498. }
  499. @override
  500. bool shouldRelayout(_PopupMenuRouteLayout oldDelegate) {
  501. return position != oldDelegate.position;
  502. }
  503. }