custombuttonstylebutton.dart 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612
  1. // Copyright 2014 The Flutter Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style license that can be
  3. // found in the LICENSE file.
  4. import 'dart:math' as math;
  5. import 'package:flutter/foundation.dart';
  6. import 'package:flutter/material.dart';
  7. import 'package:flutter/rendering.dart';
  8. import 'package:flyinsonolite/infrastructure/scale.dart';
  9. /// The base [StatefulWidget] class for buttons whose style is defined by a [ButtonStyle] object.
  10. ///
  11. /// Concrete subclasses must override [defaultStyleOf] and [themeStyleOf].
  12. ///
  13. /// See also:
  14. /// * [CustomElevatedButton], a filled button whose material elevates when pressed.
  15. /// * [FilledButton], a filled button that doesn't elevate when pressed.
  16. /// * [FilledButton.tonal], a filled button variant that uses a secondary fill color.
  17. /// * [OutlinedButton], a button with an outlined border and no fill color.
  18. /// * [CustomTextButton], a button with no outline or fill color.
  19. /// * <https://m3.material.io/components/buttons/overview>, an overview of each of
  20. /// the Material Design button types and how they should be used in designs.
  21. abstract class CustomButtonStyleButton extends StatefulWidget {
  22. /// Abstract const constructor. This constructor enables subclasses to provide
  23. /// const constructors so that they can be used in const expressions.
  24. const CustomButtonStyleButton({
  25. super.key,
  26. required this.onPressed,
  27. required this.onLongPress,
  28. required this.onHover,
  29. required this.onFocusChange,
  30. required this.style,
  31. required this.focusNode,
  32. required this.autofocus,
  33. required this.clipBehavior,
  34. this.statesController,
  35. required this.child,
  36. }) : assert(autofocus != null),
  37. assert(clipBehavior != null);
  38. /// Called when the button is tapped or otherwise activated.
  39. ///
  40. /// If this callback and [onLongPress] are null, then the button will be disabled.
  41. ///
  42. /// See also:
  43. ///
  44. /// * [enabled], which is true if the button is enabled.
  45. final VoidCallback? onPressed;
  46. /// Called when the button is long-pressed.
  47. ///
  48. /// If this callback and [onPressed] are null, then the button will be disabled.
  49. ///
  50. /// See also:
  51. ///
  52. /// * [enabled], which is true if the button is enabled.
  53. final VoidCallback? onLongPress;
  54. /// Called when a pointer enters or exits the button response area.
  55. ///
  56. /// The value passed to the callback is true if a pointer has entered this
  57. /// part of the material and false if a pointer has exited this part of the
  58. /// material.
  59. final ValueChanged<bool>? onHover;
  60. /// Handler called when the focus changes.
  61. ///
  62. /// Called with true if this widget's node gains focus, and false if it loses
  63. /// focus.
  64. final ValueChanged<bool>? onFocusChange;
  65. /// Customizes this button's appearance.
  66. ///
  67. /// Non-null properties of this style override the corresponding
  68. /// properties in [themeStyleOf] and [defaultStyleOf]. [MaterialStateProperty]s
  69. /// that resolve to non-null values will similarly override the corresponding
  70. /// [MaterialStateProperty]s in [themeStyleOf] and [defaultStyleOf].
  71. ///
  72. /// Null by default.
  73. final ButtonStyle? style;
  74. /// {@macro flutter.material.Material.clipBehavior}
  75. ///
  76. /// Defaults to [Clip.none], and must not be null.
  77. final Clip clipBehavior;
  78. /// {@macro flutter.widgets.Focus.focusNode}
  79. final FocusNode? focusNode;
  80. /// {@macro flutter.widgets.Focus.autofocus}
  81. final bool autofocus;
  82. /// {@macro flutter.material.inkwell.statesController}
  83. final MaterialStatesController? statesController;
  84. /// Typically the button's label.
  85. ///
  86. /// {@macro flutter.widgets.ProxyWidget.child}
  87. final Widget? child;
  88. /// Returns a non-null [ButtonStyle] that's based primarily on the [Theme]'s
  89. /// [ThemeData.textTheme] and [ThemeData.colorScheme].
  90. ///
  91. /// The returned style can be overridden by the [style] parameter and
  92. /// by the style returned by [themeStyleOf]. For example the default
  93. /// style of the [TextButton] subclass can be overridden with its
  94. /// [TextButton.style] constructor parameter, or with a
  95. /// [TextButtonTheme].
  96. ///
  97. /// Concrete button subclasses should return a ButtonStyle that
  98. /// has no null properties, and where all of the [MaterialStateProperty]
  99. /// properties resolve to non-null values.
  100. ///
  101. /// See also:
  102. ///
  103. /// * [themeStyleOf], Returns the ButtonStyle of this button's component theme.
  104. @protected
  105. ButtonStyle defaultStyleOf(BuildContext context);
  106. /// Returns the ButtonStyle that belongs to the button's component theme.
  107. ///
  108. /// The returned style can be overridden by the [style] parameter.
  109. ///
  110. /// Concrete button subclasses should return the ButtonStyle for the
  111. /// nearest subclass-specific inherited theme, and if no such theme
  112. /// exists, then the same value from the overall [Theme].
  113. ///
  114. /// See also:
  115. ///
  116. /// * [defaultStyleOf], Returns the default [ButtonStyle] for this button.
  117. @protected
  118. ButtonStyle? themeStyleOf(BuildContext context);
  119. /// Whether the button is enabled or disabled.
  120. ///
  121. /// Buttons are disabled by default. To enable a button, set its [onPressed]
  122. /// or [onLongPress] properties to a non-null value.
  123. bool get enabled => onPressed != null || onLongPress != null;
  124. @override
  125. State<CustomButtonStyleButton> createState() => _ButtonStyleState();
  126. @override
  127. void debugFillProperties(DiagnosticPropertiesBuilder properties) {
  128. super.debugFillProperties(properties);
  129. properties
  130. .add(FlagProperty('enabled', value: enabled, ifFalse: 'disabled'));
  131. properties.add(
  132. DiagnosticsProperty<ButtonStyle>('style', style, defaultValue: null));
  133. properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode,
  134. defaultValue: null));
  135. }
  136. /// Returns null if [value] is null, otherwise `MaterialStatePropertyAll<T>(value)`.
  137. ///
  138. /// A convenience method for subclasses.
  139. static MaterialStateProperty<T>? allOrNull<T>(T? value) =>
  140. value == null ? null : MaterialStatePropertyAll<T>(value);
  141. /// Returns an interpolated value based on the [textScaleFactor] parameter:
  142. ///
  143. /// * 0 - 1 [geometry1x]
  144. /// * 1 - 2 lerp([geometry1x], [geometry2x], [textScaleFactor] - 1)
  145. /// * 2 - 3 lerp([geometry2x], [geometry3x], [textScaleFactor] - 2)
  146. /// * otherwise [geometry3x]
  147. ///
  148. /// A convenience method for subclasses.
  149. static EdgeInsetsGeometry scaledPadding(
  150. EdgeInsetsGeometry geometry1x,
  151. EdgeInsetsGeometry geometry2x,
  152. EdgeInsetsGeometry geometry3x,
  153. double textScaleFactor,
  154. ) {
  155. assert(geometry1x != null);
  156. assert(geometry2x != null);
  157. assert(geometry3x != null);
  158. assert(textScaleFactor != null);
  159. if (textScaleFactor <= 1) {
  160. return geometry1x;
  161. } else if (textScaleFactor >= 3) {
  162. return geometry3x;
  163. } else if (textScaleFactor <= 2) {
  164. return EdgeInsetsGeometry.lerp(
  165. geometry1x, geometry2x, textScaleFactor - 1)!;
  166. }
  167. return EdgeInsetsGeometry.lerp(
  168. geometry2x, geometry3x, textScaleFactor - 2)!;
  169. }
  170. }
  171. /// The base [State] class for buttons whose style is defined by a [ButtonStyle] object.
  172. ///
  173. /// See also:
  174. ///
  175. /// * [CustomButtonStyleButton], the [StatefulWidget] subclass for which this class is the [State].
  176. /// * [CustomElevatedButton], a filled button whose material elevates when pressed.
  177. /// * [FilledButton], a filled ButtonStyleButton that doesn't elevate when pressed.
  178. /// * [OutlinedButton], similar to [TextButton], but with an outline.
  179. /// * [TextButton], a simple button without a shadow.
  180. class _ButtonStyleState extends State<CustomButtonStyleButton>
  181. with TickerProviderStateMixin {
  182. AnimationController? controller;
  183. double? elevation;
  184. Color? backgroundColor;
  185. MaterialStatesController? internalStatesController;
  186. void handleStatesControllerChange() {
  187. // Force a rebuild to resolve MaterialStateProperty properties
  188. setState(() {});
  189. }
  190. MaterialStatesController get statesController =>
  191. widget.statesController ?? internalStatesController!;
  192. void initStatesController() {
  193. if (widget.statesController == null) {
  194. internalStatesController = MaterialStatesController();
  195. }
  196. statesController.update(MaterialState.disabled, !widget.enabled);
  197. statesController.addListener(handleStatesControllerChange);
  198. }
  199. @override
  200. void initState() {
  201. super.initState();
  202. initStatesController();
  203. }
  204. @override
  205. void didUpdateWidget(CustomButtonStyleButton oldWidget) {
  206. super.didUpdateWidget(oldWidget);
  207. if (widget.statesController != oldWidget.statesController) {
  208. oldWidget.statesController?.removeListener(handleStatesControllerChange);
  209. if (widget.statesController != null) {
  210. internalStatesController?.dispose();
  211. internalStatesController = null;
  212. }
  213. initStatesController();
  214. }
  215. if (widget.enabled != oldWidget.enabled) {
  216. statesController.update(MaterialState.disabled, !widget.enabled);
  217. if (!widget.enabled) {
  218. // The button may have been disabled while a press gesture is currently underway.
  219. statesController.update(MaterialState.pressed, false);
  220. }
  221. }
  222. }
  223. @override
  224. void dispose() {
  225. statesController.removeListener(handleStatesControllerChange);
  226. internalStatesController?.dispose();
  227. controller?.dispose();
  228. super.dispose();
  229. }
  230. @override
  231. Widget build(BuildContext context) {
  232. final ButtonStyle? widgetStyle = widget.style;
  233. final ButtonStyle? themeStyle = widget.themeStyleOf(context);
  234. final ButtonStyle defaultStyle = widget.defaultStyleOf(context);
  235. assert(defaultStyle != null);
  236. T? effectiveValue<T>(T? Function(ButtonStyle? style) getProperty) {
  237. final T? widgetValue = getProperty(widgetStyle);
  238. final T? themeValue = getProperty(themeStyle);
  239. final T? defaultValue = getProperty(defaultStyle);
  240. return widgetValue ?? themeValue ?? defaultValue;
  241. }
  242. T? resolve<T>(
  243. MaterialStateProperty<T>? Function(ButtonStyle? style) getProperty) {
  244. return effectiveValue(
  245. (ButtonStyle? style) {
  246. return getProperty(style)?.resolve(statesController.value);
  247. },
  248. );
  249. }
  250. final double? resolvedElevation =
  251. resolve<double?>((ButtonStyle? style) => style?.elevation);
  252. final TextStyle? resolvedTextStyle =
  253. resolve<TextStyle?>((ButtonStyle? style) => style?.textStyle);
  254. Color? resolvedBackgroundColor =
  255. resolve<Color?>((ButtonStyle? style) => style?.backgroundColor);
  256. final Color? resolvedForegroundColor =
  257. resolve<Color?>((ButtonStyle? style) => style?.foregroundColor);
  258. final Color? resolvedShadowColor =
  259. resolve<Color?>((ButtonStyle? style) => style?.shadowColor);
  260. final Color? resolvedSurfaceTintColor =
  261. resolve<Color?>((ButtonStyle? style) => style?.surfaceTintColor);
  262. final EdgeInsetsGeometry? resolvedPadding =
  263. resolve<EdgeInsetsGeometry?>((ButtonStyle? style) => style?.padding);
  264. final Size? resolvedMinimumSize =
  265. resolve<Size?>((ButtonStyle? style) => style?.minimumSize);
  266. final Size? resolvedFixedSize =
  267. resolve<Size?>((ButtonStyle? style) => style?.fixedSize);
  268. final Size? resolvedMaximumSize =
  269. resolve<Size?>((ButtonStyle? style) => style?.maximumSize);
  270. final double? resolvedIconSize =
  271. resolve<double?>((ButtonStyle? style) => style?.iconSize);
  272. final BorderSide? resolvedSide =
  273. resolve<BorderSide?>((ButtonStyle? style) => style?.side);
  274. final OutlinedBorder? resolvedShape =
  275. resolve<OutlinedBorder?>((ButtonStyle? style) => style?.shape);
  276. final MaterialStateMouseCursor mouseCursor = _MouseCursor(
  277. (Set<MaterialState> states) => effectiveValue(
  278. (ButtonStyle? style) => style?.mouseCursor?.resolve(states)),
  279. );
  280. final MaterialStateProperty<Color?> overlayColor =
  281. MaterialStateProperty.resolveWith<Color?>(
  282. (Set<MaterialState> states) => effectiveValue(
  283. (ButtonStyle? style) => style?.overlayColor?.resolve(states)),
  284. );
  285. final VisualDensity? resolvedVisualDensity =
  286. effectiveValue((ButtonStyle? style) => style?.visualDensity);
  287. final MaterialTapTargetSize? resolvedTapTargetSize =
  288. effectiveValue((ButtonStyle? style) => style?.tapTargetSize);
  289. final Duration? resolvedAnimationDuration =
  290. effectiveValue((ButtonStyle? style) => style?.animationDuration);
  291. final bool? resolvedEnableFeedback =
  292. effectiveValue((ButtonStyle? style) => style?.enableFeedback);
  293. final AlignmentGeometry? resolvedAlignment =
  294. effectiveValue((ButtonStyle? style) => style?.alignment);
  295. final Offset densityAdjustment = Offset(
  296. resolvedVisualDensity!.baseSizeAdjustment.dx.s,
  297. resolvedVisualDensity.baseSizeAdjustment.dy.s);
  298. final InteractiveInkFeatureFactory? resolvedSplashFactory =
  299. effectiveValue((ButtonStyle? style) => style?.splashFactory);
  300. BoxConstraints constraints = resolvedVisualDensity.effectiveConstraints(
  301. BoxConstraints(
  302. minWidth: resolvedMinimumSize!.width,
  303. minHeight: resolvedMinimumSize.height,
  304. maxWidth: resolvedMaximumSize!.width,
  305. maxHeight: resolvedMaximumSize.height,
  306. ),
  307. );
  308. BoxConstraints effectiveConstraints = BoxConstraints(
  309. minWidth: constraints.minWidth.s,
  310. minHeight: constraints.minHeight.s,
  311. maxWidth: constraints.maxWidth.s,
  312. maxHeight: constraints.maxHeight.s,
  313. );
  314. if (resolvedFixedSize != null) {
  315. final Size size = effectiveConstraints.constrain(resolvedFixedSize);
  316. if (size.width.isFinite) {
  317. effectiveConstraints = effectiveConstraints.copyWith(
  318. minWidth: size.width,
  319. maxWidth: size.width,
  320. );
  321. }
  322. if (size.height.isFinite) {
  323. effectiveConstraints = effectiveConstraints.copyWith(
  324. minHeight: size.height,
  325. maxHeight: size.height,
  326. );
  327. }
  328. }
  329. // Per the Material Design team: don't allow the VisualDensity
  330. // adjustment to reduce the width of the left/right padding. If we
  331. // did, VisualDensity.compact, the default for desktop/web, would
  332. // reduce the horizontal padding to zero.
  333. final double dy = densityAdjustment.dy;
  334. final double dx = math.max(0, densityAdjustment.dx);
  335. final EdgeInsetsGeometry padding = resolvedPadding!
  336. .add(EdgeInsets.fromLTRB(dx, dy, dx, dy))
  337. .clamp(EdgeInsets.zero,
  338. EdgeInsetsGeometry.infinity); // ignore_clamp_double_lint
  339. // If an opaque button's background is becoming translucent while its
  340. // elevation is changing, change the elevation first. Material implicitly
  341. // animates its elevation but not its color. SKIA renders non-zero
  342. // elevations as a shadow colored fill behind the Material's background.
  343. if (resolvedAnimationDuration! > Duration.zero &&
  344. elevation != null &&
  345. backgroundColor != null &&
  346. elevation != resolvedElevation &&
  347. backgroundColor!.value != resolvedBackgroundColor!.value &&
  348. backgroundColor!.opacity == 1 &&
  349. resolvedBackgroundColor.opacity < 1 &&
  350. resolvedElevation == 0) {
  351. if (controller?.duration != resolvedAnimationDuration) {
  352. controller?.dispose();
  353. controller = AnimationController(
  354. duration: resolvedAnimationDuration,
  355. vsync: this,
  356. )..addStatusListener((AnimationStatus status) {
  357. if (status == AnimationStatus.completed) {
  358. setState(() {}); // Rebuild with the final background color.
  359. }
  360. });
  361. }
  362. resolvedBackgroundColor =
  363. backgroundColor; // Defer changing the background color.
  364. controller!.value = 0;
  365. controller!.forward();
  366. }
  367. elevation = resolvedElevation;
  368. backgroundColor = resolvedBackgroundColor;
  369. final Widget result = ConstrainedBox(
  370. constraints: effectiveConstraints,
  371. child: Material(
  372. elevation: resolvedElevation!,
  373. textStyle: resolvedTextStyle?.copyWith(color: resolvedForegroundColor),
  374. shape: resolvedShape!.copyWith(side: resolvedSide),
  375. color: resolvedBackgroundColor,
  376. shadowColor: resolvedShadowColor,
  377. surfaceTintColor: resolvedSurfaceTintColor,
  378. type: resolvedBackgroundColor == null
  379. ? MaterialType.transparency
  380. : MaterialType.button,
  381. animationDuration: resolvedAnimationDuration,
  382. clipBehavior: widget.clipBehavior,
  383. child: InkWell(
  384. onTap: widget.onPressed,
  385. onLongPress: widget.onLongPress,
  386. onHover: widget.onHover,
  387. mouseCursor: mouseCursor,
  388. enableFeedback: resolvedEnableFeedback,
  389. focusNode: widget.focusNode,
  390. canRequestFocus: widget.enabled,
  391. onFocusChange: widget.onFocusChange,
  392. autofocus: widget.autofocus,
  393. splashFactory: resolvedSplashFactory,
  394. overlayColor: overlayColor,
  395. highlightColor: Colors.transparent,
  396. customBorder: resolvedShape.copyWith(side: resolvedSide),
  397. statesController: statesController,
  398. child: IconTheme.merge(
  399. data: IconThemeData(
  400. color: resolvedForegroundColor, size: resolvedIconSize),
  401. child: Padding(
  402. padding: padding,
  403. child: Align(
  404. alignment: resolvedAlignment!,
  405. widthFactor: 1.0,
  406. heightFactor: 1.0,
  407. child: widget.child,
  408. ),
  409. ),
  410. ),
  411. ),
  412. ),
  413. );
  414. final Size minSize;
  415. switch (resolvedTapTargetSize!) {
  416. case MaterialTapTargetSize.padded:
  417. minSize = Size(
  418. kMinInteractiveDimension.s + densityAdjustment.dx,
  419. kMinInteractiveDimension.s + densityAdjustment.dy,
  420. );
  421. assert(minSize.width >= 0.0);
  422. assert(minSize.height >= 0.0);
  423. break;
  424. case MaterialTapTargetSize.shrinkWrap:
  425. minSize = Size.zero;
  426. break;
  427. }
  428. return Semantics(
  429. container: true,
  430. button: true,
  431. enabled: widget.enabled,
  432. child: _InputPadding(
  433. minSize: minSize,
  434. child: result,
  435. ),
  436. );
  437. }
  438. }
  439. class _MouseCursor extends MaterialStateMouseCursor {
  440. const _MouseCursor(this.resolveCallback);
  441. final MaterialPropertyResolver<MouseCursor?> resolveCallback;
  442. @override
  443. MouseCursor resolve(Set<MaterialState> states) => resolveCallback(states)!;
  444. @override
  445. String get debugDescription => 'ButtonStyleButton_MouseCursor';
  446. }
  447. /// A widget to pad the area around a [CustomButtonStyleButton]'s inner [Material].
  448. ///
  449. /// Redirect taps that occur in the padded area around the child to the center
  450. /// of the child. This increases the size of the button and the button's
  451. /// "tap target", but not its material or its ink splashes.
  452. class _InputPadding extends SingleChildRenderObjectWidget {
  453. const _InputPadding({
  454. super.child,
  455. required this.minSize,
  456. });
  457. final Size minSize;
  458. @override
  459. RenderObject createRenderObject(BuildContext context) {
  460. return _RenderInputPadding(minSize);
  461. }
  462. @override
  463. void updateRenderObject(
  464. BuildContext context, covariant _RenderInputPadding renderObject) {
  465. renderObject.minSize = minSize;
  466. }
  467. }
  468. class _RenderInputPadding extends RenderShiftedBox {
  469. _RenderInputPadding(this._minSize, [RenderBox? child]) : super(child);
  470. Size get minSize => _minSize;
  471. Size _minSize;
  472. set minSize(Size value) {
  473. if (_minSize == value) {
  474. return;
  475. }
  476. _minSize = value;
  477. markNeedsLayout();
  478. }
  479. @override
  480. double computeMinIntrinsicWidth(double height) {
  481. if (child != null) {
  482. return math.max(child!.getMinIntrinsicWidth(height), minSize.width);
  483. }
  484. return 0.0;
  485. }
  486. @override
  487. double computeMinIntrinsicHeight(double width) {
  488. if (child != null) {
  489. return math.max(child!.getMinIntrinsicHeight(width), minSize.height);
  490. }
  491. return 0.0;
  492. }
  493. @override
  494. double computeMaxIntrinsicWidth(double height) {
  495. if (child != null) {
  496. return math.max(child!.getMaxIntrinsicWidth(height), minSize.width);
  497. }
  498. return 0.0;
  499. }
  500. @override
  501. double computeMaxIntrinsicHeight(double width) {
  502. if (child != null) {
  503. return math.max(child!.getMaxIntrinsicHeight(width), minSize.height);
  504. }
  505. return 0.0;
  506. }
  507. Size _computeSize(
  508. {required BoxConstraints constraints,
  509. required ChildLayouter layoutChild}) {
  510. if (child != null) {
  511. final Size childSize = layoutChild(child!, constraints);
  512. final double height = math.max(childSize.width, minSize.width);
  513. final double width = math.max(childSize.height, minSize.height);
  514. return constraints.constrain(Size(height, width));
  515. }
  516. return Size.zero;
  517. }
  518. @override
  519. Size computeDryLayout(BoxConstraints constraints) {
  520. return _computeSize(
  521. constraints: constraints,
  522. layoutChild: ChildLayoutHelper.dryLayoutChild,
  523. );
  524. }
  525. @override
  526. void performLayout() {
  527. size = _computeSize(
  528. constraints: constraints,
  529. layoutChild: ChildLayoutHelper.layoutChild,
  530. );
  531. if (child != null) {
  532. final BoxParentData childParentData = child!.parentData! as BoxParentData;
  533. childParentData.offset =
  534. Alignment.center.alongOffset(size - child!.size as Offset);
  535. }
  536. }
  537. @override
  538. bool hitTest(BoxHitTestResult result, {required Offset position}) {
  539. if (super.hitTest(result, position: position)) {
  540. return true;
  541. }
  542. final Offset center = child!.size.center(Offset.zero);
  543. return result.addWithRawTransform(
  544. transform: MatrixUtils.forceToPoint(center),
  545. position: center,
  546. hitTest: (BoxHitTestResult result, Offset position) {
  547. assert(position == center);
  548. return child!.hitTest(result, position: center);
  549. },
  550. );
  551. }
  552. }