Commit b73c5efda4e94c39b732b39cabb28da43c2c9746

Authored by 常守达
1 parent 6dc9dd6c

feat(hw): 鸿蒙版睡眠健身圆环

... ... @@ -52,6 +52,27 @@ class ArcMultiProgressWidget extends StatefulWidget {
/// 动画曲线
final Curve curve;
/// 超过 1 圈时端头阴影颜色 (默认半透明黑)
final Color shadowColor;
/// 阴影模糊半径 (越大越发散,越小越清晰,默认 2.2)
final double shadowBlur;
/// 阴影顺时针偏移距离 (像素,默认 0.8)
final double shadowOffset;
/// 合环三角形宽度占 size 的比例 (默认 15.27 / 50 ≈ 0.3054)
final double triangleWidthRatio;
/// 合环三角形高度占 size 的比例 (默认 8.27 / 50 ≈ 0.1654)
final double triangleHeightRatio;
/// 合环三角形向下溢出底部的比例 (占 size 的比例,默认 0.0)
final double triangleBottomOverflowRatio;
/// 合环三角形圆角半径占 size 的比例 (默认 1.4 / 50 ≈ 0.028)
final double triangleCornerRadiusRatio;
const ArcMultiProgressWidget({
super.key,
required this.items,
... ... @@ -63,6 +84,13 @@ class ArcMultiProgressWidget extends StatefulWidget {
this.animate = true,
this.duration = const Duration(milliseconds: 1000),
this.curve = Curves.easeOutCubic,
this.shadowColor = const Color.fromARGB(173, 0, 0, 0), // alpha ~ 0.35
this.shadowBlur = 2.2,
this.shadowOffset = 0.8,
this.triangleWidthRatio = 0.42,
this.triangleHeightRatio = 0.22,
this.triangleBottomOverflowRatio = 0.04,
this.triangleCornerRadiusRatio = 0.02,
});
@override
... ... @@ -81,8 +109,7 @@ class _ArcMultiProgressWidgetState extends State<ArcMultiProgressWidget>
if (item.ratio == null || item.ratio! <= 0) {
return 0.0;
}
final clampedRatio = min(item.ratio!, 1.0);
return clampedRatio * totalRad;
return item.ratio! * totalRad;
}).toList();
}
... ... @@ -118,7 +145,6 @@ class _ArcMultiProgressWidgetState extends State<ArcMultiProgressWidget>
}
final startList = List<double>.from(fromAngles);
// 确保长度一致
while (startList.length < toAngles.length) {
startList.add(0.0);
}
... ... @@ -167,6 +193,13 @@ class _ArcMultiProgressWidgetState extends State<ArcMultiProgressWidget>
gap: gap,
startAngleRad: startAngleRad,
totalSweepRad: totalSweepRad,
shadowColor: widget.shadowColor,
shadowBlur: widget.shadowBlur,
shadowOffset: widget.shadowOffset,
triangleWidthRatio: widget.triangleWidthRatio,
triangleHeightRatio: widget.triangleHeightRatio,
triangleBottomOverflowRatio: widget.triangleBottomOverflowRatio,
triangleCornerRadiusRatio: widget.triangleCornerRadiusRatio,
),
),
);
... ... @@ -180,6 +213,13 @@ class _ArcMultiProgressPainter extends CustomPainter {
final double gap;
final double startAngleRad;
final double totalSweepRad;
final Color shadowColor;
final double shadowBlur;
final double shadowOffset;
final double triangleWidthRatio;
final double triangleHeightRatio;
final double triangleBottomOverflowRatio;
final double triangleCornerRadiusRatio;
_ArcMultiProgressPainter({
required this.animatedProgresses,
... ... @@ -188,6 +228,13 @@ class _ArcMultiProgressPainter extends CustomPainter {
required this.gap,
required this.startAngleRad,
required this.totalSweepRad,
required this.shadowColor,
required this.shadowBlur,
required this.shadowOffset,
required this.triangleWidthRatio,
required this.triangleHeightRatio,
required this.triangleBottomOverflowRatio,
required this.triangleCornerRadiusRatio,
});
@override
... ... @@ -202,7 +249,7 @@ class _ArcMultiProgressPainter extends CustomPainter {
final rect = Rect.fromCircle(center: center, radius: radius);
// 1. 绘制底轨
// 1. 绘制底轨 (Track)
final hasData = item.ratio != null && item.ratio! > 0;
final bgColor = (!hasData && item.noDataBackgroundColor != null)
? item.noDataBackgroundColor!
... ... @@ -216,19 +263,170 @@ class _ArcMultiProgressPainter extends CustomPainter {
canvas.drawArc(rect, startAngleRad, totalSweepRad, false, trackPaint);
// 2. 绘制进度
final progressSweep =
i < animatedProgresses.length ? animatedProgresses[i] : 0.0;
if (progressSweep > 0.001) {
final progressPaint = Paint()
..color = item.color
..strokeWidth = strokeWidth
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
canvas.drawArc(rect, startAngleRad, progressSweep, false, progressPaint);
// 2. 绘制进度 (Progress)
final sweep = i < animatedProgresses.length ? animatedProgresses[i] : 0.0;
if (sweep <= 0.001) continue;
final fullLaps = (sweep / totalSweepRad).floor();
final remainingSweep = sweep - fullLaps * totalSweepRad;
final progressPaint = Paint()
..color = item.color
..strokeWidth = strokeWidth
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
// ① 如果至少满一圈,先绘制完整的底层圆弧
if (fullLaps >= 1) {
canvas.drawArc(
rect, startAngleRad, totalSweepRad, false, progressPaint);
}
// ② 绘制超出的部分(或第一圈未满的部分)
if (fullLaps >= 1) {
if (remainingSweep > 0.01) {
final currentEndAngle = startAngleRad + remainingSweep;
// 沿切线前进方向微小偏移
final tangentOffsetX = -shadowOffset * sin(currentEndAngle);
final tangentOffsetY = shadowOffset * cos(currentEndAngle);
final shadowCenter = Offset(
center.dx + radius * cos(currentEndAngle) + tangentOffsetX,
center.dy + radius * sin(currentEndAngle) + tangentOffsetY,
);
final capCenter = Offset(
center.dx + radius * cos(currentEndAngle),
center.dy + radius * sin(currentEndAngle),
);
// 构造当前圆环轨道的裁剪区域,严格限制阴影只在环槽内显示,绝不溢出到外圈或内圈
final trackClipPath = Path()
..addOval(Rect.fromCircle(
center: center, radius: radius + strokeWidth / 2))
..addOval(Rect.fromCircle(
center: center, radius: radius - strokeWidth / 2))
..fillType = PathFillType.evenOdd;
// 绘制被精准裁剪的自然阴影
canvas.save();
canvas.clipPath(trackClipPath);
final shadowPaint = Paint()
..color = shadowColor
..style = PaintingStyle.fill
..maskFilter = MaskFilter.blur(BlurStyle.normal, shadowBlur);
canvas.drawCircle(shadowCenter, strokeWidth / 2, shadowPaint);
canvas.restore();
// 绘制第二层圆弧(带圆角端头,压在阴影上)
canvas.drawArc(
rect, startAngleRad, remainingSweep, false, progressPaint);
// 显式绘制顶层端帽实心圆
final capFillPaint = Paint()
..color = item.color
..style = PaintingStyle.fill;
canvas.drawCircle(capCenter, strokeWidth / 2, capFillPaint);
}
} else {
// 第一圈未满,直接绘制
canvas.drawArc(rect, startAngleRad, sweep, false, progressPaint);
}
}
// 3. 当所有环都合环(>= 100%)时,在底部中央开口处绘制圆润的合环指示三角形
final isAllCompleted = items.isNotEmpty &&
List.generate(items.length, (idx) {
final sw =
idx < animatedProgresses.length ? animatedProgresses[idx] : 0.0;
return sw >= totalSweepRad - 0.01;
}).every((completed) => completed);
if (isAllCompleted && count > 0) {
// 按照 size 的比例计算宽度、高度、溢出与圆角
final triangleWidth = size.width * triangleWidthRatio;
final triangleHeight = size.height * triangleHeightRatio;
final halfWidth = triangleWidth / 2;
final outerRadius = (size.width - strokeWidth) / 2;
// 以最外环端点下边缘(合环底部最低点)为 0 基准
final ringBottomY =
center.dy + outerRadius * sin(startAngleRad) + strokeWidth / 2;
final overflow = size.height * triangleBottomOverflowRatio;
final bottomY = ringBottomY + overflow;
final topY = bottomY - triangleHeight;
final topPoint = Offset(center.dx, topY);
final rightPoint = Offset(center.dx + halfWidth, bottomY);
final leftPoint = Offset(center.dx - halfWidth, bottomY);
final cornerRadius =
size.width * (triangleCornerRadiusRatio ?? (1.4 / 50.0));
final roundedTrianglePath = _createRoundedTrianglePath(
top: topPoint,
right: rightPoint,
left: leftPoint,
cornerRadius: cornerRadius,
);
final trianglePaint = Paint()
..color = items.first.color
..style = PaintingStyle.fill;
canvas.drawPath(roundedTrianglePath, trianglePaint);
}
}
/// 绘制平滑圆角三角形
Path _createRoundedTrianglePath({
required Offset top,
required Offset right,
required Offset left,
required double cornerRadius,
}) {
final path = Path();
final points = [top, right, left];
final n = points.length;
for (int i = 0; i < n; i++) {
final p0 = points[(i - 1 + n) % n];
final p1 = points[i];
final p2 = points[(i + 1) % n];
final v1 = Offset(p0.dx - p1.dx, p0.dy - p1.dy);
final v2 = Offset(p2.dx - p1.dx, p2.dy - p1.dy);
final d1 = sqrt(v1.dx * v1.dx + v1.dy * v1.dy);
final d2 = sqrt(v2.dx * v2.dx + v2.dy * v2.dy);
final u1 = Offset(v1.dx / d1, v1.dy / d1);
final u2 = Offset(v2.dx / d2, v2.dy / d2);
final dot = (u1.dx * u2.dx + u1.dy * u2.dy).clamp(-1.0, 1.0);
final angle = acos(dot);
final cutDist = cornerRadius / tan(angle / 2);
final cut1 = Offset(p1.dx + u1.dx * cutDist, p1.dy + u1.dy * cutDist);
final cut2 = Offset(p1.dx + u2.dx * cutDist, p1.dy + u2.dy * cutDist);
if (i == 0) {
path.moveTo(cut1.dx, cut1.dy);
} else {
path.lineTo(cut1.dx, cut1.dy);
}
path.arcToPoint(
cut2,
radius: Radius.circular(cornerRadius),
clockwise: true,
);
}
path.close();
return path;
}
@override
... ...