bubble_border_arrow_properties.dart 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505
  1. import 'dart:math';
  2. import 'package:flutter/material.dart';
  3. class _BubbleBorderArrowProperties {
  4. /// 箭头宽度的一半
  5. final double halfWidth;
  6. /// 箭头斜边的长度
  7. final double hypotenuse;
  8. /// 该斜边在主轴上的投影(水平时为X轴)
  9. final double projectionOnMain;
  10. /// 该斜边在纵轴上的投影(水平时为Y轴)
  11. final double projectionOnCross;
  12. /// 计算箭头半径在主轴上的投影(水平时为X轴)
  13. final double arrowProjectionOnMain;
  14. /// 计算箭头半径尖尖的长度
  15. final double topLen;
  16. _BubbleBorderArrowProperties({
  17. required this.halfWidth,
  18. required this.hypotenuse,
  19. required this.projectionOnMain,
  20. required this.projectionOnCross,
  21. required this.arrowProjectionOnMain,
  22. required this.topLen,
  23. });
  24. }
  25. class BubbleShapeBorder extends OutlinedBorder {
  26. final BorderRadius borderRadius;
  27. final AxisDirection arrowDirection;
  28. final double arrowLength;
  29. final double arrowWidth;
  30. final double arrowRadius;
  31. final double? arrowOffset;
  32. final Color? fillColor;
  33. const BubbleShapeBorder({
  34. super.side,
  35. required this.arrowDirection,
  36. this.borderRadius = BorderRadius.zero,
  37. this.arrowLength = 12,
  38. this.arrowWidth = 18,
  39. this.arrowRadius = 3,
  40. this.arrowOffset,
  41. this.fillColor,
  42. });
  43. @override
  44. OutlinedBorder copyWith({
  45. AxisDirection? arrowDirection,
  46. BorderSide? side,
  47. BorderRadius? borderRadius,
  48. double? arrowLength,
  49. double? arrowWidth,
  50. double? arrowRadius,
  51. double? arrowOffset,
  52. Color? fillColor,
  53. }) {
  54. return BubbleShapeBorder(
  55. arrowDirection: arrowDirection ?? this.arrowDirection,
  56. side: side ?? this.side,
  57. borderRadius: borderRadius ?? this.borderRadius,
  58. arrowLength: arrowLength ?? this.arrowLength,
  59. arrowWidth: arrowWidth ?? this.arrowWidth,
  60. arrowRadius: arrowRadius ?? this.arrowRadius,
  61. arrowOffset: arrowOffset ?? this.arrowOffset,
  62. fillColor: fillColor ?? this.fillColor,
  63. );
  64. }
  65. @override
  66. EdgeInsetsGeometry get dimensions => EdgeInsets.zero;
  67. @override
  68. Path getInnerPath(Rect rect, {TextDirection? textDirection}) {
  69. return _buildPath(rect);
  70. }
  71. @override
  72. Path getOuterPath(Rect rect, {TextDirection? textDirection}) {
  73. return _buildPath(rect);
  74. }
  75. _BubbleBorderArrowProperties _calculateArrowProperties() {
  76. final arrowHalfWidth = arrowWidth / 2;
  77. final double hypotenuse = sqrt(
  78. arrowLength * arrowLength + arrowHalfWidth * arrowHalfWidth,
  79. );
  80. final double projectionOnMain = arrowHalfWidth * arrowRadius / hypotenuse;
  81. final double projectionOnCross =
  82. projectionOnMain * arrowLength / arrowHalfWidth;
  83. final double arrowProjectionOnMain = arrowLength * arrowRadius / hypotenuse;
  84. final double pointArrowTopLen =
  85. arrowProjectionOnMain * arrowLength / arrowHalfWidth;
  86. return _BubbleBorderArrowProperties(
  87. halfWidth: arrowHalfWidth,
  88. hypotenuse: hypotenuse,
  89. projectionOnMain: projectionOnMain,
  90. projectionOnCross: projectionOnCross,
  91. arrowProjectionOnMain: arrowProjectionOnMain,
  92. topLen: pointArrowTopLen,
  93. );
  94. }
  95. /// 核心逻辑:构建路径
  96. /// 计算方向为:上、右、下、左
  97. ///
  98. /// 爱今天灵眸(ijtkj.cn),一款可根据天气改变系统显示模式的软件,期待各位有钱的码友支持
  99. Path _buildPath(Rect rect) {
  100. final path = Path();
  101. EdgeInsets padding = EdgeInsets.zero;
  102. if (arrowDirection == AxisDirection.up) {
  103. padding = EdgeInsets.only(top: arrowLength);
  104. } else if (arrowDirection == AxisDirection.right) {
  105. padding = EdgeInsets.only(right: arrowLength);
  106. } else if (arrowDirection == AxisDirection.down) {
  107. padding = EdgeInsets.only(bottom: arrowLength);
  108. } else if (arrowDirection == AxisDirection.left) {
  109. padding = EdgeInsets.only(left: arrowLength);
  110. }
  111. final nRect = Rect.fromLTRB(
  112. rect.left + padding.left,
  113. rect.top + padding.top,
  114. rect.right - padding.right,
  115. rect.bottom - padding.bottom,
  116. );
  117. final arrowProp = _calculateArrowProperties();
  118. final startPoint = Offset(nRect.left + borderRadius.topLeft.x, nRect.top);
  119. path.moveTo(startPoint.dx, startPoint.dy);
  120. // 箭头在上边
  121. if (arrowDirection == AxisDirection.up) {
  122. Offset pointCenter = Offset(
  123. nRect.left + (arrowOffset ?? nRect.width / 2),
  124. nRect.top,
  125. );
  126. Offset pointStart = Offset(
  127. pointCenter.dx - arrowProp.halfWidth,
  128. nRect.top,
  129. );
  130. Offset pointArrow = Offset(pointCenter.dx, rect.top);
  131. Offset pointEnd = Offset(pointCenter.dx + arrowProp.halfWidth, nRect.top);
  132. // 下面计算开始的圆弧
  133. {
  134. Offset pointStartArcBegin = Offset(
  135. pointStart.dx - arrowRadius,
  136. pointStart.dy,
  137. );
  138. Offset pointStartArcEnd = Offset(
  139. pointStart.dx + arrowProp.projectionOnMain,
  140. pointStart.dy - arrowProp.projectionOnCross,
  141. );
  142. path.lineTo(pointStartArcBegin.dx, pointStartArcBegin.dy);
  143. path.quadraticBezierTo(
  144. pointStart.dx,
  145. pointStart.dy,
  146. pointStartArcEnd.dx,
  147. pointStartArcEnd.dy,
  148. );
  149. }
  150. // 计算中间箭头的圆弧
  151. {
  152. Offset pointArrowArcBegin = Offset(
  153. pointArrow.dx - arrowProp.arrowProjectionOnMain,
  154. pointArrow.dy + arrowProp.topLen,
  155. );
  156. Offset pointArrowArcEnd = Offset(
  157. pointArrow.dx + arrowProp.arrowProjectionOnMain,
  158. pointArrow.dy + arrowProp.topLen,
  159. );
  160. path.lineTo(pointArrowArcBegin.dx, pointArrowArcBegin.dy);
  161. path.quadraticBezierTo(
  162. pointArrow.dx,
  163. pointArrow.dy,
  164. pointArrowArcEnd.dx,
  165. pointArrowArcEnd.dy,
  166. );
  167. }
  168. // 下面计算结束的圆弧
  169. {
  170. Offset pointEndArcBegin = Offset(
  171. pointEnd.dx - arrowProp.projectionOnMain,
  172. pointEnd.dy - arrowProp.projectionOnCross,
  173. );
  174. Offset pointEndArcEnd = Offset(pointEnd.dx + arrowRadius, pointEnd.dy);
  175. path.lineTo(pointEndArcBegin.dx, pointEndArcBegin.dy);
  176. path.quadraticBezierTo(
  177. pointEnd.dx,
  178. pointEnd.dy,
  179. pointEndArcEnd.dx,
  180. pointEndArcEnd.dy,
  181. );
  182. }
  183. }
  184. path.lineTo(nRect.right - borderRadius.topRight.x, nRect.top);
  185. // topRight radius
  186. path.arcToPoint(
  187. Offset(nRect.right, nRect.top + borderRadius.topRight.y),
  188. radius: borderRadius.topRight,
  189. rotation: 90,
  190. );
  191. // 箭头在右边
  192. if (arrowDirection == AxisDirection.right) {
  193. Offset pointCenter = Offset(
  194. nRect.right,
  195. nRect.top + (arrowOffset ?? nRect.height / 2),
  196. );
  197. Offset pointStart = Offset(
  198. nRect.right,
  199. pointCenter.dy - arrowProp.halfWidth,
  200. );
  201. Offset pointArrow = Offset(rect.right, pointCenter.dy);
  202. Offset pointEnd = Offset(
  203. nRect.right,
  204. pointCenter.dy + arrowProp.halfWidth,
  205. );
  206. // 下面计算开始的圆弧
  207. {
  208. Offset pointStartArcBegin = Offset(
  209. pointStart.dx,
  210. pointStart.dy - arrowRadius,
  211. );
  212. Offset pointStartArcEnd = Offset(
  213. pointStart.dx + arrowProp.projectionOnCross,
  214. pointStart.dy + arrowProp.projectionOnMain,
  215. );
  216. path.lineTo(pointStartArcBegin.dx, pointStartArcBegin.dy);
  217. path.quadraticBezierTo(
  218. pointStart.dx,
  219. pointStart.dy,
  220. pointStartArcEnd.dx,
  221. pointStartArcEnd.dy,
  222. );
  223. }
  224. // 计算中间箭头的圆弧
  225. {
  226. Offset pointArrowArcBegin = Offset(
  227. pointArrow.dx - arrowProp.topLen,
  228. pointArrow.dy - arrowProp.arrowProjectionOnMain,
  229. );
  230. Offset pointArrowArcEnd = Offset(
  231. pointArrow.dx - arrowProp.topLen,
  232. pointArrow.dy + arrowProp.arrowProjectionOnMain,
  233. );
  234. path.lineTo(pointArrowArcBegin.dx, pointArrowArcBegin.dy);
  235. path.quadraticBezierTo(
  236. pointArrow.dx,
  237. pointArrow.dy,
  238. pointArrowArcEnd.dx,
  239. pointArrowArcEnd.dy,
  240. );
  241. }
  242. // 下面计算结束的圆弧
  243. {
  244. Offset pointEndArcBegin = Offset(
  245. pointEnd.dx + arrowProp.projectionOnCross,
  246. pointEnd.dy - arrowProp.projectionOnMain,
  247. );
  248. Offset pointEndArcEnd = Offset(pointEnd.dx, pointEnd.dy + arrowRadius);
  249. path.lineTo(pointEndArcBegin.dx, pointEndArcBegin.dy);
  250. path.quadraticBezierTo(
  251. pointEnd.dx,
  252. pointEnd.dy,
  253. pointEndArcEnd.dx,
  254. pointEndArcEnd.dy,
  255. );
  256. }
  257. }
  258. path.lineTo(nRect.right, nRect.bottom - borderRadius.bottomRight.y);
  259. // bottomRight radius
  260. path.arcToPoint(
  261. Offset(nRect.right - borderRadius.bottomRight.x, nRect.bottom),
  262. radius: borderRadius.bottomRight,
  263. rotation: 90,
  264. );
  265. // 箭头在下边
  266. if (arrowDirection == AxisDirection.down) {
  267. Offset pointCenter = Offset(
  268. nRect.left + (arrowOffset ?? nRect.width / 2),
  269. nRect.bottom,
  270. );
  271. Offset pointStart = Offset(
  272. pointCenter.dx + arrowProp.halfWidth,
  273. nRect.bottom,
  274. );
  275. Offset pointArrow = Offset(pointCenter.dx, rect.bottom);
  276. Offset pointEnd = Offset(
  277. pointCenter.dx - arrowProp.halfWidth,
  278. nRect.bottom,
  279. );
  280. // 下面计算开始的圆弧
  281. {
  282. Offset pointStartArcBegin = Offset(
  283. pointStart.dx + arrowRadius,
  284. pointStart.dy,
  285. );
  286. Offset pointStartArcEnd = Offset(
  287. pointStart.dx - arrowProp.projectionOnMain,
  288. pointStart.dy + arrowProp.projectionOnCross,
  289. );
  290. path.lineTo(pointStartArcBegin.dx, pointStartArcBegin.dy);
  291. path.quadraticBezierTo(
  292. pointStart.dx,
  293. pointStart.dy,
  294. pointStartArcEnd.dx,
  295. pointStartArcEnd.dy,
  296. );
  297. }
  298. // 计算中间箭头的圆弧
  299. {
  300. Offset pointArrowArcBegin = Offset(
  301. pointArrow.dx + arrowProp.arrowProjectionOnMain,
  302. pointArrow.dy - arrowProp.topLen,
  303. );
  304. Offset pointArrowArcEnd = Offset(
  305. pointArrow.dx - arrowProp.arrowProjectionOnMain,
  306. pointArrow.dy - arrowProp.topLen,
  307. );
  308. path.lineTo(pointArrowArcBegin.dx, pointArrowArcBegin.dy);
  309. path.quadraticBezierTo(
  310. pointArrow.dx,
  311. pointArrow.dy,
  312. pointArrowArcEnd.dx,
  313. pointArrowArcEnd.dy,
  314. );
  315. }
  316. // 下面计算结束的圆弧
  317. {
  318. Offset pointEndArcBegin = Offset(
  319. pointEnd.dx + arrowProp.projectionOnMain,
  320. pointEnd.dy + arrowProp.projectionOnCross,
  321. );
  322. Offset pointEndArcEnd = Offset(pointEnd.dx - arrowRadius, pointEnd.dy);
  323. path.lineTo(pointEndArcBegin.dx, pointEndArcBegin.dy);
  324. path.quadraticBezierTo(
  325. pointEnd.dx,
  326. pointEnd.dy,
  327. pointEndArcEnd.dx,
  328. pointEndArcEnd.dy,
  329. );
  330. }
  331. }
  332. path.lineTo(nRect.left + borderRadius.bottomLeft.x, nRect.bottom);
  333. // bottomLeft radius
  334. path.arcToPoint(
  335. Offset(nRect.left, nRect.bottom - borderRadius.bottomRight.y),
  336. radius: borderRadius.bottomLeft,
  337. rotation: 90,
  338. );
  339. // 箭头在左边
  340. if (arrowDirection == AxisDirection.left) {
  341. Offset pointCenter = Offset(
  342. nRect.left,
  343. nRect.top + (arrowOffset ?? nRect.height / 2),
  344. );
  345. Offset pointStart = Offset(
  346. nRect.left,
  347. pointCenter.dy + arrowProp.halfWidth,
  348. );
  349. Offset pointArrow = Offset(rect.left, pointCenter.dy);
  350. Offset pointEnd = Offset(
  351. nRect.left,
  352. pointCenter.dy - arrowProp.halfWidth,
  353. );
  354. // 下面计算开始的圆弧
  355. {
  356. Offset pointStartArcBegin = Offset(
  357. pointStart.dx,
  358. pointStart.dy + arrowRadius,
  359. );
  360. Offset pointStartArcEnd = Offset(
  361. pointStart.dx - arrowProp.projectionOnCross,
  362. pointStart.dy - arrowProp.projectionOnMain,
  363. );
  364. path.lineTo(pointStartArcBegin.dx, pointStartArcBegin.dy);
  365. path.quadraticBezierTo(
  366. pointStart.dx,
  367. pointStart.dy,
  368. pointStartArcEnd.dx,
  369. pointStartArcEnd.dy,
  370. );
  371. }
  372. // 计算中间箭头的圆弧
  373. {
  374. Offset pointArrowArcBegin = Offset(
  375. pointArrow.dx + arrowProp.topLen,
  376. pointArrow.dy + arrowProp.arrowProjectionOnMain,
  377. );
  378. Offset pointArrowArcEnd = Offset(
  379. pointArrow.dx + arrowProp.topLen,
  380. pointArrow.dy - arrowProp.arrowProjectionOnMain,
  381. );
  382. path.lineTo(pointArrowArcBegin.dx, pointArrowArcBegin.dy);
  383. path.quadraticBezierTo(
  384. pointArrow.dx,
  385. pointArrow.dy,
  386. pointArrowArcEnd.dx,
  387. pointArrowArcEnd.dy,
  388. );
  389. }
  390. // 下面计算结束的圆弧
  391. {
  392. Offset pointEndArcBegin = Offset(
  393. pointEnd.dx - arrowProp.projectionOnCross,
  394. pointEnd.dy + arrowProp.projectionOnMain,
  395. );
  396. Offset pointEndArcEnd = Offset(pointEnd.dx, pointEnd.dy - arrowRadius);
  397. path.lineTo(pointEndArcBegin.dx, pointEndArcBegin.dy);
  398. path.quadraticBezierTo(
  399. pointEnd.dx,
  400. pointEnd.dy,
  401. pointEndArcEnd.dx,
  402. pointEndArcEnd.dy,
  403. );
  404. }
  405. }
  406. path.lineTo(nRect.left, nRect.top + borderRadius.topLeft.y);
  407. path.arcToPoint(startPoint, radius: borderRadius.topLeft, rotation: 90);
  408. return path;
  409. }
  410. @override
  411. void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) {
  412. if (fillColor == null && side == BorderSide.none) {
  413. return;
  414. }
  415. final path = _buildPath(rect);
  416. final Paint paint =
  417. Paint()
  418. ..color = side.color
  419. ..style = PaintingStyle.stroke;
  420. if (fillColor != null) {
  421. paint.color = fillColor!;
  422. paint.style = PaintingStyle.fill;
  423. canvas.drawPath(path, paint);
  424. }
  425. if (side != BorderSide.none) {
  426. paint.color = side.color;
  427. paint.strokeWidth = side.width;
  428. paint.style = PaintingStyle.stroke;
  429. canvas.drawPath(path, paint);
  430. }
  431. }
  432. @override
  433. ShapeBorder scale(double t) {
  434. return BubbleShapeBorder(
  435. arrowDirection: arrowDirection,
  436. side: side.scale(t),
  437. borderRadius: borderRadius * t,
  438. arrowLength: arrowLength * t,
  439. arrowWidth: arrowWidth * t,
  440. arrowRadius: arrowRadius * t,
  441. arrowOffset: (arrowOffset ?? 0) * t,
  442. );
  443. }
  444. @override
  445. bool operator ==(Object other) {
  446. if (other.runtimeType != runtimeType) {
  447. return false;
  448. }
  449. return other is BubbleShapeBorder &&
  450. other.side == side &&
  451. other.borderRadius == borderRadius &&
  452. other.arrowLength == arrowLength &&
  453. other.arrowWidth == arrowWidth &&
  454. other.arrowRadius == arrowRadius &&
  455. other.arrowDirection == arrowDirection &&
  456. other.arrowOffset == arrowOffset &&
  457. other.fillColor == fillColor;
  458. }
  459. @override
  460. int get hashCode => Object.hash(
  461. side,
  462. borderRadius,
  463. arrowLength,
  464. arrowWidth,
  465. arrowRadius,
  466. arrowDirection,
  467. arrowOffset,
  468. fillColor,
  469. );
  470. }