实战:用 PathEffect 画虚线——地图标注线

在地图 APP 里,你经常能看到虚线标注——比如从 A 点到 B 点的路线、某个区域的边界线、或者 POI(兴趣点)的标注线。今天我们用 PathEffect 来实现这种效果。

虚线绘制流程

下面是用PathEffect绘制虚线的完整流程:

创建Path路径

定义路径点坐标

创建Pen画笔

设置画笔颜色和宽度

创建虚线效果createDashPathEffect

设置intervals线段和间隔

应用到画笔setPathEffect

attachPen到Canvas

strokePath绘制路径

detachPen释放画笔

PathEffect 是什么?

PathEffect 是路径特效,它可以改变路径的绘制方式。比如:

  • 虚线:把实线变成虚线
  • 路径重复:沿路径重复绘制图案
  • 路径变形:让路径扭曲、变形

今天我们主要用虚线效果。

第一步:创建虚线效果

import { PathEffect } from '@kit.ArkGraphics2D';

// 创建虚线效果
// 参数:[线段长度, 间隔长度]
let dashEffect = PathEffect.createDashPathEffect([10, 5], 0);

createDashPathEffect(intervals, phase) 的参数:

  • intervals:一个数组,交替表示"画"和"不画"的长度。[10, 5] 表示画 10 像素,空 5 像素,再画 10 像素,再空 5 像素…
  • phase:偏移量。设为 0 就从头开始,设为其他值可以调整虚线的起始位置。

第二步:设置地图背景

import { common } from '@kit.AbilityKit';
import { CanvasRenderingContext2D, Path, PathEffect, Pen } from '@kit.ArkGraphics2D';

导入需要的模块。

drawMapAnnotation() {
  const ctx = this.context;
  const width = 350;
  const height = 400;

  ctx.clearRect(0, 0, width, height);

清空画布。

  // 模拟地图背景(灰色网格)
  ctx.strokeStyle = '#e8e8e8';
  ctx.lineWidth = 1;
  for (let x = 0; x < width; x += 30) {
    ctx.beginPath();
    ctx.moveTo(x, 0);
    ctx.lineTo(x, height);
    ctx.stroke();
  }
  for (let y = 0; y < height; y += 30) {
    ctx.beginPath();
    ctx.moveTo(0, y);
    ctx.lineTo(width, y);
    ctx.stroke();
  }

画一个灰色网格模拟地图背景。每 30 像素画一条线。

第三步:定义地点和画路线

  // 定义两个地点
  const pointA = { x: 80, y: 300, name: '起点' };
  const pointB = { x: 270, y: 100, name: '终点' };

定义起点和终点的坐标。

  // 画实线路线(主路线)
  let mainPath = new Path();
  mainPath.moveTo(pointA.x, pointA.y);
  mainPath.cubicTo(
    pointA.x + 50, pointA.y - 100,  // 控制点1
    pointB.x - 50, pointB.y + 100,  // 控制点2
    pointB.x, pointB.y              // 终点
  );

用三次贝塞尔曲线画主路线,从 A 点到 B 点是一条平滑的曲线。

  // 设置画笔
  let mainPen = new Pen();
  mainPen.setColor({ alpha: 255, red: 33, green: 150, blue: 243 });  // 蓝色
  mainPen.setStrokeWidth(4);
  ctx.attachPen(mainPen);
  ctx.strokePath(mainPath);
  ctx.detachPen();

用蓝色画笔画主路线,宽度 4 像素。

第四步:画虚线标注

  // 画虚线标注(距离标注线)
  let annotationPath = new Path();
  annotationPath.moveTo(pointA.x, pointA.y);
  annotationPath.lineTo(pointA.x, pointA.y - 60);
  annotationPath.lineTo(pointB.x, pointB.y - 60);
  annotationPath.lineTo(pointB.x, pointB.y);

画一个"几"字形的标注线:从 A 点向上,水平延伸到 B 点上方,再向下到 B 点。

  let dashPen = new Pen();
  dashPen.setColor({ alpha: 180, red: 150, green: 150, blue: 150 });  // 灰色半透明
  dashPen.setStrokeWidth(1);
  dashPen.setPathEffect(PathEffect.createDashPathEffect([8, 4], 0));

创建虚线画笔。setPathEffect 应用虚线效果,[8, 4] 表示画 8 像素、空 4 像素。

  ctx.attachPen(dashPen);
  ctx.strokePath(annotationPath);
  ctx.detachPen();

用虚线画笔画标注线。

第五步:画标记点

  // 画起点标记
  this.drawMarker(ctx, pointA.x, pointA.y, '#4CAF50', 'A');

  // 画终点标记
  this.drawMarker(ctx, pointB.x, pointB.y, '#f44336', 'B');

调用辅助方法画起点和终点的标记。

drawMarker(ctx: CanvasRenderingContext2D, x: number, y: number, color: string, label: string) {
  // 外圈
  ctx.beginPath();
  ctx.arc(x, y, 15, 0, Math.PI * 2);
  ctx.fillStyle = color;
  ctx.fill();

  // 内圈
  ctx.beginPath();
  ctx.arc(x, y, 8, 0, Math.PI * 2);
  ctx.fillStyle = '#ffffff';
  ctx.fill();

  // 标签
  ctx.fillStyle = '#ffffff';
  ctx.font = 'bold 12px sans-serif';
  ctx.textAlign = 'center';
  ctx.textBaseline = 'middle';
  ctx.fillText(label, x, y);
}

标记由外圈(彩色)、内圈(白色)、标签文字组成。

第六步:画 POI 标注线

  // 显示距离信息
  ctx.fillStyle = '#333333';
  ctx.font = '14px sans-serif';
  ctx.textAlign = 'center';
  ctx.fillText('2.5 公里', (pointA.x + pointB.x) / 2, pointA.y - 65);

在标注线上方显示距离。

  // 画 POI 标注线
  this.drawPOIAnnotation(ctx, 200, 200, '餐厅', '#ff9800');
  this.drawPOIAnnotation(ctx, 150, 150, '商场', '#9c27b0');
}

