gradient_circle_header.dart 23 KB

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