鸿蒙Flutter香道入门页面开发:扇形轮盘与合香古方
鸿蒙Flutter香道入门页面开发:扇形轮盘与合香古方
前言
香道文化中有一个核心概念叫"香型"——香气被归纳为花香、果香、木香、药香和树脂香五种基本类型,每种在原材中的占比关系决定了香品的整体气味特征。这天然适合用扇形图来呈现——360度圆形被划分为5段扇形,每段面积对应一种香型的比例。但普通的饼图方案在呈现传统文化主题时显得过于"商务化",需要一套更雅致的视觉语言:低透明度的扇形填充+描边边界+环内放射状文字+圆心竖排标题。配合合香古方(鹅梨帐中香、雪中春信等)的线装书排版和香品收藏的古书页式网格,整个页面营造出檀木棕+古铜金+宣纸白的禅意氛围。本文基于IncensePage的完整代码,在HarmonyOS 7.0平台上介绍扇形轮盘的drawArc绘制和香道文化页面的构建。
背景
香道入门应用的信息结构分为四个区域:今日品香卡片(推荐香品+香韵标签)、香型轮盘(5段扇形+放射状标注+底部图例)、香品收藏网格(2列×6种香品,含产地/年份/品级/香韵特征)和合香古方列表(4首古法配方,含配料/比例/制法)。其中香型轮盘需要Canvas绘制——因为扇形图的精确角度控制和环内文字排版超出了Wrap+Container的Widget布局能力。合香配方使用左侧金色竖线装饰的线装书式卡片,营造古书排版感。香品收藏网格使用2列Wrap布局,展示奇楠/一级/二级等品级标签和香韵文字。
Flutter × Harmony7.0 跨端开发介绍
IncensePage在鸿蒙平台上通过_AromaWheelPainter的CustomPainter实现扇形香型轮盘。Framework层管理静态的香品数据和配方数据——_incenses(6种香品)和_recipes(4首古方)以const List
AOT编译将5次drawArc调用(含useCenter=true的扇形填充和描边)和5次TextPainter的余弦/正弦坐标定位编译为ARM64原生机器码。_AromaWheelPainter的shouldRepaint始终返回false——香型轮盘是静态图表,仅需绘制一次。const修饰的香品和配方数据在编译期分配。IncensePage整个页面为静态内容(无状态切换,无用户输入),所有Widget在首次build时构建完成后不再变更。
开发核心代码
第一部分:5段扇形+放射状标注的Canvas轮盘绘制
_AromaWheelPainter的绘制基于两个同心圆——外圆半径outerR=60px用于扇形区域,内圆半径innerR=25px用于中心圆。5段扇形数据以元组(起始比例, 结束比例, 颜色, 标签)存储——花香25%(粉色)、果香15%(金色)、木香25%(绿色)、药香15%(紫色)、树脂香20%(古铜色)。每段扇形的绘制分三个层次:第一层用drawArc(useCenter=true)以15%透明度填充整个扇形区域;第二层用drawArc(useCenter=true, style=stroke)以40%透明度描边2px宽度沿扇形边界绘制;第三层用TextPainter在扇形中线的中间半径位置绘制放射状标签。
void paint(Canvas canvas, Size size) {
final cx = size.width/2, cy = size.height/2, outerR = 60.0, innerR = 25.0;
final segments = [
(0.0, 0.25, const Color(0xFFEC4899), '花香'), (0.25, 0.40, const Color(0xFFF59E0B), '果香'),
(0.40, 0.65, const Color(0xFF16A34A), '木香'), (0.65, 0.80, const Color(0xFF8B5CF6), '药香'),
(0.80, 1.0, const Color(0xFFB8860B), '树脂'),
];
for (final seg in segments) {
final startAngle = -math.pi/2 + seg.$1*2*math.pi;
final sweepAngle = (seg.$2 - seg.$1)*2*math.pi;
canvas.drawArc(Rect.fromCircle(center: Offset(cx,cy), radius: outerR),
startAngle, sweepAngle, true, Paint()..color = seg.$3.withValues(alpha: 0.15));
canvas.drawArc(Rect.fromCircle(center: Offset(cx,cy), radius: outerR),
startAngle, sweepAngle, true, Paint()..color = seg.$3.withValues(alpha: 0.4)..style = PaintingStyle.stroke..strokeWidth = 2);
final midAngle = startAngle + sweepAngle/2;
final labelR = (outerR + innerR)/2;
final tp = TextPainter(text: TextSpan(text: seg.$4, style: TextStyle(color: seg.$3, fontSize: 10, fontWeight: FontWeight.w800)), textDirection: TextDirection.ltr)..layout();
tp.paint(canvas, Offset(cx + labelR*math.cos(midAngle) - tp.width/2, cy + labelR*math.sin(midAngle) - tp.height/2));
}
canvas.drawCircle(Offset(cx,cy), innerR, Paint()..color = const Color(0xFFF5F0EB));
canvas.drawCircle(Offset(cx,cy), innerR, Paint()..color = const Color(0xFF5C4033)..style = PaintingStyle.stroke..strokeWidth = 1);
final ct = TextPainter(text: const TextSpan(text: '香\n型', style: TextStyle(color: Color(0xFF5C4033), fontSize: 11, fontWeight: FontWeight.w900)), textDirection: TextDirection.ltr)..layout();
ct.paint(canvas, Offset(cx - ct.width/2, cy - ct.height/2));
}
起始角度-startAngle = -π/2 + seg.$1 * 2π——从顶部12点钟位置(-π/2=270度)顺时针开始。useCenter=true使drawArc填充从圆心到弧线的完整扇形。标签的位置通过midAngle(扇形中线的角度)和labelR(内外半径的平均值)计算——cx+labelRcos(midAngle)和cy+labelRsin(midAngle)确定标签中心坐标,减去tp.width/2和tp.height/2实现居中。中心圆使用宣纸白色(0xFFF5F0EB)填充+檀木棕色(0xFF5C4033)1px描边,圆心以竖排两行TextPainter居中绘制"香\n型"标题。轮盘下方使用_LegendItem组件Row布局5个图例——8px彩色圆点+10sp标签文字。
第二部分:香品收藏的古书页式网格
香品收藏使用2列Wrap网格展示6种香品——海南沉香(奇楠/2015)、印度老山檀(一级/2012)、越南芽庄(二级/2018)、柬埔寨菩萨(一级/2016)、降真香(二级/2019)、太行崖柏(二级/2020)。每张卡片的宽度由(屏幕宽-68)/2动态计算,内部展示36×36圆角图标+名称+产地、品级标签(使用金棕色alpha:0.1背景+8sp文字)+年份、以及一行12sp的香韵特征描述(如"花香·蜜甜·凉韵")。
Wrap(spacing: 8, runSpacing: 8,
children: _incenses.map((i) {
return Container(width: (MediaQuery.of(context).size.width - 68)/2, padding: const EdgeInsets.all(14),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFE5D5C0))),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(children: [
Container(width: 36, height: 36, decoration: BoxDecoration(color: _incensePrimary.withValues(alpha: 0.06), borderRadius: BorderRadius.circular(10)),
alignment: Alignment.center, child: Text(i['icon'] as String, style: const TextStyle(fontSize: 16))),
SizedBox(width: 8),
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(i['name'] as String, style: TextStyle(color: _incensePrimary, fontSize: 12, fontWeight: FontWeight.w800)),
Text('${i['origin']}', style: TextStyle(color: Color(0xFF8B7355), fontSize: 9)),
])),
]),
SizedBox(height: 8),
Row(children: [
Container(padding: EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(color: _incenseGold.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(4)),
child: Text(i['grade'] as String, style: TextStyle(color: _incenseGold, fontSize: 8, fontWeight: FontWeight.w800))),
SizedBox(width: 6), Text('${i['year']}年', style: TextStyle(color: Color(0xFFA89880), fontSize: 9)),
]),
SizedBox(height: 4),
Text(i['notes'] as String, style: TextStyle(color: Color(0xFF6B5B4F), fontSize: 9, fontWeight: FontWeight.w500)),
]));
}).toList(),
)
卡片使用白色背景+淡米色边框(0xFFE5D5C0)+16px圆角。品级标签(奇楠/一级/二级)使用古铜金色背景+文字配4px小圆角——视觉上类似古书中标注版本等级的印鉴标记。年份信息使用灰色小字紧贴品级标签,香韵特征使用深棕灰9sp文字单行展示。整体排版采用竖向信息流——图标+名称→品级+年份→香韵——符合中国传统古籍的阅读顺序(从上到下、从大到小)。
第三部分:合香古方的线装书排版与今日品香卡片
合香古方列表使用纵向Column排列4首经典配方——鹅梨帐中香(南唐·蒸制法)、雪中春信(宋·炼蜜法)、二苏旧局(宋·蜜丸法)、宣和御制香(宋·炼蜜法)。每首配方使用左侧3px古铜金色竖线装饰(BorderDirectional的left: BorderSide)的线装书排版——左侧竖线模拟古书的装订线,卡片内容横向排列方名+朝代标签+制法标签,下方显示配料文本和比例数值。
Container(
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16),
border: Border(left: BorderSide(color: _incenseGold.withValues(alpha: 0.4), width: 3))),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(children: [
Text(r['name'] as String, style: TextStyle(color: _incensePrimary, fontSize: 14, fontWeight: FontWeight.w800)),
SizedBox(width: 8),
Container(padding: EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(color: _incensePrimary.withValues(alpha: 0.06), borderRadius: BorderRadius.circular(4)),
child: Text(r['era'] as String, style: TextStyle(color: _incensePrimary, fontSize: 9, fontWeight: FontWeight.w600))),
Spacer(),
Container(padding: EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(color: _incenseGold.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8)),
child: Text(r['method'] as String, style: TextStyle(color: _incenseGold, fontSize: 9, fontWeight: FontWeight.w700))),
]),
SizedBox(height: 8),
Row(children: [
const Icon(Icons.grass, color: Color(0xFF8B7355), size: 14), SizedBox(width: 4),
Expanded(child: Text('配料:${r['materials']}', style: TextStyle(color: Color(0xFF8B7355), fontSize: 11))),
Text('比例 ${r['ratio']}', style: TextStyle(color: _incensePrimary, fontSize: 11, fontWeight: FontWeight.w700)),
]),
]),
)
今日品香卡片使用檀木棕+古铜金的极低透明度对角线渐变(alpha:0.04→0.02)作为背景,配古铜金边框(alpha:0.15)。卡片左侧64×64区域展示品香香炉emoji+竖向小字"今日",右侧展示推荐香品"海南沉香·奇楠"(18sp深棕粗体)、品香参数"品香温度80°C·时长15分钟"和3个香韵标签(“🌸花香”“🍯蜜甜”“🌿凉韵”)——标签使用金棕色背景+8px圆角。标题栏使用"🪔"香炉emoji配"香道入门"标题和"一缕沉香·静心养性"副标题,右侧展示圆形"品香"印章(绛红边框+竖向两行文字)。
心得
drawArc的useCenter=true在扇形图绘制中的双重角色是香型轮盘实现中的关键认知。当useCenter=true时,drawArc不仅绘制弧线(sweepAngle范围),还从弧线两端向圆心绘制连接线,形成完整的扇形区域。填充层使用useCenter=true使每段扇形具有"面积"——这是香型比例数据(25%/15%/25%/15%/20%)的视觉载体。描边层同样使用useCenter=true——这确保了相邻扇形之间的半径边界线上也有描边覆盖,视觉上各段清晰分界。如果描边层使用useCenter=false,仅弧线段被描边而半径边界线没有描边,相邻扇形之间会出现未处理的接缝。
放射状标签的坐标计算(cx+labelRcos(midAngle)和cy+labelRsin(midAngle))依赖Flutter Canvas的坐标系——Y轴向下,角度从右侧3点钟方向顺时针递增。startAngle = -π/2(-90度,顶部12点钟)是扇形图的起始位置,正好与Mathematical的"0度右侧、逆时针递增"规则相配合。labelR = (outerR+innerR)/2将标签放置在外圆和内圆之间的环上,这个中间位置恰好是扇形段"最宽"的区域——离圆心和弧线边界都有足够间距,文字的视觉冲突最小。
线装书排版的左侧竖线装饰(BorderDirectional left: 3px金色半透明)是将传统书籍装帧形式转化为数字UI元素的典型案例。中国线装书的特点是在书脊一侧用线穿孔装订——将这一视觉特征简化为一条3px竖线放置在卡片左侧,既保留了"古籍"的文化暗示,又不干扰内容的可读性。4首合香古方均采用此种排版,在视觉上形成统一的"古籍系列"感,与常规的圆角矩形卡片形成明确的文化区分。
在鸿蒙适配方面,IncensePage的Canvas轮盘和Widget布局完全基于Flutter实现。Chinese fonts for 香品名称和配方配料文本在鸿蒙设备上与Android可能存在细微的字体度量差异——字符宽度和行高需要通过TextStyle的height和letterSpacing进行微调。如果未来需要接入香品数据库API动态加载收藏列表,_incenses和_recipes可以替换为从网络获取的FutureBuilder异步数据。
总结
本文以IncensePage为完整案例,展示了在HarmonyOS 7.0上使用Flutter构建香道入门页面的实现方案。核心技术包括:drawArc(useCenter=true)的5段扇形图绘制+放射状TextPainter坐标定位的香型轮盘、左侧3px金色竖线BorderDirectional的线装书式配方排版、以及2列Wrap网格+品级标签的古书页式香品收藏。所有Canvas绘制在首次构建时完成(shouldRepaint=false),后续不产生重绘开销。
香道页面的设计核心是"传统文化的数字化转译"——用扇形图的面积比例表达香型占比(这是数据可视化)、用线装书排版表达古方的历史感(这是文化视觉化)、用品级标签(奇楠/一级/二级)表达香品的等级体系(这是信息层级化)。Flutter提供了实现这三种转译的技术手段:Canvas的drawArc处理面积比例、BoxDecoration的BorderDirectional处理竖线装饰、Container的padding+borderRadius处理标签样式。这些技术手段的共同特征是它们在像素级别赋予开发者完整的控制权,使传统文化中的每一处视觉细节都可以通过代码精确表达——无需依赖设计师提供的图片素材,也无需受限于平台原生组件的样式约束。
更多推荐





所有评论(0)