画两个 POI(兴趣点)的标注。

drawPOIAnnotation(ctx: CanvasRenderingContext2D, x: number, y: number, name: string, color: string) {
  // POI 点
  ctx.beginPath();
  ctx.arc(x, y, 6, 0, Math.PI * 2);
  ctx.fillStyle = color;
  ctx.fill();

画 POI 的小圆点。

  // 标注线(虚线)
  let labelX = x + 40;
  let labelY = y - 30;

  let annotationPath = new Path();
  annotationPath.moveTo(x, y);
  annotationPath.lineTo(labelX, labelY);

  let pen = new Pen();
  pen.setColor({ alpha: 200, red: 100, green: 100, blue: 100 });
  pen.setStrokeWidth(1);
  pen.setPathEffect(PathEffect.createDashPathEffect([4, 3], 0));
  ctx.attachPen(pen);
  ctx.strokePath(annotationPath);
  ctx.detachPen();

用虚线连接 POI 点和标签。

  // 标签背景
  ctx.fillStyle = color;
  this.roundRect(ctx, labelX, labelY - 12, 50, 20, 4);
  ctx.fill();

  // 标签文字
  ctx.fillStyle = '#ffffff';
  ctx.font = '12px sans-serif';
  ctx.textAlign = 'center';
  ctx.textBaseline = 'middle';
  ctx.fillText(name, labelX + 25, labelY - 2);
}

画标签的背景和文字。

不同虚线样式对比

通过调整intervals数组可以实现多种虚线效果:

标准虚线

点线交替

长虚线

点状

长短交替

虚线样式选择

intervals参数

[10, 5]

[15, 5, 5, 5]

[20, 10]

[2, 6]

[20, 5, 10, 5]

画10空5循环

画15空5画5空5循环

画20空10循环

画2空6循环

画20空5画10空5循环

不同的虚线样式

你可以通过调整 intervals 数组来实现不同的虚线效果:

// 标准虚线
PathEffect.createDashPathEffect([10, 5], 0);

// 点线交替
PathEffect.createDashPathEffect([15, 5, 5, 5], 0);

// 长虚线
PathEffect.createDashPathEffect([20, 10], 0);

// 点状
PathEffect.createDashPathEffect([2, 6], 0);

// 长短交替
PathEffect.createDashPathEffect([20, 5, 10, 5], 0);

intervals 数组可以有任意多个值,Canvas 会循环使用。比如 [15, 5, 5, 5] 表示:画 15 像素,空 5 像素,画 5 像素,空 5 像素,然后重复。

phase 参数的妙用

phase 参数可以偏移虚线的起始位置。这在做动画时很有用——通过不断改变 phase,可以让虚线"流动"起来:

@State dashPhase: number = 0;

drawAnimatedDash() {
  let effect = PathEffect.createDashPathEffect([10, 5], this.dashPhase);
  // ...
}

地图标注线组成

一个完整的地图标注线包含多个元素:

地图标注线

主路线

虚线标注

标记点

POI标注

贝塞尔曲线平滑路径

蓝色实线绘制

距离标注线

灰色虚线

起点标记绿色

终点标记红色

内外圈圆形

POI小圆点

虚线连接标签

标签背景和文字

小结

地图标注线的核心:

  1. Path 画路线和标注线
  2. PathEffect.createDashPathEffect 创建虚线效果
  3. Pen.setPathEffect 应用到画笔
  4. 调整 intervals 数组控制虚线样式

虚线是 UI 设计的基础元素,地图标注只是其中一个应用场景。你还可以用它画表格边框、分隔线、选区边框等等。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