|
|
@@ -0,0 +1,166 @@
|
|
|
+import 'dart:math';
|
|
|
+
|
|
|
+import 'package:flutter/material.dart';
|
|
|
+import 'package:keyboard/utils/common_expand.dart';
|
|
|
+
|
|
|
+import '../resource/assets.gen.dart';
|
|
|
+
|
|
|
+class HeartFillAnimation extends StatefulWidget {
|
|
|
+ const HeartFillAnimation({
|
|
|
+ super.key,
|
|
|
+ required this.fillProgress,
|
|
|
+ this.width = 250.0,
|
|
|
+ });
|
|
|
+
|
|
|
+ final double fillProgress;
|
|
|
+ final double width;
|
|
|
+
|
|
|
+ @override
|
|
|
+ State createState() => _HeartFillAnimationState();
|
|
|
+}
|
|
|
+
|
|
|
+class _HeartFillAnimationState extends State<HeartFillAnimation>
|
|
|
+ with SingleTickerProviderStateMixin {
|
|
|
+ late AnimationController _waveController;
|
|
|
+
|
|
|
+ @override
|
|
|
+ void initState() {
|
|
|
+ super.initState();
|
|
|
+ _waveController = AnimationController(
|
|
|
+ vsync: this,
|
|
|
+ duration: const Duration(milliseconds: 1000),
|
|
|
+ )..repeat();
|
|
|
+ }
|
|
|
+
|
|
|
+ @override
|
|
|
+ void dispose() {
|
|
|
+ _waveController.dispose();
|
|
|
+ super.dispose();
|
|
|
+ }
|
|
|
+
|
|
|
+ @override
|
|
|
+ Widget build(BuildContext context) {
|
|
|
+ final height = widget.width * 208 / 250;
|
|
|
+ return Stack(
|
|
|
+ alignment: Alignment.center,
|
|
|
+ children: [
|
|
|
+ AnimatedBuilder(
|
|
|
+ animation: _waveController,
|
|
|
+ builder: (context, child) {
|
|
|
+ return CustomPaint(
|
|
|
+ size: Size(widget.width, height),
|
|
|
+ painter: HeartFillPainter(
|
|
|
+ fillProgress: widget.fillProgress,
|
|
|
+ waveOffset: -_waveController.value * 2 * pi,
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ },
|
|
|
+ ),
|
|
|
+ Image(
|
|
|
+ image: Assets.images.bgHeartFill.provider(),
|
|
|
+ width: widget.width,
|
|
|
+ height: height,
|
|
|
+ fit: BoxFit.fill,
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ );
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+class HeartFillPainter extends CustomPainter {
|
|
|
+ HeartFillPainter({required this.fillProgress, required this.waveOffset});
|
|
|
+
|
|
|
+ final double fillProgress;
|
|
|
+ final double waveOffset;
|
|
|
+
|
|
|
+ static final _originalHeartPath = _createOriginalHeartPath();
|
|
|
+
|
|
|
+ static Path _createOriginalHeartPath() {
|
|
|
+ final path = Path();
|
|
|
+ path.moveTo(182.981, 0);
|
|
|
+ path.cubicTo(158.332, 0, 136.822, 13.4803, 125.435, 33.4817);
|
|
|
+ path.cubicTo(125.07, 34.1387, 124.121, 34.1387, 123.756, 33.4817);
|
|
|
+ path.cubicTo(112.344, 13.4803, 90.8336, 0, 66.1847, 0);
|
|
|
+ path.cubicTo(29.6371, 0, 0, 29.6371, 0, 66.1847);
|
|
|
+ path.cubicTo(0, 134.504, 85.9676, 188.719, 115.078, 205.119);
|
|
|
+ path.cubicTo(121.041, 208.478, 128.123, 208.473, 134.081, 205.106);
|
|
|
+ path.cubicTo(163.184, 188.66, 249.166, 134.307, 249.166, 66.1847);
|
|
|
+ path.cubicTo(249.166, 29.6371, 219.529, 0, 182.981, 0);
|
|
|
+ return path;
|
|
|
+ }
|
|
|
+
|
|
|
+ @override
|
|
|
+ void paint(Canvas canvas, Size size) {
|
|
|
+ // 应用缩放矩阵
|
|
|
+ final scaleX = size.width / 250;
|
|
|
+ final scaleY = size.height / 208;
|
|
|
+ final matrix = Matrix4.identity()..scale(scaleX, scaleY);
|
|
|
+
|
|
|
+ // 创建缩放后的爱心路径
|
|
|
+ final heartPath = _originalHeartPath.transform(matrix.storage);
|
|
|
+
|
|
|
+ // 设置裁剪区域
|
|
|
+ canvas.save();
|
|
|
+ canvas.clipPath(heartPath);
|
|
|
+
|
|
|
+ // 计算填充高度
|
|
|
+ final fillHeight = size.height * (1 - fillProgress);
|
|
|
+ final fillRect = Rect.fromLTRB(0, fillHeight, size.width, size.height);
|
|
|
+
|
|
|
+ // 绘制次级波浪
|
|
|
+ final subWavePath = _createWavePath(
|
|
|
+ fillHeight,
|
|
|
+ size,
|
|
|
+ offset: -pi / 5,
|
|
|
+ amplitude: 15,
|
|
|
+ frequency: 0.02,
|
|
|
+ );
|
|
|
+ canvas.drawPath(
|
|
|
+ subWavePath,
|
|
|
+ Paint()
|
|
|
+ ..shader = LinearGradient(
|
|
|
+ colors: ["#66FFB9D5".color, "#66FF219B".color],
|
|
|
+ ).createShader(fillRect),
|
|
|
+ );
|
|
|
+
|
|
|
+ // 绘制主波浪
|
|
|
+ final wavePath = _createWavePath(fillHeight, size);
|
|
|
+ canvas.drawPath(
|
|
|
+ wavePath,
|
|
|
+ Paint()
|
|
|
+ ..shader = LinearGradient(
|
|
|
+ colors: ["#FF458C".color, "#FF74BC".color],
|
|
|
+ ).createShader(fillRect),
|
|
|
+ );
|
|
|
+
|
|
|
+ canvas.restore();
|
|
|
+ }
|
|
|
+
|
|
|
+ Path _createWavePath(
|
|
|
+ double fillHeight,
|
|
|
+ Size size, {
|
|
|
+ double offset = 0,
|
|
|
+ double amplitude = 10.0,
|
|
|
+ double frequency = 0.03,
|
|
|
+ }) {
|
|
|
+ final path = Path()..moveTo(0, fillHeight);
|
|
|
+
|
|
|
+ for (double x = 0; x <= size.width; x++) {
|
|
|
+ final y =
|
|
|
+ fillHeight + sin(x * frequency + waveOffset + offset) * amplitude;
|
|
|
+ path.lineTo(x, y);
|
|
|
+ }
|
|
|
+
|
|
|
+ path
|
|
|
+ ..lineTo(size.width, size.height)
|
|
|
+ ..lineTo(0, size.height)
|
|
|
+ ..close();
|
|
|
+ return path;
|
|
|
+ }
|
|
|
+
|
|
|
+ @override
|
|
|
+ bool shouldRepaint(HeartFillPainter oldDelegate) {
|
|
|
+ return fillProgress != oldDelegate.fillProgress ||
|
|
|
+ waveOffset != oldDelegate.waveOffset;
|
|
|
+ }
|
|
|
+}
|