48 情绪折线图EmotionTrendChart 完整实现前言图48 情绪折线图EmotionTrendChart 完整实现 运行效果截图HarmonyOS NEXT除了雷达图情绪趋势折线图是 App 中另一个核心 Canvas 可视化组件。它在TrendPage趋势页和个人中心中展示用户近 30 天的情绪能量变化。本文以 [EmotionTrendChart.ets](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/common/components/EmotionTrendChart.ets) 为例完整解析 Canvas 折线图的实现——从坐标映射、渐变填充到逐段绘制动画。鸿蒙官方·Canvas 渐变文档developer.huawei.com项目源码[EmotionTrendChart.ets](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/common/components/EmotionTrendChart.ets)图EmotionTrendChart 绘制流程——坐标映射 → 渐变填充区域 → 折线描边 → 数据点标记30天情绪数据坐标映射时间轴 × 分值轴绘制背景渐变区域createLinearGradient绘制折线moveTo/lineTo绘制数据点arc 小圆点绘制 X 轴标签日期文字入场动画逐段绘制 progress一、图表整体结构1.1 最终效果100 ────────────────────────────── 75 ──────────╮────────────────── 50 ─────╮────╯──────╮─────────── 25 ─────╯───────────╯─────────── 0 ────────────────────────────── 6/01 6/15 6/30 ┌──────┐ ┌──────┐ ┌──────┐ ┌────┐ │ 暖色 │ │ 静色 │ │ 柔色 │ │ 静 │ ← 四色情绪区 └──────┘ └──────┘ └──────┘ └────┘1.2 数据接口exportinterfaceTrendPoint{label:string// 横轴标签如 6/01value:number// 0-100 的情绪能量值}// 实际数据示例this.trendData[{label:6/01,value:62},{label:6/03,value:55},{label:6/05,value:68},// ...{label:6/30,value:72}]二、布局与坐标系2.1 内边距与绘图区constWthis.chartWidth// 画布宽度 320constHthis.chartHeight// 画布高度 180// 四边留白constpadL28// 左纵轴刻度constpadR14// 右留白constpadT14// 上留白constpadB24// 下横轴标签// 内部绘图区实际绘图范围constinnerWW-padL-padR// 320-28-14 278constinnerHH-padT-padB// 180-14-24 1422.2 坐标映射// 数据点坐标转换conststepXinnerW/Math.max(1,this.data.length-1)// stepX相邻数据点的水平间距constallPoints:ChartPoint[]this.data.map((p,i):ChartPoint({x:padLstepX*i,// X 左边界 间距 × 索引y:padTinnerH*(1-p.value/100)// Y 顶部 高度 × (1-数值比例)}))Y 轴公式解释// 因为 Canvas Y 轴向下为正所以// value100 → 图表顶部padT// value0 → 图表底部padT innerH// 即y padT innerH * (1 - value/100)三、情绪色块背景3.1 四色分区图表背景被分为四个色块区域对应情绪的四种状态privatestaticreadonlyZONES:Zone[][{w:0.200,fill:#F0E1D0},// 暖色{w:0.267,fill:#E1E8E5},// 静色{w:0.233,fill:#E8DCDC},// 柔色{w:0.300,fill:#E1E8E5}// 静色]3.2 绘制代码// 绘制四色背景块letzxpadLthis.zones.forEach(z{constzwMath.round(z.w*innerW)// 色块宽度 绘图区宽度 × 比例ctx.fillStylez.fill ctx.globalAlpha0.5// 50% 透明度ctx.fillRect(zx,padT,zw,innerH)// 填充矩形zxzw// 移动到下一个色块起始位置})ctx.globalAlpha1// 恢复透明度色块比例颜色情绪含义暖色20.0%#F0E1D0积极/温暖静色26.7%#E1E8E5平静/中性柔色23.3%#E8DCDC柔和/犹豫静色30.0%#E1E8E5平静/中性四、参考线与纵轴刻度4.1 虚线参考线4 条水平虚线将图表 5 等分0, 25, 50, 75, 100// 设置虚线样式ctx.strokeStyleAppColors.LINEctx.lineWidth1ctx.setLineDash([4,4])// 实线 4px间隔 4pxfor(leti0;i4;i){constypadT(innerH*i)/4// 等分 Y 坐标ctx.beginPath()ctx.moveTo(padL,y)// 从左边界开始ctx.lineTo(padLinnerW,y)// 到右边界结束ctx.stroke()}ctx.setLineDash([])// 恢复实线4.2 纵轴刻度标签ctx.fillStyleAppColors.TEXT_3ctx.font10px HarmonyOS Sans SCctx.textAlignrightctx.textBaselinemiddleconstticks[100,75,50,25,0]// 5 个刻度值ticks.forEach((t,i){constypadT(innerH*i)/4ctx.fillText(${t},padL-6,y)// 刻度在参考线左侧 6px})五、折线主体绘制5.1 从左到右逐段绘制通过drawProgress控制可见数据点的数量实现从左到右的逐段绘制动画// 计算当前可见点数constvisibleCountMath.floor(this.drawProgress*this.data.length)if(visibleCount0)return// 截取可见点constpointsallPoints.slice(0,visibleCount)constlastPointpoints[points.length-1]5.2 渐变填充在折线下方创建渐变填充区域// 创建垂直渐变constgradctx.createLinearGradient(0,padT,0,padTinnerH)grad.addColorStop(0,rgba(168,144,122,0.32))// 顶部较深grad.addColorStop(1,rgba(168,144,122,0.02))// 底部几乎透明// 构建填充路径折线 底部闭合ctx.beginPath()ctx.moveTo(allPoints[0].x,padTinnerH)// 从左下角开始points.forEach((p)ctx.lineTo(p.x,p.y))// 沿数据点向上ctx.lineTo(lastPoint.x,padTinnerH)// 回到底部ctx.closePath()ctx.fillStylegrad ctx.fill()填充路径示意图╱╲ ╱╲ ╱ ╲ ╱ ╲ ╱ ╲╱ ╲____________________ ← 沿底部闭合5.3 主轨迹线ctx.beginPath()points.forEach((p,i){if(i0)ctx.moveTo(p.x,p.y)elsectx.lineTo(p.x,p.y)})ctx.strokeStyleAppColors.PRIMARY// 品牌色 #A8907Actx.lineWidth2ctx.stroke()5.4 数据点points.forEach((p,i){ctx.beginPath()// 最后一个数据点更大highlight且仅当动画完成时ctx.arc(p.x,p.y,iallPoints.length-1this.drawProgress1?4:2.5,0,Math.PI*2)ctx.fillStyleiallPoints.length-1this.drawProgress1?AppColors.PRIMARY_DEEP// 最后一个点深色突出:AppColors.PRIMARY// 普通点品牌色ctx.fill()})数据点大小规则条件半径颜色普通点2.5pxPRIMARY最后一个点动画未完成2.5pxPRIMARY最后一个点动画已完成4pxPRIMARY_DEEP六、横轴标签6.1 仅显示首/中/末三个标签为避免拥挤只显示 3 个横轴标签// 只显示首、中、尾三个标签constlabelIdx[0,Math.floor(this.data.length/2),this.data.length-1]ctx.fillStyleAppColors.TEXT_3ctx.textAligncenterctx.textBaselinetoplabelIdx.forEach((i){// 只在可见范围内的标签才绘制if(this.data[i]ivisibleCount){ctx.fillText(this.data[i].label,allPoints[i].x,padTinnerH6)}})6.2 为什么只显示三个标签方案效果全部显示30 天 × 每天一个标签 → 重叠无法阅读 ❌指定间隔如每隔 5 天显示 → 逻辑复杂首/中/末三个清晰、简洁、足够表达时间范围 ✅七、完整的绘制流程7.1 绘制顺序第 1 层四色情绪背景块fillRect 第 2 层虚线参考线 × 4setLineDash 第 3 层纵轴刻度 × 5fillText 第 4 层渐变填充区域createLinearGradient fill 第 5 层主折线轨迹lineWidth2 stroke 第 6 层数据点arc fill 第 7 层横轴标签fillText × 37.2 绘制函数完整结构privatedraw():void{// 1. 清空画布ctx.clearRect(0,0,W,H)// 2. 四色背景区this.drawZones()// 3. 参考线 纵轴this.drawGridLines()this.drawYTicks()// 4. 数据折线渐变填充 轨迹 数据点if(this.data.length0)returnthis.drawLine()// 5. 横轴标签this.drawXLabels()}八、动画整合8.1 从左到右的绘制动画// aboutToAppear 触发动画aboutToAppear():void{animateTo({duration:AppAnimations.CHART_DRAW_DURATION,// 1200mscurve:AppAnimations.CURVE_SMOOTH},(){this.drawProgress1})}// Watch 监听StateWatch(onDrawProgressChange)drawProgress:number0onDrawProgressChange():void{this.draw()}// draw() 中使用 drawProgressconstvisibleCountMath.floor(this.drawProgress*this.data.length)// visibleCount 从 0 逐渐增长到 data.length// 从而实现从左到右逐段绘制8.2 动画时序T0ms drawProgress0, visibleCount0 → 空画布 T300ms drawProgress0.25, visibleCount8 → 前 8 个数据点 T600ms drawProgress0.5, visibleCount15 → 一半数据 T900ms drawProgress0.75, visibleCount23 → 大部分 T1200ms drawProgress1, visibleCount30 → 完成最后一个点放大九、复用情况EmotionTrendChart 在两个页面中复用9.1 TrendPage趋势页// TrendPage.ets — 展示 30 天情绪趋势EmotionTrendChart({data:this.trendData,chartWidth:320,chartHeight:180})9.2 个人中心统计概览// MePage.ets — 最近 7 天概览同组件不同尺寸EmotionTrendChart({data:this.weeklyData,chartWidth:260,chartHeight:140})9.3 设计要点总结特性实现说明渐变填充createLinearGradient顶部 rgba(168,144,122,0.32) → 底部 0.02虚线参考线setLineDash([4,4])4 条水平线5 等分逐段动画drawProgress × data.length从左到右逐点显现最后一个点高亮半径 4px 深色指示当前位置纵轴刻度手动计算 5 个刻度100/75/50/25/0横轴标签首/中/末三个避免拥挤总结本文完整解析了 EmotionTrendChart 情绪折线图的实现布局系统四边留白padL/padR/padT/padB 内部绘图区色块背景四色情绪分区暖/静/柔/静比例共 100%参考系统4 条虚线参考线 5 个纵轴刻度折线绘制渐变填充底部 主轨迹线 数据点动画实现drawProgress控制可见点数 → 从左到右逐段绘制下一篇文章将介绍MatchRing 匹配度圆环绘制——双人合盘匹配度的环形进度图。如果这篇文章对你有帮助欢迎点赞、收藏⭐、关注你的支持是我持续创作的动力参考资源createLinearGradient 渐变setLineDash 虚线设置arc 弧线绘制[EmotionTrendChart 组件源码](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/common/components/EmotionTrendChart.ets)[TrendPage 趋势页源码](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/pages/TrendPage.ets)[AppAnimations 动画常量](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/common/theme/AppAnimations.ets)HarmonyOS 开发文档