fsuper.dart 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079
  1. /// Copyright 1999-2020 Fliggy Android Team <alitrip_android@list.alibaba-inc.com>.
  2. ///
  3. /// Licensed under the Apache License, Version 2.0 (the "License");
  4. /// you may not use this file except in compliance with the License.
  5. /// You may obtain a copy of the License at following link.
  6. ///
  7. /// http://www.apache.org/licenses/LICENSE-2.0
  8. ///
  9. /// Unless required by applicable law or agreed to in writing, software
  10. /// distributed under the License is distributed on an "AS IS" BASIS,
  11. /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. /// See the License for the specific language governing permissions and
  13. /// limitations under the License.
  14. export 'fdefine.dart';
  15. import 'package:flutter/material.dart';
  16. import 'package:flutter/rendering.dart';
  17. import 'package:flutter/scheduler.dart';
  18. import 'dart:math' as math;
  19. import 'fdefine.dart';
  20. import 'fcontrol.dart';
  21. /// FSuper 是一个强大的组件,能够支持富文本、圆角、边框、图片、小红点、以及同时设置多达两个子组件,且能够通过相对位置控制它们。
  22. /// FSuper 能够帮助开发者快速舒适的构建复杂视图。
  23. ///
  24. /// FSuper is a powerful component that can support rich text, rounded corners, borders, pictures, small red dots, and set up to two sub-components at the same time, and control their relative positions.
  25. /// FSuper can help developers build complex views quickly and comfortably.
  26. // ignore: must_be_immutable
  27. class FSuper extends StatefulWidget {
  28. /// 宽。null 将会自适应文字大小。设置 double.infinity 将会充满父容器。
  29. ///
  30. /// Width,null will adapt the text size. Setting double.infinity will fill the parent container.
  31. final double width;
  32. /// 宽。null 将会自适应文字大小。
  33. ///
  34. /// Height, null will adapt the text size.
  35. final double height;
  36. /// 组件最大宽度。
  37. ///
  38. /// Maximum component width.
  39. final double maxWidth;
  40. /// 组件最大高度
  41. ///
  42. /// Maximum component height
  43. final double maxHeight;
  44. /// 文字内容
  45. ///
  46. /// Text content
  47. final String text;
  48. /// Widget 文本样式
  49. ///
  50. /// Widget text style
  51. final TextStyle style;
  52. /// 文字整体相对位置。详见 [Alignment]
  53. ///
  54. /// The overall relative position of the text. See [Alignment] for details
  55. final Alignment textAlignment;
  56. /// 文字对齐方式。详见 [Align]
  57. ///
  58. /// Text alignment. See [Align] for details
  59. final TextAlign textAlign;
  60. /// 富文本内容。详见 [TextSpan] 。
  61. /// [TextSpan] 元素默认会使用 [textColor],[textSize],[textStyle],[textWeight] 的配置。
  62. ///
  63. /// Rich text content. See [TextSpan] for more details.
  64. /// The [TextSpan] element uses the configuration of [textColor], [textSize], [textStyle], [textWeight] by default.
  65. final List<TextSpan> spans;
  66. /// 点击监听回调
  67. ///
  68. /// Click listen callback
  69. final GestureTapCallback onClick;
  70. /// 组件背景颜色。
  71. ///
  72. /// Component background color
  73. final Color backgroundColor;
  74. /// 设置组件渐变色背景。会覆盖 [backgroundColor]
  75. /// 你可选择 [LinearGradient],[RadialGradient],[SweepGradient] 等..
  76. ///
  77. /// Sets the gradient background of the component. [BackgroundColor]
  78. /// You can choose [LinearGradient], [RadialGradient], [SweepGradient], etc ..
  79. final Gradient gradient;
  80. /// 组件背景图片,会覆盖 [backgroundColor] 和 [gradient]。详见 [ImageProvider]
  81. ///
  82. /// Component background image, overwrites [backgroundColor] and [gradient].
  83. /// See [ImageProvider] for details
  84. final ImageProvider backgroundImage;
  85. /// 设置组件圆角。详见 [FCorner]
  86. ///
  87. /// Sets the component fillet. See [FCorner] for details
  88. final FCorner corner;
  89. /// 设置圆角风格,默认 [FCornerStyle.round]
  90. ///
  91. /// Set rounded corner style, default [FCornerStyle.round]
  92. final FCornerStyle cornerStyle;
  93. /// 设置组件内间距,只影响文字。通过该属性可以设置文字和 [FSuper.child1]、[FSuper.child2] 的间距。
  94. ///
  95. /// Sets the spacing between components, affecting only text.
  96. /// This property can set the space between the text and [FSuper.child1], [FSuper.child2].
  97. final EdgeInsetsGeometry padding;
  98. /// 设置组件与父容器边缘的间距。详见 [EdgeInsets]
  99. ///
  100. /// Sets the distance between the component and the edge of the parent container. See [EdgeInsets] for details
  101. final EdgeInsets margin;
  102. /// 子组件。子组件允许是任意类型的 Widget。
  103. /// 通过 [child1Alignment] 和 [child1Margin] 可以控制它在组件中的位置。
  104. ///
  105. /// Subassembly. Child components are allowed to be any type of widget.
  106. /// [Child1Alignment] and [child1Margin] can control its position in the component.
  107. final Widget child1;
  108. /// 指定 [child1] 在组件中的相对位置
  109. ///
  110. /// Specify the relative position of the child component in the component
  111. final Alignment child1Alignment;
  112. /// 基于 [child1] 的相对位置,为它设置偏移
  113. ///
  114. /// Set an offset for the child component based on its relative position
  115. final EdgeInsets child1Margin;
  116. /// 为 [child1] 设置点击监听器
  117. ///
  118. /// Set click listener for [child1]
  119. final GestureTapCallback onChild1Click;
  120. /// 子组件。子组件允许是任意类型的 Widget。
  121. /// 通过 [child1Alignment] 和 [child1Margin] 可以控制它在组件中的位置。
  122. ///
  123. /// Subassembly. Child components are allowed to be any type of widget.
  124. /// [Child1Alignment] and [child1Margin] can control its position in the component.
  125. final Widget child2;
  126. /// 指定 [child2] 在组件中的相对位置
  127. ///
  128. /// Specify the relative position of the child component in the component
  129. final Alignment child2Alignment;
  130. /// 基于 [child2] 的相对位置,为它设置偏移
  131. ///
  132. /// Set an offset for the child component based on its relative position
  133. final EdgeInsets child2Margin;
  134. /// 为 [child2] 设置点击监听器
  135. ///
  136. /// Set click listener for [child2]
  137. final GestureTapCallback onChild2Click;
  138. /// 是否显示小红点。默认不展示。
  139. ///
  140. /// Whether to show the Red Point.Not displayed by default.
  141. final bool redPoint;
  142. /// 小红点颜色。默认 [Colors.redAccent]
  143. ///
  144. /// Red Point color. Default [Colors.redAccent]
  145. final Color redPointColor;
  146. /// 设置 Red Point 大小。Red Point 的 宽=高
  147. ///
  148. /// Set the Red Point size. Red Point width = height
  149. final double redPointSize;
  150. /// 设置 Red Point 中的文字。比如消息数量..
  151. ///
  152. /// Set the text in Red Point. Like number of messages ...
  153. final String redPointText;
  154. /// 小红点文本样式
  155. ///
  156. /// red point text style
  157. final TextStyle redPointTextStyle;
  158. /// 基于组件右上角设置 Red Point 的位置偏移。
  159. /// 默认 Red Point 会向右上方偏移 [redPointSize] / 2
  160. ///
  161. /// Sets the position of the Red Point based on the upper right corner of the component.
  162. /// The default Red Point is shifted to the upper right [redPointSize] / 2
  163. final Offset redPointOffset;
  164. /// 设置边框颜色。
  165. ///
  166. /// Set the border color.
  167. final Color strokeColor;
  168. /// 设置边框宽
  169. ///
  170. /// Set border width
  171. final double strokeWidth;
  172. /// 设置组件阴影颜色
  173. ///
  174. /// Set component shadow color
  175. final Color shadowColor;
  176. /// 设置组件阴影偏移
  177. ///
  178. /// Set component shadow offset
  179. final Offset shadowOffset;
  180. /// 设置组件高斯与阴影形状卷积的标准偏差。
  181. ///
  182. /// Sets the standard deviation of the component's Gaussian convolution with the shadow shape.
  183. final double shadowBlur;
  184. /// 是否支持 Neumorphism 风格。开启该项 [highlightColor] 将会失效
  185. ///
  186. /// Whether to support the Neumorphism style. Open this item [highlightColor] will be invalid
  187. final bool isSupportNeumorphism;
  188. /// 当 [isSupportNeumorphism] 为 true 时有效。光源方向,分为左上、左下、右上、右下四个方向。用来控制光源照射方向,会影响高亮方向和阴影方向
  189. ///
  190. /// Valid when [isSupportNeumorphism] is true. The direction of the light source is divided into four directions: upper left, lower left, upper right, and lower right. Used to control the illumination direction of the light source, which will affect the highlight direction and shadow direction
  191. final FLightOrientation lightOrientation;
  192. /// 开启 Neumorphism 风格后,亮部阴影颜色
  193. ///
  194. /// After the Neumorphism style is turned on, the bright shadow color
  195. final Color highlightShadowColor;
  196. /// 开启 Neumorphism 风格后,是否呈浮起效果,否则为凹陷效果,默认为 true
  197. ///
  198. /// Whether the Neumorphism style is turned on, whether it is a floating effect, otherwise it is a concave effect, the default is true
  199. final bool float;
  200. FSuper({
  201. this.width,
  202. this.height,
  203. this.maxWidth,
  204. this.maxHeight,
  205. this.text,
  206. this.textAlignment,
  207. this.textAlign,
  208. this.spans,
  209. this.onClick,
  210. this.backgroundColor,
  211. this.backgroundImage,
  212. this.child1,
  213. this.child1Alignment,
  214. this.child1Margin,
  215. this.onChild1Click,
  216. this.child2,
  217. this.child2Alignment,
  218. this.child2Margin,
  219. this.onChild2Click,
  220. this.redPoint = false,
  221. this.redPointColor = Colors.redAccent,
  222. this.redPointSize = 20,
  223. this.redPointOffset,
  224. this.redPointText,
  225. this.redPointTextStyle = const TextStyle(color: Colors.white, fontSize: 11),
  226. this.gradient,
  227. this.padding,
  228. this.corner,
  229. this.cornerStyle = FCornerStyle.round,
  230. this.strokeColor,
  231. this.strokeWidth,
  232. this.shadowColor,
  233. this.shadowOffset,
  234. this.shadowBlur = 1,
  235. this.margin,
  236. this.style,
  237. this.isSupportNeumorphism = false,
  238. this.highlightShadowColor,
  239. this.lightOrientation,
  240. this.float = true,
  241. });
  242. @override
  243. State<StatefulWidget> createState() {
  244. return _FSuperState();
  245. }
  246. /// 在富文本 [spans] 中插入垂直方向的文字间距
  247. ///
  248. /// Insert vertical spacing in rich text [spans]
  249. static TextSpan VerticalSpace(double space) {
  250. return TextSpan(
  251. text: '\n \n',
  252. style: TextStyle(
  253. fontSize: 1,
  254. height: space,
  255. ));
  256. }
  257. /// 在富文本 [spans] 中插入水平方向的文字间距
  258. ///
  259. /// Insert horizontal spacing in rich text [spans]
  260. static TextSpan HorizontalSpace(double space) {
  261. return TextSpan(
  262. text: " ",
  263. style: TextStyle(
  264. fontSize: 0.1,
  265. wordSpacing: (space - 1),
  266. ));
  267. }
  268. }
  269. class _FSuperState extends State<FSuper> {
  270. GlobalKey rootKey = GlobalKey();
  271. Size containerSize;
  272. /// [FSuper] 在初始化时会注册一帧回调,当组件首次测量完成后,将会根据现有尺寸根据再次确定尺寸配置。
  273. /// 接着会确定 [FSuper.child1],[FSuper.child2] 的位置。
  274. /// 最后会再次触发绘制。
  275. ///
  276. /// [FSuper] A frame callback is registered during initialization.
  277. /// After the component's first measurement is completed, the size configuration will be determined again based on the existing size.
  278. /// Then the location of [FSuper.child1], [FSuper.child2] will be determined.
  279. /// Finally, the drawing will be triggered again.
  280. @override
  281. void initState() {
  282. WidgetsBinding.instance.addPostFrameCallback(_handleSizeChanged);
  283. super.initState();
  284. }
  285. void _handleSizeChanged(duration) {
  286. if (!mounted) return;
  287. RenderBox rootBox = rootKey.currentContext.findRenderObject() as RenderBox;
  288. if (rootBox != null && containerSize != rootBox.size) {
  289. setState(() {
  290. containerSize = rootBox.size;
  291. });
  292. }
  293. WidgetsBinding.instance.addPostFrameCallback(_handleSizeChanged);
  294. }
  295. @override
  296. Widget build(BuildContext context) {
  297. BorderRadius borderRadius = widget.corner == null
  298. ? BorderRadius.all(Radius.circular(0))
  299. : BorderRadius.only(
  300. topLeft: Radius.circular(widget.corner.leftTopCorner),
  301. topRight: Radius.circular(widget.corner.rightTopCorner),
  302. bottomRight: Radius.circular(widget.corner.rightBottomCorner),
  303. bottomLeft: Radius.circular(widget.corner.leftBottomCorner),
  304. );
  305. var sideColor = widget.strokeColor ?? Colors.transparent;
  306. var borderSide = BorderSide(
  307. width: widget.strokeWidth ?? 0,
  308. color: sideColor,
  309. style: BorderStyle.solid,
  310. );
  311. var shape = widget.cornerStyle == FCornerStyle.round
  312. ? RoundedRectangleBorder(borderRadius: borderRadius)
  313. : BeveledRectangleBorder(borderRadius: borderRadius);
  314. /// shape
  315. FShape fShape = FShape(
  316. borderShape: widget.cornerStyle == FCornerStyle.round
  317. ? FBorderShape.RoundedRectangle
  318. : FBorderShape.BeveledRectangle,
  319. side: borderSide,
  320. borderRadius: borderRadius,
  321. );
  322. var decorationImage = widget.backgroundImage != null
  323. ? DecorationImage(
  324. fit: BoxFit.cover,
  325. image: widget.backgroundImage,
  326. )
  327. : null;
  328. var decoration = decorationImage != null
  329. ? ShapeDecoration(image: decorationImage, shape: shape)
  330. : null;
  331. var textPart = Text.rich(
  332. TextSpan(text: widget.text, children: widget.spans),
  333. textAlign: widget.textAlign ?? TextAlign.center,
  334. style: widget.style,
  335. );
  336. List<Widget> children = [];
  337. /// Build base container
  338. Widget containerPart = FControl(
  339. key: rootKey,
  340. lightOrientation: widget.lightOrientation,
  341. width: widget.width,
  342. height: widget.height,
  343. shape: fShape,
  344. gradient: widget.gradient,
  345. supportDropShadow:
  346. (widget.shadowColor != null && widget.shadowBlur != 0.0) ||
  347. widget.isSupportNeumorphism,
  348. dropShadow: FShadow(
  349. highlightColor: widget.isSupportNeumorphism
  350. ? widget.highlightShadowColor ?? Colors.white.withOpacity(0.83)
  351. : Colors.transparent,
  352. highlightBlur: widget.isSupportNeumorphism ? _shadowBlur : 0.0,
  353. highlightDistance: shadowDistance,
  354. shadowColor: widget.shadowColor ?? Color(0xffd1d9e6),
  355. shadowBlur: _shadowBlur,
  356. shadowDistance: shadowDistance,
  357. shadowOffset: widget.shadowOffset,
  358. ),
  359. supportInnerShadow: widget.isSupportNeumorphism,
  360. innerShadow: FShadow(
  361. highlightColor:
  362. widget.highlightShadowColor ?? Colors.white.withOpacity(0.83),
  363. highlightBlur: _shadowBlur,
  364. highlightDistance: shadowDistance,
  365. shadowColor: widget.shadowColor ?? Color(0xffd1d9e6),
  366. shadowBlur: _shadowBlur,
  367. shadowDistance: shadowDistance,
  368. ),
  369. appearance: widget.isSupportNeumorphism
  370. ? FAppearance.Neumorphism
  371. : FAppearance.Material,
  372. colorForCallback: (sender, state) {
  373. return widget.backgroundColor ?? Colors.transparent;
  374. },
  375. userInteractive: false,
  376. isSelected: !widget.float,
  377. controlType: FType.Toggle,
  378. maskColor: Colors.transparent,
  379. child: Container(
  380. alignment: widget.textAlignment,
  381. decoration: decoration,
  382. padding: widget.padding,
  383. child: textPart,
  384. ),
  385. );
  386. if (widget.maxWidth != null || widget.maxHeight != null) {
  387. containerPart = LimitedBox(
  388. maxWidth: widget.maxWidth ?? double.infinity,
  389. maxHeight: widget.maxHeight ?? double.infinity,
  390. child: Container(
  391. width: widget.maxWidth ?? double.infinity,
  392. child: containerPart),
  393. );
  394. }
  395. children.add(Container(
  396. margin: widget.margin,
  397. child: GestureDetector(
  398. onTap: widget.onClick,
  399. child: containerPart,
  400. ),
  401. ));
  402. /// if getted [containerSize], [child1] or [child2] not null,build child part
  403. if (containerSize != null) {
  404. if (widget.child1 != null || widget.child2 != null) {
  405. children.addAll(buildChildPart());
  406. }
  407. }
  408. /// build red point
  409. if (widget.redPoint) {
  410. var redPointPart = buildRedPoint();
  411. children.add(redPointPart);
  412. }
  413. /// build up
  414. var stackPart = _Stack(
  415. overflow: Overflow.visible,
  416. children: children,
  417. );
  418. return stackPart;
  419. }
  420. double get _shadowBlur {
  421. if ((widget.shadowBlur == null || widget.shadowBlur == 0.0) &&
  422. widget.isSupportNeumorphism) {
  423. return 6.0;
  424. } else {
  425. return widget.shadowBlur;
  426. }
  427. }
  428. double get shadowDistance {
  429. if (widget.isSupportNeumorphism) {
  430. return widget.shadowOffset?.dy ?? 3.0;
  431. } else {
  432. return widget.shadowOffset?.dy ?? 0.0;
  433. }
  434. }
  435. Size child1Size = Size.zero;
  436. Size child2Size = Size.zero;
  437. List<Widget> buildChildPart() {
  438. List<Widget> children = [];
  439. if (widget.child1 != null) {
  440. Offset offset = computeChildOffset(
  441. widget.child1Alignment, child1Size, widget.child1Margin);
  442. children.add(_MeasureSize(
  443. onChange: (size) {
  444. setState(() {
  445. child1Size = size;
  446. });
  447. },
  448. child: Positioned(
  449. left: offset.dx,
  450. top: offset.dy,
  451. child: GestureDetector(
  452. /// 当子 Child 不处理事件时,作为整体,应该响应 FSuper 的事件
  453. ///
  454. /// When Child does not handle events, as a whole, it should respond to FSuper events
  455. onTap: widget.onChild1Click ?? widget.onClick,
  456. child: widget.child1,
  457. ),
  458. )));
  459. }
  460. if (widget.child2 != null) {
  461. Offset offset = computeChildOffset(
  462. widget.child2Alignment, child2Size, widget.child2Margin);
  463. children.add(_MeasureSize(
  464. onChange: (size) {
  465. setState(() {
  466. child2Size = size;
  467. });
  468. },
  469. child: Positioned(
  470. left: offset.dx,
  471. top: offset.dy,
  472. child: GestureDetector(
  473. /// 当子 Child 不处理事件时,作为整体,应该响应 FSuper 的事件
  474. ///
  475. /// When Child does not handle events, as a whole, it should respond to FSuper events
  476. onTap: widget.onChild2Click ?? widget.onClick,
  477. child: widget.child2,
  478. ),
  479. )));
  480. }
  481. return children;
  482. }
  483. Offset computeChildOffset(
  484. Alignment alignment, Size childSize, EdgeInsets childMargin) {
  485. Offset childOffset = Offset.zero;
  486. Size size = containerSize;
  487. if (alignment == Alignment.topLeft) {
  488. childOffset = Offset(0, 0);
  489. } else if (alignment == Alignment.topCenter) {
  490. childOffset = Offset(size.width / 2 - childSize.width / 2, 0);
  491. } else if (alignment == Alignment.topRight) {
  492. childOffset = Offset(size.width - childSize.width, 0);
  493. } else if (alignment == Alignment.centerRight) {
  494. childOffset = Offset(
  495. size.width - childSize.width, size.height / 2 - childSize.height / 2);
  496. } else if (alignment == Alignment.bottomRight) {
  497. childOffset =
  498. Offset(size.width - childSize.width, size.height - childSize.height);
  499. } else if (alignment == Alignment.bottomCenter) {
  500. childOffset = Offset(
  501. size.width / 2 - childSize.width / 2, size.height - childSize.height);
  502. } else if (alignment == Alignment.bottomLeft) {
  503. childOffset = Offset(0, size.height - childSize.height);
  504. } else if (alignment == Alignment.centerLeft) {
  505. childOffset = Offset(0, size.height / 2 - childSize.height / 2);
  506. } else {
  507. childOffset = Offset(size.width / 2 - childSize.width / 2,
  508. size.height / 2 - childSize.height / 2);
  509. }
  510. if (childMargin != null) {
  511. childOffset = childOffset.translate(childMargin.left - childMargin.right,
  512. childMargin.top - childMargin.bottom);
  513. }
  514. if(widget.margin != null){
  515. childOffset = childOffset.translate(widget.margin.left, widget.margin.top);
  516. }
  517. return childOffset;
  518. }
  519. Widget buildRedPoint() {
  520. bool redPointTextEmpty =
  521. widget.redPointText == null || widget.redPointText == "";
  522. Color redPointTextColor = widget.redPointTextStyle?.color ?? Colors.white;
  523. double redPointTextSize =
  524. redPointTextEmpty ? 0.0 : (widget.redPointTextStyle?.fontSize ?? 0.0);
  525. var offset = widget.redPointOffset ??
  526. Offset(widget.redPointSize / 2.0, widget.redPointSize / 2.0);
  527. var redPointPart = Positioned(
  528. right: -offset.dx,
  529. top: -offset.dy,
  530. child: Container(
  531. padding: EdgeInsets.fromLTRB(
  532. redPointTextSize / 2.0, 0, redPointTextSize / 2.0, 0),
  533. constraints: BoxConstraints(
  534. minWidth: widget.redPointSize,
  535. maxHeight: widget.redPointSize,
  536. ),
  537. alignment: Alignment.center,
  538. decoration: BoxDecoration(
  539. color: widget.redPointColor,
  540. borderRadius:
  541. BorderRadius.all(Radius.circular(widget.redPointSize / 2.0)),
  542. ),
  543. child: redPointTextEmpty
  544. ? null
  545. : Text(
  546. widget.redPointText ?? "",
  547. style: widget.redPointTextStyle.copyWith(
  548. color: redPointTextColor, fontSize: redPointTextSize),
  549. ),
  550. ),
  551. );
  552. return redPointPart;
  553. }
  554. }
  555. typedef void _OnChildSizeChange(Size size);
  556. class _MeasureSize extends StatefulWidget {
  557. final Widget child;
  558. final _OnChildSizeChange onChange;
  559. const _MeasureSize({
  560. Key key,
  561. @required this.onChange,
  562. @required this.child,
  563. }) : super(key: key);
  564. @override
  565. _MeasureSizeState createState() => _MeasureSizeState();
  566. }
  567. class _MeasureSizeState extends State<_MeasureSize> {
  568. GlobalKey key = GlobalKey();
  569. Size oldSize = Size.zero;
  570. @override
  571. Widget build(BuildContext context) {
  572. SchedulerBinding.instance.addPostFrameCallback(postFrameCallback);
  573. return Container(
  574. key: key,
  575. child: widget.child,
  576. );
  577. }
  578. void postFrameCallback(_) {
  579. var context = key.currentContext;
  580. if (context == null) return;
  581. var newSize = context.size;
  582. if (oldSize == newSize) return;
  583. oldSize = newSize;
  584. widget.onChange(newSize);
  585. }
  586. }
  587. class _Stack extends MultiChildRenderObjectWidget {
  588. /// Creates a stack layout widget.
  589. ///
  590. /// By default, the non-positioned children of the stack are aligned by their
  591. /// top left corners.
  592. _Stack({
  593. Key key,
  594. this.alignment = AlignmentDirectional.topStart,
  595. this.textDirection,
  596. this.fit = StackFit.loose,
  597. this.overflow = Overflow.clip,
  598. List<Widget> children = const <Widget>[],
  599. }) : super(key: key, children: children);
  600. /// How to align the non-positioned and partially-positioned children in the
  601. /// stack.
  602. ///
  603. /// The non-positioned children are placed relative to each other such that
  604. /// the points determined by [alignment] are co-located. For example, if the
  605. /// [alignment] is [Alignment.topLeft], then the top left corner of
  606. /// each non-positioned child will be located at the same global coordinate.
  607. ///
  608. /// Partially-positioned children, those that do not specify an alignment in a
  609. /// particular axis (e.g. that have neither `top` nor `bottom` set), use the
  610. /// alignment to determine how they should be positioned in that
  611. /// under-specified axis.
  612. ///
  613. /// Defaults to [AlignmentDirectional.topStart].
  614. ///
  615. /// See also:
  616. ///
  617. /// * [Alignment], a class with convenient constants typically used to
  618. /// specify an [AlignmentGeometry].
  619. /// * [AlignmentDirectional], like [Alignment] for specifying alignments
  620. /// relative to text direction.
  621. final AlignmentGeometry alignment;
  622. /// The text direction with which to resolve [alignment].
  623. ///
  624. /// Defaults to the ambient [Directionality].
  625. final TextDirection textDirection;
  626. /// How to size the non-positioned children in the stack.
  627. ///
  628. /// The constraints passed into the [Stack] from its parent are either
  629. /// loosened ([StackFit.loose]) or tightened to their biggest size
  630. /// ([StackFit.expand]).
  631. final StackFit fit;
  632. /// Whether overflowing children should be clipped. See [Overflow].
  633. ///
  634. /// Some children in a stack might overflow its box. When this flag is set to
  635. /// [Overflow.clip], children cannot paint outside of the stack's box.
  636. final Overflow overflow;
  637. @override
  638. _RenderStack createRenderObject(BuildContext context) {
  639. return _RenderStack(
  640. alignment: alignment,
  641. textDirection: textDirection ?? Directionality.of(context),
  642. fit: fit,
  643. overflow: overflow,
  644. );
  645. }
  646. @override
  647. void updateRenderObject(BuildContext context, _RenderStack renderObject) {
  648. renderObject
  649. ..alignment = alignment
  650. ..textDirection = textDirection ?? Directionality.of(context)
  651. ..fit = fit
  652. ..overflow = overflow;
  653. }
  654. @override
  655. void debugFillProperties(DiagnosticPropertiesBuilder properties) {
  656. super.debugFillProperties(properties);
  657. properties
  658. .add(DiagnosticsProperty<AlignmentGeometry>('alignment', alignment));
  659. properties.add(EnumProperty<TextDirection>('textDirection', textDirection,
  660. defaultValue: null));
  661. properties.add(EnumProperty<StackFit>('fit', fit));
  662. properties.add(EnumProperty<Overflow>('overflow', overflow));
  663. }
  664. }
  665. class _RenderStack extends RenderBox
  666. with
  667. ContainerRenderObjectMixin<RenderBox, StackParentData>,
  668. RenderBoxContainerDefaultsMixin<RenderBox, StackParentData> {
  669. /// Creates a stack render object.
  670. ///
  671. /// By default, the non-positioned children of the stack are aligned by their
  672. /// top left corners.
  673. _RenderStack({
  674. List<RenderBox> children,
  675. AlignmentGeometry alignment = AlignmentDirectional.topStart,
  676. TextDirection textDirection,
  677. StackFit fit = StackFit.loose,
  678. Overflow overflow = Overflow.clip,
  679. }) : assert(alignment != null),
  680. assert(fit != null),
  681. assert(overflow != null),
  682. _alignment = alignment,
  683. _textDirection = textDirection,
  684. _fit = fit,
  685. _overflow = overflow {
  686. addAll(children);
  687. }
  688. bool _hasVisualOverflow = false;
  689. @override
  690. void setupParentData(RenderBox child) {
  691. if (child.parentData is! StackParentData)
  692. child.parentData = StackParentData();
  693. }
  694. Alignment _resolvedAlignment;
  695. void _resolve() {
  696. if (_resolvedAlignment != null) return;
  697. _resolvedAlignment = alignment.resolve(textDirection);
  698. }
  699. void _markNeedResolution() {
  700. _resolvedAlignment = null;
  701. markNeedsLayout();
  702. }
  703. /// How to align the non-positioned or partially-positioned children in the
  704. /// stack.
  705. ///
  706. /// The non-positioned children are placed relative to each other such that
  707. /// the points determined by [alignment] are co-located. For example, if the
  708. /// [alignment] is [Alignment.topLeft], then the top left corner of
  709. /// each non-positioned child will be located at the same global coordinate.
  710. ///
  711. /// Partially-positioned children, those that do not specify an alignment in a
  712. /// particular axis (e.g. that have neither `top` nor `bottom` set), use the
  713. /// alignment to determine how they should be positioned in that
  714. /// under-specified axis.
  715. ///
  716. /// If this is set to an [AlignmentDirectional] object, then [textDirection]
  717. /// must not be null.
  718. AlignmentGeometry get alignment => _alignment;
  719. AlignmentGeometry _alignment;
  720. set alignment(AlignmentGeometry value) {
  721. assert(value != null);
  722. if (_alignment == value) return;
  723. _alignment = value;
  724. _markNeedResolution();
  725. }
  726. /// The text direction with which to resolve [alignment].
  727. ///
  728. /// This may be changed to null, but only after the [alignment] has been changed
  729. /// to a value that does not depend on the direction.
  730. TextDirection get textDirection => _textDirection;
  731. TextDirection _textDirection;
  732. set textDirection(TextDirection value) {
  733. if (_textDirection == value) return;
  734. _textDirection = value;
  735. _markNeedResolution();
  736. }
  737. /// How to size the non-positioned children in the stack.
  738. ///
  739. /// The constraints passed into the [RenderStack] from its parent are either
  740. /// loosened ([StackFit.loose]) or tightened to their biggest size
  741. /// ([StackFit.expand]).
  742. StackFit get fit => _fit;
  743. StackFit _fit;
  744. set fit(StackFit value) {
  745. assert(value != null);
  746. if (_fit != value) {
  747. _fit = value;
  748. markNeedsLayout();
  749. }
  750. }
  751. /// Whether overflowing children should be clipped. See [Overflow].
  752. ///
  753. /// Some children in a stack might overflow its box. When this flag is set to
  754. /// [Overflow.clip], children cannot paint outside of the stack's box.
  755. Overflow get overflow => _overflow;
  756. Overflow _overflow;
  757. set overflow(Overflow value) {
  758. assert(value != null);
  759. if (_overflow != value) {
  760. _overflow = value;
  761. markNeedsPaint();
  762. }
  763. }
  764. /// Helper function for calculating the intrinsics metrics of a Stack.
  765. static double getIntrinsicDimension(
  766. RenderBox firstChild, double mainChildSizeGetter(RenderBox child)) {
  767. double extent = 0.0;
  768. RenderBox child = firstChild;
  769. while (child != null) {
  770. final StackParentData childParentData =
  771. child.parentData as StackParentData;
  772. if (!childParentData.isPositioned)
  773. extent = math.max(extent, mainChildSizeGetter(child));
  774. assert(child.parentData == childParentData);
  775. child = childParentData.nextSibling;
  776. }
  777. return extent;
  778. }
  779. @override
  780. double computeMinIntrinsicWidth(double height) {
  781. return getIntrinsicDimension(
  782. firstChild, (RenderBox child) => child.getMinIntrinsicWidth(height));
  783. }
  784. @override
  785. double computeMaxIntrinsicWidth(double height) {
  786. return getIntrinsicDimension(
  787. firstChild, (RenderBox child) => child.getMaxIntrinsicWidth(height));
  788. }
  789. @override
  790. double computeMinIntrinsicHeight(double width) {
  791. return getIntrinsicDimension(
  792. firstChild, (RenderBox child) => child.getMinIntrinsicHeight(width));
  793. }
  794. @override
  795. double computeMaxIntrinsicHeight(double width) {
  796. return getIntrinsicDimension(
  797. firstChild, (RenderBox child) => child.getMaxIntrinsicHeight(width));
  798. }
  799. @override
  800. double computeDistanceToActualBaseline(TextBaseline baseline) {
  801. return defaultComputeDistanceToHighestActualBaseline(baseline);
  802. }
  803. /// Lays out the positioned `child` according to `alignment` within a Stack of `size`.
  804. ///
  805. /// Returns true when the child has visual overflow.
  806. static bool layoutPositionedChild(RenderBox child,
  807. StackParentData childParentData, Size size, Alignment alignment) {
  808. assert(childParentData.isPositioned);
  809. assert(child.parentData == childParentData);
  810. bool hasVisualOverflow = false;
  811. BoxConstraints childConstraints = const BoxConstraints();
  812. if (childParentData.left != null && childParentData.right != null)
  813. childConstraints = childConstraints.tighten(
  814. width: size.width - childParentData.right - childParentData.left);
  815. else if (childParentData.width != null)
  816. childConstraints = childConstraints.tighten(width: childParentData.width);
  817. if (childParentData.top != null && childParentData.bottom != null)
  818. childConstraints = childConstraints.tighten(
  819. height: size.height - childParentData.bottom - childParentData.top);
  820. else if (childParentData.height != null)
  821. childConstraints =
  822. childConstraints.tighten(height: childParentData.height);
  823. child.layout(childConstraints, parentUsesSize: true);
  824. double x;
  825. if (childParentData.left != null) {
  826. x = childParentData.left;
  827. } else if (childParentData.right != null) {
  828. x = size.width - childParentData.right - child.size.width;
  829. } else {
  830. x = alignment.alongOffset(size - child.size as Offset).dx;
  831. }
  832. if (x < 0.0 || x + child.size.width > size.width) hasVisualOverflow = true;
  833. double y;
  834. if (childParentData.top != null) {
  835. y = childParentData.top;
  836. } else if (childParentData.bottom != null) {
  837. y = size.height - childParentData.bottom - child.size.height;
  838. } else {
  839. y = alignment.alongOffset(size - child.size as Offset).dy;
  840. }
  841. if (y < 0.0 || y + child.size.height > size.height)
  842. hasVisualOverflow = true;
  843. childParentData.offset = Offset(x, y);
  844. return hasVisualOverflow;
  845. }
  846. @override
  847. void performLayout() {
  848. final BoxConstraints constraints = this.constraints;
  849. _resolve();
  850. assert(_resolvedAlignment != null);
  851. _hasVisualOverflow = false;
  852. bool hasNonPositionedChildren = false;
  853. if (childCount == 0) {
  854. size = constraints.biggest;
  855. assert(size.isFinite);
  856. return;
  857. }
  858. double width = constraints.minWidth;
  859. double height = constraints.minHeight;
  860. BoxConstraints nonPositionedConstraints;
  861. assert(fit != null);
  862. switch (fit) {
  863. case StackFit.loose:
  864. nonPositionedConstraints = constraints.loosen();
  865. break;
  866. case StackFit.expand:
  867. nonPositionedConstraints = BoxConstraints.tight(constraints.biggest);
  868. break;
  869. case StackFit.passthrough:
  870. nonPositionedConstraints = constraints;
  871. break;
  872. }
  873. assert(nonPositionedConstraints != null);
  874. RenderBox child = firstChild;
  875. while (child != null) {
  876. final StackParentData childParentData =
  877. child.parentData as StackParentData;
  878. if (!childParentData.isPositioned) {
  879. hasNonPositionedChildren = true;
  880. child.layout(nonPositionedConstraints, parentUsesSize: true);
  881. final Size childSize = child.size;
  882. width = math.max(width, childSize.width);
  883. height = math.max(height, childSize.height);
  884. }
  885. child = childParentData.nextSibling;
  886. }
  887. if (hasNonPositionedChildren) {
  888. size = Size(width, height);
  889. assert(size.width == constraints.constrainWidth(width));
  890. assert(size.height == constraints.constrainHeight(height));
  891. } else {
  892. size = constraints.biggest;
  893. }
  894. assert(size.isFinite);
  895. child = firstChild;
  896. while (child != null) {
  897. final StackParentData childParentData =
  898. child.parentData as StackParentData;
  899. if (!childParentData.isPositioned) {
  900. childParentData.offset =
  901. _resolvedAlignment.alongOffset(size - child.size as Offset);
  902. } else {
  903. _hasVisualOverflow = layoutPositionedChild(
  904. child, childParentData, size, _resolvedAlignment) ||
  905. _hasVisualOverflow;
  906. }
  907. assert(child.parentData == childParentData);
  908. child = childParentData.nextSibling;
  909. }
  910. }
  911. @override
  912. bool hitTestChildren(BoxHitTestResult result, {Offset position}) {
  913. return defaultHitTestChildren(result, position: position);
  914. }
  915. /// Override in subclasses to customize how the stack paints.
  916. ///
  917. /// By default, the stack uses [defaultPaint]. This function is called by
  918. /// [paint] after potentially applying a clip to contain visual overflow.
  919. @protected
  920. void paintStack(PaintingContext context, Offset offset) {
  921. defaultPaint(context, offset);
  922. }
  923. @override
  924. void paint(PaintingContext context, Offset offset) {
  925. if (_overflow == Overflow.clip && _hasVisualOverflow) {
  926. context.pushClipRect(
  927. needsCompositing, offset, Offset.zero & size, paintStack);
  928. } else {
  929. paintStack(context, offset);
  930. }
  931. }
  932. @override
  933. Rect describeApproximatePaintClip(RenderObject child) =>
  934. _hasVisualOverflow ? Offset.zero & size : null;
  935. @override
  936. void debugFillProperties(DiagnosticPropertiesBuilder properties) {
  937. super.debugFillProperties(properties);
  938. properties
  939. .add(DiagnosticsProperty<AlignmentGeometry>('alignment', alignment));
  940. properties.add(EnumProperty<TextDirection>('textDirection', textDirection));
  941. properties.add(EnumProperty<StackFit>('fit', fit));
  942. properties.add(EnumProperty<Overflow>('overflow', overflow));
  943. }
  944. bool hitTest(BoxHitTestResult result, {@required Offset position}) {
  945. if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
  946. result.add(BoxHitTestEntry(this, position));
  947. return true;
  948. }
  949. return false;
  950. }
  951. }