gradient_circle_header.dart 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712
  1. import 'dart:async';
  2. import 'package:flutter/material.dart'
  3. hide RefreshIndicatorState, RefreshIndicator;
  4. import 'package:flutter_svg/flutter_svg.dart';
  5. import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
  6. import 'package:pull_to_refresh_flutter3/pull_to_refresh_flutter3.dart';
  7. import '../constants/assets.dart';
  8. /// 自定义渐变圆形到圆柱形刷新头部
  9. class GradientCircleHeader extends RefreshIndicator {
  10. /// refreshing content
  11. final Widget? refresh;
  12. /// complete content
  13. final Widget? complete;
  14. /// failed content
  15. final Widget? failed;
  16. const GradientCircleHeader({
  17. super.key,
  18. this.refresh,
  19. this.complete,
  20. super.completeDuration = const Duration(milliseconds: 600),
  21. this.failed,
  22. }) : super(height: 60.0, refreshStyle: RefreshStyle.UnFollow);
  23. @override
  24. State<StatefulWidget> createState() {
  25. return _GradientCircleHeaderState();
  26. }
  27. }
  28. class _GradientCircleHeaderState
  29. extends RefreshIndicatorState<GradientCircleHeader>
  30. with TickerProviderStateMixin {
  31. AnimationController? _animationController;
  32. late AnimationController _dismissCtl;
  33. late AnimationController _rotateCtl; // 旋转动画控制器
  34. late AnimationController _colorCtl; // 颜色过渡动画控制器
  35. double _currentOffset = 0.0; // 跟踪当前下拉距离
  36. bool _svgsPrecached = false; // SVG 是否已预加载
  37. // 默认状态的背景渐变
  38. final Color _defaultBgStartColor = const Color(0xA6BABABA);
  39. final Color _defaultBgEndColor = const Color(0x33404040);
  40. // 默认状态的边框渐变
  41. final List<Color> _defaultBorderColors = const [
  42. Color(0xFFDFDFDF),
  43. Color(0xFFA2A2A2),
  44. Color(0x4F6B6B6B),
  45. ];
  46. // 当前状态的背景渐变颜色
  47. Color _currentBgStartColor = const Color(0xA6BABABA);
  48. Color _currentBgEndColor = const Color(0x33404040);
  49. // 当前状态的边框渐变颜色
  50. List<Color> _currentBorderColors = [
  51. const Color(0xFFDFDFDF),
  52. const Color(0xFFA2A2A2),
  53. const Color(0x4F6B6B6B),
  54. ];
  55. // 成功状态的背景渐变
  56. final Color _successBgStartColor = const Color(0xFF0D2116);
  57. final Color _successBgEndColor = const Color(0xFF228643);
  58. // 成功状态的边框渐变
  59. final List<Color> _successBorderColors = [
  60. const Color(0xFF253B2E),
  61. const Color(0xFF41905A),
  62. const Color(0xFF7BF4AB),
  63. ];
  64. // 失败状态的背景渐变
  65. final Color _failedBgStartColor = const Color(0xFF1E1215);
  66. final Color _failedBgEndColor = const Color(0xFFAA4140);
  67. // 失败状态的边框渐变
  68. final List<Color> _failedBorderColors = [
  69. const Color(0xFF4B3033),
  70. const Color(0xFFDC7A7F),
  71. const Color(0xFFFFBDBB),
  72. ];
  73. @override
  74. void onOffsetChange(double offset) {
  75. // 圆形完全显示需要的距离:topMargin(12) + diameter(30) = 42
  76. const double circleFullyVisibleOffset = 42.0;
  77. // 更新当前下拉距离(但在特定状态下固定为完整圆形的距离)
  78. if (mounted) {
  79. setState(() {
  80. // 如果正在刷新、成功、失败状态,固定 offset 为完整圆形的距离
  81. if (mode == RefreshStatus.refreshing ||
  82. mode == RefreshStatus.completed ||
  83. mode == RefreshStatus.failed) {
  84. _currentOffset = circleFullyVisibleOffset;
  85. } else {
  86. _currentOffset = offset;
  87. }
  88. });
  89. }
  90. // 如果正在刷新、成功、失败状态,保持圆形状态但不强制设置值
  91. // 让 readyToRefresh 的动画自然完成
  92. if (mode == RefreshStatus.refreshing ||
  93. mode == RefreshStatus.completed ||
  94. mode == RefreshStatus.failed) {
  95. // 不在这里强制设置值,避免打断动画造成闪烁
  96. return;
  97. }
  98. // 如果动画正在执行(收缩动画),不打断它
  99. if (_animationController!.isAnimating) {
  100. return;
  101. }
  102. // 跟随 offset 变化
  103. if (offset <= circleFullyVisibleOffset) {
  104. // 第一阶段:圆形逐渐显示,不变形
  105. _animationController!.value = 0.0;
  106. } else {
  107. // 第二阶段:圆形完全显示后,继续下拉变成圆柱
  108. final double cylinderOffset = offset - circleFullyVisibleOffset;
  109. _animationController!.value = cylinderOffset.clamp(0.0, 50.0);
  110. }
  111. }
  112. @override
  113. Future<void> readyToRefresh() {
  114. // 使用短时间的快速动画收缩到圆形
  115. return _animationController!.animateTo(
  116. 0.0,
  117. duration: Duration(milliseconds: 150), // 快速收缩
  118. curve: Curves.easeOut,
  119. );
  120. }
  121. @override
  122. void initState() {
  123. _dismissCtl = AnimationController(
  124. vsync: this,
  125. duration: Duration(milliseconds: 400),
  126. value: 1.0,
  127. );
  128. _animationController = AnimationController(
  129. vsync: this,
  130. lowerBound: 0.0,
  131. upperBound: 50.0,
  132. duration: Duration(milliseconds: 150), // 缩短动画时长以匹配快速回弹
  133. );
  134. _rotateCtl = AnimationController(
  135. vsync: this,
  136. duration: Duration(milliseconds: 1000),
  137. );
  138. _colorCtl = AnimationController(
  139. vsync: this,
  140. duration: Duration(milliseconds: 300),
  141. );
  142. super.initState();
  143. }
  144. @override
  145. void didChangeDependencies() {
  146. super.didChangeDependencies();
  147. // 预加载 SVG 图标,避免首次显示时闪烁
  148. _precacheSvgs();
  149. }
  150. /// 预加载所有状态使用的 SVG 图标
  151. void _precacheSvgs() {
  152. if (_svgsPrecached) return;
  153. _svgsPrecached = true;
  154. // 预缓存刷新状态的 SVG
  155. final refreshLoader = SvgAssetLoader(Assets.refreshCircle);
  156. svg.cache.putIfAbsent(
  157. refreshLoader.cacheKey(null),
  158. () => refreshLoader.loadBytes(null),
  159. );
  160. // 预缓存成功状态的 SVG
  161. final successLoader = SvgAssetLoader(Assets.successCircle);
  162. svg.cache.putIfAbsent(
  163. successLoader.cacheKey(null),
  164. () => successLoader.loadBytes(null),
  165. );
  166. // 预缓存失败状态的 SVG
  167. final failedLoader = SvgAssetLoader(Assets.failedCircle);
  168. svg.cache.putIfAbsent(
  169. failedLoader.cacheKey(null),
  170. () => failedLoader.loadBytes(null),
  171. );
  172. // 预缓存下拉箭头的 SVG
  173. final arrowLoader = SvgAssetLoader(Assets.arrowDownCircle);
  174. svg.cache.putIfAbsent(
  175. arrowLoader.cacheKey(null),
  176. () => arrowLoader.loadBytes(null),
  177. );
  178. }
  179. // 颜色过渡方法
  180. void _animateToColor({
  181. required Color bgStartColor,
  182. required Color bgEndColor,
  183. required List<Color> borderColors,
  184. }) {
  185. // 如果已经是目标颜色,不需要动画
  186. if (_currentBgStartColor == bgStartColor &&
  187. _currentBgEndColor == bgEndColor) {
  188. return;
  189. }
  190. final ColorTween bgStartTween = ColorTween(
  191. begin: _currentBgStartColor,
  192. end: bgStartColor,
  193. );
  194. final ColorTween bgEndTween = ColorTween(
  195. begin: _currentBgEndColor,
  196. end: bgEndColor,
  197. );
  198. void listener() {
  199. if (mounted) {
  200. setState(() {
  201. _currentBgStartColor = bgStartTween.evaluate(_colorCtl)!;
  202. _currentBgEndColor = bgEndTween.evaluate(_colorCtl)!;
  203. _currentBorderColors = borderColors; // 直接设置边框颜色
  204. });
  205. }
  206. }
  207. _colorCtl.removeListener(listener);
  208. _colorCtl.addListener(listener);
  209. _colorCtl.reset();
  210. _colorCtl.forward().then((_) {
  211. _colorCtl.removeListener(listener);
  212. });
  213. }
  214. @override
  215. bool needReverseAll() {
  216. return false;
  217. }
  218. @override
  219. Widget buildContent(BuildContext context, RefreshStatus? mode) {
  220. Widget? child;
  221. if (mode == RefreshStatus.refreshing) {
  222. // 刷新状态:启动旋转动画
  223. if (!_rotateCtl.isAnimating) {
  224. _rotateCtl.repeat();
  225. }
  226. // 确保可见
  227. if (_dismissCtl.value != 1.0) {
  228. _dismissCtl.value = 1.0;
  229. }
  230. // 确保动画值是0(圆形状态),但不强制设置避免闪烁
  231. // 如果动画还在执行,让它完成
  232. if (!_animationController!.isAnimating &&
  233. _animationController!.value != 0.0) {
  234. _animationController!.value = 0.0;
  235. }
  236. // 和 idle 状态一样显示圆形边框,但图标是旋转的 refresh
  237. const double circleRadius = 15.0;
  238. const double topMargin = 12.0;
  239. final double topCircleCenterY = topMargin + circleRadius;
  240. // 刷新状态强制使用圆形,避免第一帧动画值未归零导致的闪烁
  241. const double cylinderHeight = 0.0;
  242. final double bottomCircleCenterY = topCircleCenterY + cylinderHeight;
  243. final double arrowTopPosition =
  244. bottomCircleCenterY - 10.0; // 10是图标高度的一半(20/2)
  245. return widget.refresh ??
  246. FadeTransition(
  247. opacity: _dismissCtl,
  248. child: SizedBox(
  249. height: 60.0,
  250. child: Stack(
  251. clipBehavior: Clip.none,
  252. children: <Widget>[
  253. // 绘制圆形边框
  254. CustomPaint(
  255. painter: _GradientCircleToCylinderPainter(
  256. listener: _animationController,
  257. bgStartColor: _currentBgStartColor,
  258. bgEndColor: _currentBgEndColor,
  259. borderColors: _currentBorderColors,
  260. currentOffset: 42.0, // 强制使用完整圆形的 offset
  261. isDefaultStyle: true, // 刷新状态使用默认样式渐变方向
  262. forceCircle: true, // 强制显示圆形,避免闪烁
  263. ),
  264. child: Container(height: 60.0),
  265. ),
  266. // 旋转的刷新图标
  267. Positioned(
  268. left: 0,
  269. right: 0,
  270. top: arrowTopPosition,
  271. child: RotationTransition(
  272. turns: _rotateCtl,
  273. child: SvgPicture.asset(
  274. ReactiveTheme.isLightTheme
  275. ? Assets.refreshCircleDark
  276. : Assets.refreshCircle,
  277. width: 20,
  278. height: 20,
  279. ),
  280. ),
  281. ),
  282. ],
  283. ),
  284. ),
  285. );
  286. } else if (mode == RefreshStatus.completed) {
  287. // 停止旋转动画
  288. _rotateCtl.stop();
  289. // 重置 dismiss 控制器,确保可见
  290. if (_dismissCtl.value != 1.0) {
  291. _dismissCtl.value = 1.0;
  292. }
  293. // 和刷新状态一样显示圆形边框,但颜色变绿,图标变成成功图标
  294. const double circleRadius = 15.0;
  295. const double topMargin = 12.0;
  296. final double topCircleCenterY = topMargin + circleRadius;
  297. // 成功状态强制使用圆形
  298. const double cylinderHeight = 0.0;
  299. final double bottomCircleCenterY = topCircleCenterY + cylinderHeight;
  300. final double arrowTopPosition =
  301. bottomCircleCenterY - 10.0; // 10是图标高度的一半(20/2)
  302. return widget.complete ??
  303. FadeTransition(
  304. opacity: _dismissCtl,
  305. child: SizedBox(
  306. height: 60.0,
  307. child: Stack(
  308. clipBehavior: Clip.none,
  309. children: <Widget>[
  310. // 绘制圆形边框(绿色渐变)- 直接使用成功状态颜色
  311. CustomPaint(
  312. painter: _GradientCircleToCylinderPainter(
  313. listener: _animationController,
  314. bgStartColor: _successBgStartColor,
  315. bgEndColor: _successBgEndColor,
  316. borderColors: _successBorderColors,
  317. currentOffset: 42.0, // 强制使用完整圆形的 offset
  318. forceCircle: true, // 强制显示圆形
  319. ),
  320. child: Container(height: 60.0),
  321. ),
  322. // 成功图标
  323. Positioned(
  324. left: 0,
  325. right: 0,
  326. top: arrowTopPosition,
  327. child: SvgPicture.asset(
  328. Assets.successCircle,
  329. width: 20,
  330. height: 20,
  331. ),
  332. ),
  333. ],
  334. ),
  335. ),
  336. );
  337. } else if (mode == RefreshStatus.failed) {
  338. // 停止旋转动画
  339. _rotateCtl.stop();
  340. // 重置 dismiss 控制器,确保可见
  341. if (_dismissCtl.value != 1.0) {
  342. _dismissCtl.value = 1.0;
  343. }
  344. // 和刷新状态一样显示圆形边框,但颜色变红,图标变成失败图标
  345. const double circleRadius = 15.0;
  346. const double topMargin = 12.0;
  347. final double topCircleCenterY = topMargin + circleRadius;
  348. // 失败状态强制使用圆形
  349. const double cylinderHeight = 0.0;
  350. final double bottomCircleCenterY = topCircleCenterY + cylinderHeight;
  351. final double arrowTopPosition =
  352. bottomCircleCenterY - 10.0; // 10是图标高度的一半(20/2)
  353. return widget.failed ??
  354. FadeTransition(
  355. opacity: _dismissCtl,
  356. child: SizedBox(
  357. height: 60.0,
  358. child: Stack(
  359. clipBehavior: Clip.none,
  360. children: <Widget>[
  361. // 绘制圆形边框(红色渐变)- 直接使用失败状态颜色
  362. CustomPaint(
  363. painter: _GradientCircleToCylinderPainter(
  364. listener: _animationController,
  365. bgStartColor: _failedBgStartColor,
  366. bgEndColor: _failedBgEndColor,
  367. borderColors: _failedBorderColors,
  368. currentOffset: 42.0, // 强制使用完整圆形的 offset
  369. forceCircle: true, // 强制显示圆形
  370. ),
  371. child: Container(height: 60.0),
  372. ),
  373. // 失败图标
  374. Positioned(
  375. left: 0,
  376. right: 0,
  377. top: arrowTopPosition,
  378. child: SvgPicture.asset(
  379. Assets.failedCircle,
  380. width: 20,
  381. height: 20,
  382. ),
  383. ),
  384. ],
  385. ),
  386. ),
  387. );
  388. } else if (mode == RefreshStatus.idle || mode == RefreshStatus.canRefresh) {
  389. // 停止旋转动画,恢复原始颜色
  390. _rotateCtl.stop();
  391. _animateToColor(
  392. bgStartColor: _defaultBgStartColor,
  393. bgEndColor: _defaultBgEndColor,
  394. borderColors: _defaultBorderColors,
  395. );
  396. // 重置 dismiss 控制器,确保可见
  397. if (_dismissCtl.value != 1.0) {
  398. _dismissCtl.value = 1.0;
  399. }
  400. // 计算箭头位置 - 始终在底部圆(30x30)的中心
  401. const double circleRadius = 15.0;
  402. const double topMargin = 12.0;
  403. final double topCircleCenterY = topMargin + circleRadius; // 顶部圆心Y坐标 = 27
  404. // 圆柱高度
  405. final double cylinderHeight = _animationController?.value ?? 0.0;
  406. // 箭头在底部圆的中心:顶部圆心 + 圆柱延伸高度
  407. // 当cylinderHeight=0时,箭头在顶部圆中心
  408. // 当cylinderHeight>0时,箭头在底部圆中心(底部圆心 = 顶部圆心 + 圆柱高度)
  409. final double bottomCircleCenterY = topCircleCenterY + cylinderHeight;
  410. final double arrowTopPosition =
  411. bottomCircleCenterY - 10.0; // 10是图标高度的一半(20/2),让图标居中
  412. return FadeTransition(
  413. opacity: _dismissCtl,
  414. child: SizedBox(
  415. height: 60.0,
  416. child: Stack(
  417. clipBehavior: Clip.none,
  418. children: <Widget>[
  419. // 绘制圆形到圆柱形的渐变边框
  420. CustomPaint(
  421. painter: _GradientCircleToCylinderPainter(
  422. listener: _animationController,
  423. bgStartColor: _currentBgStartColor,
  424. bgEndColor: _currentBgEndColor,
  425. borderColors: _currentBorderColors,
  426. currentOffset: _currentOffset,
  427. isDefaultStyle: true, // idle/canRefresh 状态使用默认样式渐变方向
  428. ),
  429. child: Container(height: 60.0),
  430. ),
  431. // 箭头在底部圆(30x30)的中心
  432. Positioned(
  433. left: 0,
  434. right: 0,
  435. top: arrowTopPosition,
  436. child: SvgPicture.asset(
  437. ReactiveTheme.isLightTheme
  438. ? Assets.arrowDownCircleDark
  439. : Assets.arrowDownCircle,
  440. width: 20,
  441. height: 20,
  442. ),
  443. ),
  444. ],
  445. ),
  446. ),
  447. );
  448. }
  449. return SizedBox(height: 60.0, child: Center(child: child));
  450. }
  451. @override
  452. void resetValue() {
  453. _animationController!.reset();
  454. _dismissCtl.value = 1.0;
  455. _currentOffset = 0.0;
  456. _rotateCtl.reset();
  457. _colorCtl.reset();
  458. // 重置为原始颜色
  459. _currentBgStartColor = _defaultBgStartColor;
  460. _currentBgEndColor = _defaultBgEndColor;
  461. _currentBorderColors = List.from(_defaultBorderColors);
  462. }
  463. @override
  464. void dispose() {
  465. _dismissCtl.dispose();
  466. _animationController!.dispose();
  467. _rotateCtl.dispose();
  468. _colorCtl.dispose();
  469. super.dispose();
  470. }
  471. }
  472. /// 绘制从圆形到圆柱形的渐变边框效果
  473. class _GradientCircleToCylinderPainter extends CustomPainter {
  474. final Animation<double>? listener;
  475. final Color bgStartColor;
  476. final Color bgEndColor;
  477. final List<Color> borderColors;
  478. final double currentOffset; // 当前下拉距离
  479. final bool isDefaultStyle; // 是否为默认/刷新状态的样式
  480. final bool forceCircle; // 强制显示圆形(用于 refreshing/completed/failed 状态)
  481. double get value => forceCircle ? 0.0 : listener!.value;
  482. _GradientCircleToCylinderPainter({
  483. this.listener,
  484. required this.bgStartColor,
  485. required this.bgEndColor,
  486. required this.borderColors,
  487. required this.currentOffset,
  488. this.isDefaultStyle = false,
  489. this.forceCircle = false,
  490. }) : super(repaint: forceCircle ? null : listener); // forceCircle 时不需要监听动画
  491. @override
  492. void paint(Canvas canvas, Size size) {
  493. final double middleW = size.width / 2;
  494. final double circleRadius = 15.0; // 圆的半径(改为15)
  495. final double strokeWidth = 2.0; // 边框宽度
  496. final double topMargin = 12.0; // 顶部边距
  497. const double circleFullyVisibleOffset = 42.0; // 圆形完全显示需要的距离(12 + 30 = 42)
  498. // 圆心位置
  499. final double circleCenterY = topMargin + circleRadius;
  500. // 计算下拉的距离,控制圆柱的高度
  501. final double pullDistance = value;
  502. // 背景渐变色
  503. final Gradient backgroundGradient = LinearGradient(
  504. colors: [bgStartColor, bgEndColor],
  505. // 默认/刷新状态:从右到左,其他状态:从上到下
  506. begin: isDefaultStyle ? Alignment.centerRight : Alignment.topCenter,
  507. end: isDefaultStyle ? Alignment.centerLeft : Alignment.bottomCenter,
  508. );
  509. // 边框渐变色(支持多个颜色)
  510. final Gradient borderGradient = LinearGradient(
  511. colors: borderColors,
  512. // 默认/刷新状态:从下到上(反转),其他状态:从上到下
  513. begin: isDefaultStyle ? Alignment.bottomCenter : Alignment.topCenter,
  514. end: isDefaultStyle ? Alignment.topCenter : Alignment.bottomCenter,
  515. );
  516. // 阴影效果
  517. final Paint shadowPaint = Paint()
  518. ..color = Colors.black.withOpacity(0.1)
  519. ..maskFilter = MaskFilter.blur(BlurStyle.normal, 2);
  520. // 第一阶段:圆形逐渐显示(使用裁剪)
  521. if (currentOffset < circleFullyVisibleOffset) {
  522. canvas.save();
  523. // 裁剪显示区域,让圆形从顶部逐渐出现
  524. canvas.clipRect(Rect.fromLTWH(0, 0, size.width, currentOffset));
  525. // 绘制阴影
  526. canvas.drawCircle(
  527. Offset(middleW, circleCenterY + 1),
  528. circleRadius,
  529. shadowPaint,
  530. );
  531. // 绘制背景
  532. final Rect bgRect = Rect.fromCircle(
  533. center: Offset(middleW, circleCenterY),
  534. radius: circleRadius,
  535. );
  536. final Paint bgPaint = Paint()
  537. ..shader = backgroundGradient.createShader(bgRect)
  538. ..style = PaintingStyle.fill;
  539. canvas.drawCircle(Offset(middleW, circleCenterY), circleRadius, bgPaint);
  540. // 绘制边框(渐变)
  541. final Paint borderPaint = Paint()
  542. ..shader = borderGradient.createShader(bgRect)
  543. ..style = PaintingStyle.stroke
  544. ..strokeWidth = strokeWidth
  545. ..strokeCap = StrokeCap.round;
  546. canvas.drawCircle(
  547. Offset(middleW, circleCenterY),
  548. circleRadius,
  549. borderPaint,
  550. );
  551. canvas.restore();
  552. } else {
  553. // 第二阶段:绘制圆柱形效果(包括 pullDistance = 0 的情况)
  554. if (pullDistance <= 0.1) {
  555. // pullDistance很小时,只绘制圆形
  556. // 绘制阴影
  557. canvas.drawCircle(
  558. Offset(middleW, circleCenterY + 1),
  559. circleRadius,
  560. shadowPaint,
  561. );
  562. // 绘制背景
  563. final Rect bgRect = Rect.fromCircle(
  564. center: Offset(middleW, circleCenterY),
  565. radius: circleRadius,
  566. );
  567. final Paint bgPaint = Paint()
  568. ..shader = backgroundGradient.createShader(bgRect)
  569. ..style = PaintingStyle.fill;
  570. canvas.drawCircle(
  571. Offset(middleW, circleCenterY),
  572. circleRadius,
  573. bgPaint,
  574. );
  575. // 绘制边框(渐变)
  576. final Paint borderPaint = Paint()
  577. ..shader = borderGradient.createShader(bgRect)
  578. ..style = PaintingStyle.stroke
  579. ..strokeWidth = strokeWidth
  580. ..strokeCap = StrokeCap.round;
  581. canvas.drawCircle(
  582. Offset(middleW, circleCenterY),
  583. circleRadius,
  584. borderPaint,
  585. );
  586. } else {
  587. // 绘制圆柱
  588. Path path = Path();
  589. // 绘制顶部半圆(上半部分)
  590. path.addArc(
  591. Rect.fromCircle(
  592. center: Offset(middleW, circleCenterY),
  593. radius: circleRadius,
  594. ),
  595. -3.14159, // π (左侧)
  596. 3.14159, // π (到右侧)
  597. );
  598. // 右侧垂直线
  599. path.lineTo(middleW + circleRadius, circleCenterY + pullDistance);
  600. // 绘制底部半圆
  601. path.addArc(
  602. Rect.fromCircle(
  603. center: Offset(middleW, circleCenterY + pullDistance),
  604. radius: circleRadius,
  605. ),
  606. 0, // 0 (右侧)
  607. 3.14159, // π (到左侧)
  608. );
  609. // 左侧垂直线(回到起点)
  610. path.lineTo(middleW - circleRadius, circleCenterY);
  611. // 绘制阴影
  612. canvas.drawPath(path.shift(Offset(0, 1)), shadowPaint);
  613. // 绘制背景
  614. final Rect bgRect = Rect.fromLTWH(
  615. middleW - circleRadius,
  616. circleCenterY - circleRadius,
  617. circleRadius * 2,
  618. circleRadius * 2 + pullDistance,
  619. );
  620. final Paint bgPaint = Paint()
  621. ..shader = backgroundGradient.createShader(bgRect)
  622. ..style = PaintingStyle.fill;
  623. canvas.drawPath(path, bgPaint);
  624. // 绘制边框(渐变)
  625. final Paint borderPaint = Paint()
  626. ..shader = borderGradient.createShader(bgRect)
  627. ..style = PaintingStyle.stroke
  628. ..strokeWidth = strokeWidth
  629. ..strokeCap = StrokeCap.round;
  630. canvas.drawPath(path, borderPaint);
  631. }
  632. }
  633. }
  634. @override
  635. bool shouldRepaint(CustomPainter oldDelegate) {
  636. return true;
  637. }
  638. }