我们的目标是从this这个半圆环图变成that一个。正如您所看到的,我们希望保持
strokecap.sqaure
或
.butt
然后在盖子的角上实施圆角边缘。
这似乎不是直接可能的,我们现在的最后一个选择是以某种方式将 svg 编程到真实的图表中,或者以某种方式将边框半径烘焙到颤动路径中,但这都很丑陋,我们没有人有编程 svg 的经验.
目前已实施的版本:
import 'package:flutter/material.dart';
import 'dart:math' as math;
import '../../models/recipe.dart';
class RecipeCaloriesChart extends StatelessWidget {
final Recipe recipe;
final double size;
const RecipeCaloriesChart({
super.key,
required this.recipe,
this.size = 200,
});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: size,
height: size,
child: CustomPaint(
painter: _CaloriesArcPainter(
fatsPercentage: recipe.fatsPerc,
carbsPercentage: recipe.carbsPerc,
proteinPercentage: recipe.proteinPerc,
calories: recipe.calories,
),
),
),
_buildLegend(),
],
);
}
Widget _buildLegend() {
final textStyle = TextStyle(
fontSize: 14,
color: Colors.grey[600],
);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
children: [
_LegendItem(
color: const Color(0xFF808DA0),
label: 'Fats',
percentage: recipe.fatsPerc,
grams: recipe.fatG.round(),
textStyle: textStyle,
),
const SizedBox(height: 8),
_LegendItem(
color: const Color(0xFFA9B3C7),
label: 'Carbs',
percentage: recipe.carbsPerc,
grams: recipe.carbsG.round(),
textStyle: textStyle,
),
const SizedBox(height: 8),
_LegendItem(
color: const Color(0xFFAFC3DF),
label: 'Proteins',
percentage: recipe.proteinPerc,
grams: recipe.proteinG.round(),
textStyle: textStyle,
),
],
),
);
}
}
class _LegendItem extends StatelessWidget {
final Color color;
final String label;
final double percentage;
final int grams;
final TextStyle textStyle;
const _LegendItem({
required this.color,
required this.label,
required this.percentage,
required this.grams,
required this.textStyle,
});
@override
Widget build(BuildContext context) {
return Row(
children: [
Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
),
const SizedBox(width: 8),
Text(label, style: textStyle),
const Spacer(),
Text('${percentage.round()}%', style: textStyle),
const SizedBox(width: 16),
Text('${grams}g', style: textStyle),
],
);
}
}
class _CaloriesArcPainter extends CustomPainter {
final double fatsPercentage;
final double carbsPercentage;
final double proteinPercentage;
final double calories;
_CaloriesArcPainter({
required this.fatsPercentage,
required this.carbsPercentage,
required this.proteinPercentage,
required this.calories,
});
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final radius = size.width * 0.45;
final strokeWidth = size.width * 0.18;
const borderWidth = 10.0;
final rect = Rect.fromCircle(center: center, radius: radius);
const startAngle = -math.pi;
const gapAngle = math.pi / 48;
const totalGaps = gapAngle * 2;
const availableAngle = math.pi - totalGaps;
final fatsAngle = (fatsPercentage / 100) * availableAngle;
final carbsAngle = (carbsPercentage / 100) * availableAngle;
final proteinsAngle = (proteinPercentage / 100) * availableAngle;
_drawArcWithBorder(
canvas,
rect,
startAngle,
fatsAngle,
const Color(0xFF808DA0),
strokeWidth,
borderWidth,
);
_drawArcWithBorder(
canvas,
rect,
startAngle + fatsAngle + gapAngle,
carbsAngle,
const Color(0xFFA9B3C7),
strokeWidth,
borderWidth,
);
_drawArcWithBorder(
canvas,
rect,
startAngle + fatsAngle + carbsAngle + (2 * gapAngle),
proteinsAngle,
const Color(0xFFAFC3DF),
strokeWidth,
borderWidth,
);
final textPainter = TextPainter(
text: TextSpan(
text: calories.round().toString(),
style: TextStyle(
color: Colors.black,
fontSize: size.width * 0.15,
fontWeight: FontWeight.bold,
),
),
textDirection: TextDirection.ltr,
);
textPainter.layout();
textPainter.paint(
canvas,
Offset(
center.dx - textPainter.width / 2,
center.dy - textPainter.height / 2,
),
);
}
void _drawArcWithBorder(
Canvas canvas,
Rect rect,
double startAngle,
double sweepAngle,
Color color,
double strokeWidth,
double borderWidth,
) {
final paint = Paint()
..color = color
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth
..strokeCap = StrokeCap.butt;
canvas.drawArc(rect, startAngle, sweepAngle, false, paint);
final borderPaint = Paint()
..color = const Color(0x33FFFFFF)
..style = PaintingStyle.stroke
..strokeWidth = borderWidth
..strokeCap = StrokeCap.butt;
final innerRadius = rect.width / 2 - (strokeWidth / 2);
final outerRadius = rect.width / 2 + (strokeWidth / 2);
final center = Offset(rect.center.dx, rect.center.dy);
final innerBorderRect = Rect.fromCircle(center: center, radius: innerRadius);
final outerBorderRect = Rect.fromCircle(center: center, radius: outerRadius);
canvas.drawArc(innerBorderRect, startAngle, sweepAngle, false, borderPaint);
canvas.drawArc(outerBorderRect, startAngle, sweepAngle, false, borderPaint);
final startInnerPoint = Offset(
center.dx + innerRadius * math.cos(startAngle),
center.dy + innerRadius * math.sin(startAngle),
);
final startOuterPoint = Offset(
center.dx + outerRadius * math.cos(startAngle),
center.dy + outerRadius * math.sin(startAngle),
);
final endInnerPoint = Offset(
center.dx + innerRadius * math.cos(startAngle + sweepAngle),
center.dy + innerRadius * math.sin(startAngle + sweepAngle),
);
final endOuterPoint = Offset(
center.dx + outerRadius * math.cos(startAngle + sweepAngle),
center.dy + outerRadius * math.sin(startAngle + sweepAngle),
);
canvas.drawLine(startInnerPoint, startOuterPoint, borderPaint);
canvas.drawLine(endInnerPoint, endOuterPoint, borderPaint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
我们正在尝试实现的 svg :
<svg width="1000" height="475" viewBox="0 0 1000 475" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_2076_12939" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="1000" height="475">
<rect width="1000" height="474.081" rx="10" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_2076_12939)">
<mask id="path-2-inside-1_2076_12939" fill="white">
<path d="M16.6066 471.986C11.0913 471.697 6.84376 466.99 7.24418 461.482C12.6017 387.78 34.4621 316.179 71.2588 251.998C108.055 187.817 158.801 132.777 219.698 90.9167C224.249 87.7882 230.457 89.0757 233.493 93.6895L307.473 206.133C310.509 210.746 309.22 216.935 304.698 220.106C264.039 248.612 230.116 285.742 205.377 328.891C180.639 372.041 165.735 420.075 161.677 469.565C161.225 475.07 156.536 479.308 151.02 479.02L16.6066 471.986Z"/>
</mask>
<path d="M16.6066 471.986C11.0913 471.697 6.84376 466.99 7.24418 461.482C12.6017 387.78 34.4621 316.179 71.2588 251.998C108.055 187.817 158.801 132.777 219.698 90.9167C224.249 87.7882 230.457 89.0757 233.493 93.6895L307.473 206.133C310.509 210.746 309.22 216.935 304.698 220.106C264.039 248.612 230.116 285.742 205.377 328.891C180.639 372.041 165.735 420.075 161.677 469.565C161.225 475.07 156.536 479.308 151.02 479.02L16.6066 471.986Z" fill="#808DA0" stroke="white" stroke-opacity="0.2" stroke-width="10" mask="url(#path-2-inside-1_2076_12939)"/>
<mask id="path-3-inside-2_2076_12939" fill="white">
<path d="M252.414 81.8575C249.595 77.1085 251.155 70.9636 255.96 68.2412C309.343 37.9979 367.929 17.9691 428.715 9.21496C492.802 -0.0145766 558.079 3.46915 620.82 19.4672C683.561 35.4653 742.536 63.6644 794.379 102.455C843.551 139.247 885.393 184.883 917.776 236.996C920.691 241.687 919.118 247.828 914.369 250.648L792.138 323.209C787.389 326.028 781.267 324.454 778.305 319.793C756.822 285.976 729.361 256.317 697.238 232.282C662.445 206.248 622.864 187.323 580.757 176.586C538.649 165.849 494.839 163.511 451.828 169.705C412.118 175.424 373.808 188.312 338.755 207.713C333.923 210.388 327.795 208.837 324.975 204.088L252.414 81.8575Z"/>
</mask>
<path d="M252.414 81.8575C249.595 77.1085 251.155 70.9636 255.96 68.2412C309.343 37.9979 367.929 17.9691 428.715 9.21496C492.802 -0.0145766 558.079 3.46915 620.82 19.4672C683.561 35.4653 742.536 63.6644 794.379 102.455C843.551 139.247 885.393 184.883 917.776 236.996C920.691 241.687 919.118 247.828 914.369 250.648L792.138 323.209C787.389 326.028 781.267 324.454 778.305 319.793C756.822 285.976 729.361 256.317 697.238 232.282C662.445 206.248 622.864 187.323 580.757 176.586C538.649 165.849 494.839 163.511 451.828 169.705C412.118 175.424 373.808 188.312 338.755 207.713C333.923 210.388 327.795 208.837 324.975 204.088L252.414 81.8575Z" fill="#A9B3C7" stroke="white" stroke-opacity="0.2" stroke-width="10" mask="url(#path-3-inside-2_2076_12939)"/>
<mask id="path-4-inside-3_2076_12939" fill="white">
<path d="M928.71 276.586C933.623 274.063 939.661 275.997 942.083 280.961C969.543 337.219 986.038 398.194 990.686 460.624C991.096 466.132 986.856 470.846 981.342 471.144L846.94 478.413C841.426 478.711 836.729 474.48 836.268 468.976C832.856 428.254 822.098 388.485 804.514 351.597C802.138 346.611 804.061 340.589 808.974 338.067L928.71 276.586Z"/>
</mask>
<path d="M928.71 276.586C933.623 274.063 939.661 275.997 942.083 280.961C969.543 337.219 986.038 398.194 990.686 460.624C991.096 466.132 986.856 470.846 981.342 471.144L846.94 478.413C841.426 478.711 836.729 474.48 836.268 468.976C832.856 428.254 822.098 388.485 804.514 351.597C802.138 346.611 804.061 340.589 808.974 338.067L928.71 276.586Z" fill="#AFC3DF" stroke="white" stroke-opacity="0.2" stroke-width="10" mask="url(#path-4-inside-3_2076_12939)"/>
</g>
</svg>
任何想法都将非常感激,这花费了我们比应有的时间更长的时间,而且我们的设计团队对此并不那么灵活。
我们尝试将圆形边缘烘焙到路径中(使用点),但我们似乎无法做到这一点(它影响了形状的其他部分)。我们也尝试过使用cliprect,但仍然无法做到这一点。我相信这与我们尝试过的内容无关,更多的是缺乏经验。
感谢 pskink 对我的问题的评论,这个问题已经解决:
这可能是一个很好的起点:pastebin.com/YTyCPVZd – pskink
非常感谢兄弟,请自己发布答案,以便我选择!