flutter_flow_widgets.dart 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. import 'package:font_awesome_flutter/font_awesome_flutter.dart';
  2. import 'package:flutter/material.dart';
  3. import 'package:auto_size_text/auto_size_text.dart';
  4. class FFButtonOptions {
  5. const FFButtonOptions({
  6. this.textAlign,
  7. this.textStyle,
  8. this.elevation,
  9. this.height,
  10. this.width,
  11. this.padding,
  12. this.color,
  13. this.disabledColor,
  14. this.disabledTextColor,
  15. this.splashColor,
  16. this.iconSize,
  17. this.iconColor,
  18. this.iconAlignment,
  19. this.iconPadding,
  20. this.borderRadius,
  21. this.borderSide,
  22. this.hoverColor,
  23. this.hoverBorderSide,
  24. this.hoverTextColor,
  25. this.hoverElevation,
  26. this.maxLines,
  27. });
  28. final TextAlign? textAlign;
  29. final TextStyle? textStyle;
  30. final double? elevation;
  31. final double? height;
  32. final double? width;
  33. final EdgeInsetsGeometry? padding;
  34. final Color? color;
  35. final Color? disabledColor;
  36. final Color? disabledTextColor;
  37. final int? maxLines;
  38. final Color? splashColor;
  39. final double? iconSize;
  40. final Color? iconColor;
  41. final IconAlignment? iconAlignment;
  42. final EdgeInsetsGeometry? iconPadding;
  43. final BorderRadius? borderRadius;
  44. final BorderSide? borderSide;
  45. final Color? hoverColor;
  46. final BorderSide? hoverBorderSide;
  47. final Color? hoverTextColor;
  48. final double? hoverElevation;
  49. }
  50. class FFButtonWidget extends StatefulWidget {
  51. const FFButtonWidget({
  52. super.key,
  53. required this.text,
  54. required this.onPressed,
  55. this.icon,
  56. this.iconData,
  57. required this.options,
  58. this.showLoadingIndicator = true,
  59. this.focusNode,
  60. });
  61. final String text;
  62. final Widget? icon;
  63. final IconData? iconData;
  64. final Function()? onPressed;
  65. final FFButtonOptions options;
  66. final bool showLoadingIndicator;
  67. final FocusNode? focusNode;
  68. @override
  69. State<FFButtonWidget> createState() => _FFButtonWidgetState();
  70. }
  71. class _FFButtonWidgetState extends State<FFButtonWidget> {
  72. bool loading = false;
  73. late FocusNode _internalFocusNode;
  74. FocusNode get _focusNode => widget.focusNode ?? _internalFocusNode;
  75. int get maxLines => widget.options.maxLines ?? 1;
  76. String? get text =>
  77. widget.options.textStyle?.fontSize == 0 ? null : widget.text;
  78. @override
  79. void initState() {
  80. super.initState();
  81. _internalFocusNode = FocusNode();
  82. }
  83. @override
  84. void dispose() {
  85. _internalFocusNode.dispose();
  86. super.dispose();
  87. }
  88. @override
  89. Widget build(BuildContext context) {
  90. Widget textWidget = loading
  91. ? SizedBox(
  92. width: widget.options.width == null
  93. ? _getTextWidth(text, widget.options.textStyle, maxLines)
  94. : null,
  95. child: Center(
  96. child: SizedBox(
  97. width: 23,
  98. height: 23,
  99. child: CircularProgressIndicator(
  100. valueColor: AlwaysStoppedAnimation<Color>(
  101. widget.options.textStyle?.color ?? Colors.white,
  102. ),
  103. ),
  104. ),
  105. ),
  106. )
  107. : AutoSizeText(
  108. text ?? '',
  109. style:
  110. text == null ? null : widget.options.textStyle?.withoutColor(),
  111. textAlign: widget.options.textAlign,
  112. maxLines: maxLines,
  113. overflow: TextOverflow.ellipsis,
  114. );
  115. final onPressed = widget.onPressed != null
  116. ? (widget.showLoadingIndicator
  117. ? () async {
  118. if (loading) {
  119. return;
  120. }
  121. setState(() => loading = true);
  122. try {
  123. await widget.onPressed!();
  124. } finally {
  125. if (mounted) {
  126. setState(() => loading = false);
  127. }
  128. }
  129. }
  130. : () => widget.onPressed!())
  131. : null;
  132. ButtonStyle style = ButtonStyle(
  133. shape: WidgetStateProperty.resolveWith<OutlinedBorder>((states) {
  134. if (states.contains(WidgetState.hovered) &&
  135. widget.options.hoverBorderSide != null) {
  136. return RoundedRectangleBorder(
  137. borderRadius:
  138. widget.options.borderRadius ?? BorderRadius.circular(8),
  139. side: widget.options.hoverBorderSide!,
  140. );
  141. }
  142. return RoundedRectangleBorder(
  143. borderRadius: widget.options.borderRadius ?? BorderRadius.circular(8),
  144. side: widget.options.borderSide ?? BorderSide.none,
  145. );
  146. }),
  147. foregroundColor: WidgetStateProperty.resolveWith<Color?>((states) {
  148. if (states.contains(WidgetState.disabled) &&
  149. widget.options.disabledTextColor != null) {
  150. return widget.options.disabledTextColor;
  151. }
  152. if (states.contains(WidgetState.hovered) &&
  153. widget.options.hoverTextColor != null) {
  154. return widget.options.hoverTextColor;
  155. }
  156. return widget.options.textStyle?.color ?? Colors.white;
  157. }),
  158. backgroundColor: WidgetStateProperty.resolveWith<Color?>((states) {
  159. if (states.contains(WidgetState.disabled) &&
  160. widget.options.disabledColor != null) {
  161. return widget.options.disabledColor;
  162. }
  163. if (states.contains(WidgetState.hovered) &&
  164. widget.options.hoverColor != null) {
  165. return widget.options.hoverColor;
  166. }
  167. return widget.options.color;
  168. }),
  169. overlayColor: WidgetStateProperty.resolveWith<Color?>((states) {
  170. if (states.contains(WidgetState.pressed)) {
  171. return widget.options.splashColor;
  172. }
  173. return widget.options.hoverColor == null ? null : Colors.transparent;
  174. }),
  175. padding: WidgetStateProperty.all(
  176. widget.options.padding ??
  177. const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4.0),
  178. ),
  179. elevation: WidgetStateProperty.resolveWith<double?>((states) {
  180. if (states.contains(WidgetState.hovered) &&
  181. widget.options.hoverElevation != null) {
  182. return widget.options.hoverElevation!;
  183. }
  184. return widget.options.elevation ?? 2.0;
  185. }),
  186. iconColor: WidgetStateProperty.resolveWith<Color?>((states) {
  187. if (states.contains(WidgetState.disabled) &&
  188. widget.options.disabledTextColor != null) {
  189. return widget.options.disabledTextColor;
  190. }
  191. if (states.contains(WidgetState.hovered) &&
  192. widget.options.hoverTextColor != null) {
  193. return widget.options.hoverTextColor;
  194. }
  195. return widget.options.iconColor;
  196. }),
  197. );
  198. if ((widget.icon != null || widget.iconData != null) && !loading) {
  199. Widget icon = widget.icon ??
  200. FaIcon(
  201. widget.iconData!,
  202. size: widget.options.iconSize,
  203. color: widget.options.iconColor,
  204. );
  205. if (text == null) {
  206. return Container(
  207. height: widget.options.height,
  208. width: widget.options.width,
  209. decoration: BoxDecoration(
  210. border: Border.fromBorderSide(
  211. widget.options.borderSide ?? BorderSide.none,
  212. ),
  213. borderRadius:
  214. widget.options.borderRadius ?? BorderRadius.circular(8),
  215. ),
  216. child: IconButton(
  217. splashRadius: 1.0,
  218. icon: Padding(
  219. padding: widget.options.iconPadding ?? EdgeInsets.zero,
  220. child: icon,
  221. ),
  222. onPressed: onPressed,
  223. style: style,
  224. focusNode: _focusNode,
  225. ),
  226. );
  227. }
  228. return SizedBox(
  229. height: widget.options.height,
  230. width: widget.options.width,
  231. child: ElevatedButton.icon(
  232. icon: Padding(
  233. padding: widget.options.iconPadding ?? EdgeInsets.zero,
  234. child: icon,
  235. ),
  236. label: textWidget,
  237. onPressed: onPressed,
  238. style: style,
  239. iconAlignment: widget.options.iconAlignment ?? IconAlignment.start,
  240. focusNode: _focusNode,
  241. ),
  242. );
  243. }
  244. return SizedBox(
  245. height: widget.options.height,
  246. width: widget.options.width,
  247. child: ElevatedButton(
  248. onPressed: onPressed,
  249. style: style,
  250. focusNode: _focusNode,
  251. child: textWidget,
  252. ),
  253. );
  254. }
  255. }
  256. extension _WithoutColorExtension on TextStyle {
  257. TextStyle withoutColor() => TextStyle(
  258. inherit: inherit,
  259. color: null,
  260. backgroundColor: backgroundColor,
  261. fontSize: fontSize,
  262. fontWeight: fontWeight,
  263. fontStyle: fontStyle,
  264. letterSpacing: letterSpacing,
  265. wordSpacing: wordSpacing,
  266. textBaseline: textBaseline,
  267. height: height,
  268. leadingDistribution: leadingDistribution,
  269. locale: locale,
  270. foreground: foreground,
  271. background: background,
  272. shadows: shadows,
  273. fontFeatures: fontFeatures,
  274. decoration: decoration,
  275. decorationColor: decorationColor,
  276. decorationStyle: decorationStyle,
  277. decorationThickness: decorationThickness,
  278. debugLabel: debugLabel,
  279. fontFamily: fontFamily,
  280. fontFamilyFallback: fontFamilyFallback,
  281. // The _package field is private so unfortunately we can't set it here,
  282. // but it's almost always unset anyway.
  283. // package: _package,
  284. overflow: overflow,
  285. );
  286. }
  287. // Slightly hacky method of getting the layout width of the provided text.
  288. double? _getTextWidth(String? text, TextStyle? style, int maxLines) =>
  289. text != null
  290. ? (TextPainter(
  291. text: TextSpan(text: text, style: style),
  292. textDirection: TextDirection.ltr,
  293. maxLines: maxLines,
  294. )..layout())
  295. .size
  296. .width
  297. : null;
  298. class FFFocusIndicator extends StatefulWidget {
  299. final Widget Function(FocusNode focusNode)? builder;
  300. final Widget? child;
  301. final Border? border;
  302. final BorderRadius? borderRadius;
  303. final EdgeInsetsGeometry? padding;
  304. final void Function()? onTap;
  305. final void Function()? onLongPress;
  306. final void Function()? onDoubleTap;
  307. const FFFocusIndicator({
  308. super.key,
  309. this.builder,
  310. this.child,
  311. this.border,
  312. this.borderRadius,
  313. this.padding,
  314. this.onTap,
  315. this.onLongPress,
  316. this.onDoubleTap,
  317. }) : assert(
  318. builder != null || child != null,
  319. 'Either builder or child must be provided',
  320. );
  321. @override
  322. State<FFFocusIndicator> createState() => _FFFocusIndicatorState();
  323. }
  324. class _FFFocusIndicatorState extends State<FFFocusIndicator> {
  325. late FocusNode _focusNode;
  326. bool _hasFocus = false;
  327. @override
  328. void initState() {
  329. super.initState();
  330. _focusNode = FocusNode();
  331. _focusNode.addListener(_onFocusChange);
  332. }
  333. @override
  334. void dispose() {
  335. _focusNode.removeListener(_onFocusChange);
  336. _focusNode.dispose();
  337. super.dispose();
  338. }
  339. void _onFocusChange() {
  340. if (mounted) {
  341. if (_focusNode.hasFocus) {
  342. // No single ScrollPositionAlignmentPolicy scrolls in both directions.
  343. // keepVisibleAtEnd scrolls DOWN (handles Shift+Tab wrap first → last).
  344. // keepVisibleAtStart scrolls UP (handles Tab wrap last → first).
  345. // Each is a no-op when the widget is already visible. We call
  346. // keepVisibleAtEnd first so that keepVisibleAtStart gets the final
  347. // say — ensuring the top of the widget is shown when it's taller
  348. // than the viewport.
  349. WidgetsBinding.instance.addPostFrameCallback((_) {
  350. if (mounted && _focusNode.hasFocus) {
  351. Scrollable.ensureVisible(
  352. context,
  353. alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd,
  354. );
  355. Scrollable.ensureVisible(
  356. context,
  357. alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart,
  358. );
  359. }
  360. });
  361. }
  362. setState(() {
  363. _hasFocus = _focusNode.hasFocus;
  364. });
  365. }
  366. }
  367. @override
  368. Widget build(BuildContext context) {
  369. final bool hasInteractions = widget.onTap != null ||
  370. widget.onLongPress != null ||
  371. widget.onDoubleTap != null;
  372. Widget childWidget;
  373. if (widget.builder != null) {
  374. // Builder mode: pass focus node to builder
  375. childWidget = widget.builder!(_focusNode);
  376. } else if (hasInteractions) {
  377. // Child mode with interactions: wrap in InkWell
  378. childWidget = InkWell(
  379. splashColor: Colors.transparent,
  380. hoverColor: Colors.transparent,
  381. highlightColor: Colors.transparent,
  382. focusColor: Colors.transparent,
  383. focusNode: _focusNode,
  384. onTap: widget.onTap,
  385. onLongPress: widget.onLongPress,
  386. onDoubleTap: widget.onDoubleTap,
  387. child: widget.child!,
  388. );
  389. } else {
  390. // Child mode without interactions: just use child
  391. childWidget = widget.child!;
  392. }
  393. return AnimatedContainer(
  394. duration: const Duration(milliseconds: 200),
  395. padding: _hasFocus ? widget.padding : null,
  396. decoration: BoxDecoration(
  397. border: _hasFocus ? widget.border : null,
  398. borderRadius: widget.borderRadius ?? BorderRadius.circular(4),
  399. ),
  400. child: childWidget,
  401. );
  402. }
  403. }