小邢哥  | 14年编程老炮,拆解技术脉络,记录程序员的进化史


开篇唠嗑:为什么我要写这篇文章

大家好,我是小邢哥。

掐指一算,从2013年参加工作到现在,我已经在这个行业摸爬滚打了14年。从最早的Java Swing画圆画方,到Android的Canvas,再到Flutter的CustomPainter,再到现在的HarmonyOS ArkTS——画图形这件事,我这辈子估计画了不下十万个

前两天有个粉丝私信我:" 小邢哥,HarmonyOS 6.0的图形绘制我看官方文档看了三遍,还是云里雾里的,能不能给讲讲?"

我一看官方文档,嚯,写得确实很全面,但对新手来说,确实有点"学院派"。于是我决定用我这14年的经验,给大家来一篇接地气、能落地、有深度的技术科普。

今天这篇文章,我承诺:看完你就能在HarmonyOS 6.0下画出任何你想要的几何图形。

废话不多说,系好安全带,咱们发车!


第一章:先搞清楚——为什么要学绘制几何图形?

1.1 图形绘制:UI开发的"内功"

很多新手可能会问:"小邢哥,现在UI组件那么丰富,我直接用Button、Image不就行了吗?为什么要学画图形?"

好问题!

我来给你举几个真实的开发场景:

场景一:个性化的数据可视化

你接到一个需求:展示用户的健康数据,需要一个半圆形的进度条,进度条外围还要有刻度线。请问,系统组件里有这玩意儿吗?

没有!你只能自己画。

场景二:创意型的按钮设计

设计师给你甩了一个六边形的按钮,说这是最新的设计趋势。你找遍了组件库,发现所有按钮都是圆角矩形。

怎么办?自己画!

场景三:炫酷的引导动画

产品经理说新用户引导页要有一个聚光灯效果,只高亮某个区域,其他地方暗下来。这种不规则的遮罩效果,组件库里也没有。

还是得自己画!

所以你看,绑定几何图形绘制是UI开发的"内功"。组件库是"招式",用得好可以应付大多数场景;但遇到复杂需求,没有内功的人就只能干瞪眼了。

1.2 HarmonyOS 6.0的两条技术路线

在HarmonyOS 6.0的ArkTS框架下,官方给我们提供了两条技术路线来实现几何图形:

技术路线 核心工具 适用场景 特点
绘制组件 Shape、Circle、Rect、Line等 需要独立展示几何图形 直接生成图形组件
形状裁剪 clipShape属性 把现有组件裁剪成特定形状 改变组件的可见区域

打个比方:

  • 绘制组件就像是用画笔在白纸上画画,你画什么就是什么;

  • 形状裁剪就像是用剪刀把照片裁成特定形状,照片还是那张照片,只是你只能看到剪刀圈定的部分。

这两种方式各有千秋,接下来我会逐一深入讲解。


第二章:绘制组件Shape——你的专属画笔

2.1 Shape家族全景图

在HarmonyOS 6.0中,Shape相关的绘制组件一共有8个核心成员

Shape家族
├── Shape(容器组件,用于组合多个图形)
├── Circle(圆形)
├── Ellipse(椭圆)
├── Rect(矩形)
├── Line(直线)
├── Polyline(折线)
├── Polygon(多边形)
└── Path(自由路径,最灵活)

我给大家画一张能力分布图:

灵活度:Path > Polygon > Polyline > 其他基础图形
易用度:Circle/Rect > Ellipse > Line > Polyline > Polygon > Path
性能:基础图形 > 复杂图形 > Path

记住这个规律:越灵活的组件,学习成本越高,性能开销也越大

2.2 Circle——从最简单的圆形开始

2.2.1 基础用法

圆形是最简单的几何图形了,没有之一。在HarmonyOS中画一个圆,代码简洁到令人发指:

@Entry
@Component
struct Study1 {
  build() {
    Column({ space: 20 }) {
      // 最简单的圆形
      Circle({ width: 100, height: 100 })
        .fill(Color.Blue)
      
      Text('这是一个蓝色的圆')
        .fontSize(16)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

运行结果:屏幕中央出现一个直径100vp的蓝色圆形。

代码拆解:

  • Circle({ width: 100, height: 100 }):创建一个圆形,宽高都是100vp

  • .fill(Color.Blue):填充颜色为蓝色

是不是简单得有点不像话?

2.2.2 进阶:描边与填充

实际开发中,我们经常需要给圆形加上边框,或者只要边框不要填充:

@Entry
@Component
struct Study2 {
  build() {
    Column({ space: 30 }) {
      // 纯填充圆形
      Circle({ width: 80, height: 80 })
        .fill('#FF6B6B')
      Text('纯填充').fontSize(14)

      // 纯描边圆形(空心圆)
      Circle({ width: 80, height: 80 })
        .fill(Color.Transparent) // 填充透明
        .stroke('#4ECDC4')       // 描边颜色
        .strokeWidth(3)           // 描边宽度
      Text('纯描边').fontSize(14)

      // 填充+描边
      Circle({ width: 80, height: 80 })
        .fill('#FFE66D')
        .stroke('#FF6B6B')
        .strokeWidth(4)
      Text('填充+描边').fontSize(14)

      // 虚线描边
      Circle({ width: 80, height: 80 })
        .fill(Color.Transparent)
        .stroke('#95E1D3')
        .strokeWidth(3)
        .strokeDashArray([10, 5]) // 虚线:10vp实线,5vp间隔
      Text('虚线描边').fontSize(14)
    }
    .width('100%')
    .padding(20)
  }
}

重点属性解析:

属性 作用 取值示例
fill 填充颜色 Color.Blue、'#FF6B6B'、Color.Transparent
stroke 描边颜色 Color.Red、'#4ECDC4'
strokeWidth 描边宽度 数值,单位vp
strokeDashArray 虚线样式 [实线长度, 间隔长度]

2.2.3 小邢哥的踩坑笔记

坑点一:宽高不一致时会变成椭圆吗?

新手常问:如果我写Circle({ width: 100, height: 50 })会怎样?

答案:Circle永远是正圆!当宽高不一致时,它会取较小值作为直径。所以width: 100, height: 50的结果是一个直径50vp的圆,而不是椭圆。

想画椭圆?请用Ellipse组件!

坑点二:fill和stroke的默认值

如果你既不写fill也不写stroke,圆形默认会有一个黑色的填充。所以想画空心圆,一定要显式写fill(Color.Transparent)

2.3 Ellipse——当圆被"压扁"

椭圆就是"不那么圆的圆"。在数学上,椭圆有两个轴:长轴和短轴。在HarmonyOS中,椭圆的宽度对应横轴,高度对应纵轴。

@Entry
@Component
struct EllipseDemo {
  build() {
    Column({ space: 30 }) {
      // 横向椭圆(扁椭圆)
      Ellipse({ width: 150, height: 80 })
        .fill('#A8E6CF')
        .stroke('#3D5A80')
        .strokeWidth(2)
      Text('横向椭圆').fontSize(14)

      // 纵向椭圆(瘦椭圆)
      Ellipse({ width: 80, height: 150 })
        .fill('#FFD3B6')
        .stroke('#E07A5F')
        .strokeWidth(2)
      Text('纵向椭圆').fontSize(14)

      // 当宽高相等时,就是圆
      Ellipse({ width: 100, height: 100 })
        .fill('#DCEDC1')
        .stroke('#5C7AEA')
        .strokeWidth(2)
      Text('宽高相等=圆形').fontSize(14)
    }
    .width('100%')
    .padding(20)
  }
}

使用场景举例:

  • 设计师给的头像框是椭圆形的

  • 需要做一个跑道形状的背景

  • 数据可视化中的椭圆饼图

2.4 Rect——矩形,UI界的"劳模"

矩形可能是所有几何图形中使用频率最高的。按钮是矩形、卡片是矩形、输入框是矩形……可以说,现代UI就是建立在矩形基础上的

2.4.1 基础矩形
@Entry
@Component
struct RectDemo {
  build() {
    Column({ space: 20 }) {
      // 普通矩形
      Rect({ width: 150, height: 80 })
        .fill('#6C5CE7')
      Text('普通矩形').fontSize(14)

      // 正方形
      Rect({ width: 100, height: 100 })
        .fill('#00B894')
      Text('正方形').fontSize(14)
    }
    .width('100%')
    .padding(20)
  }
}

2.4.2 圆角矩形——UI设计的最爱

现代UI设计非常喜欢圆角,因为圆角给人柔和、友好的感觉。HarmonyOS的Rect组件提供了多种圆角设置方式:

@Entry
@Component
struct RoundRectDemo {
  build() {
    Column({ space: 25 }) {
      // 统一圆角
      Rect({ width: 150, height: 80 })
        .fill('#74B9FF')
        .radius(15) // 四个角统一15vp圆角
      Text('统一圆角').fontSize(14)

      // 分别设置四个角
      Rect({ width: 150, height: 80 })
        .fill('#FD79A8')
        .radiusWidth(20)  // 水平方向圆角半径
        .radiusHeight(10) // 垂直方向圆角半径
      Text('椭圆形圆角').fontSize(14)

      // 胶囊形状(圆角=短边的一半)
      Rect({ width: 150, height: 60 })
        .fill('#55EFC4')
        .radius(30) // 30 = 60/2,形成胶囊
      Text('胶囊形状').fontSize(14)

      // 超大圆角变成圆形
      Rect({ width: 100, height: 100 })
        .fill('#FFEAA7')
        .radius(50) // 50 = 100/2
      Text('圆形(矩形变的)').fontSize(14)
    }
    .width('100%')
    .padding(20)
  }
}

小邢哥的设计心得:

圆角大小的选择有讲究:

  • 圆角 = 4~8vp:微圆角,适合专业、严肃的场景

  • 圆角 = 12~16vp:中等圆角,通用性强,大多数App都这么干

  • 圆角 = 高度/2:胶囊形,适合按钮、标签

  • 圆角 = 尺寸/2:变成圆形

2.4.3 进阶:描边艺术

描边可以让矩形更有层次感:

@Entry
@Component
struct RectStrokeDemo {
  build() {
    Column({ space: 25 }) {
      // 实线描边
      Rect({ width: 140, height: 70 })
        .fill(Color.White)
        .stroke('#2D3436')
        .strokeWidth(2)
        .radius(10)
      Text('实线描边').fontSize(14)

      // 虚线描边
      Rect({ width: 140, height: 70 })
        .fill(Color.White)
        .stroke('#0984E3')
        .strokeWidth(2)
        .strokeDashArray([8, 4]) // 8vp实线,4vp间隔
        .radius(10)
      Text('虚线描边').fontSize(14)

      // 圆点虚线
      Rect({ width: 140, height: 70 })
        .fill(Color.White)
        .stroke('#E17055')
        .strokeWidth(3)
        .strokeDashArray([1, 6]) // 1vp实线,6vp间隔 → 圆点效果
        .strokeLineCap(LineCapStyle.Round)
        .radius(10)
      Text('圆点描边').fontSize(14)
    }
    .width('100%')
    .padding(20)
  }
}

strokeLineCap属性解析:

这个属性控制线条端点的样式,有三个选项:

效果 使用场景
LineCapStyle.Butt 平头,线条在端点处截断 默认值,普通线条
LineCapStyle.Round 圆头,端点有半圆 虚线变圆点
LineCapStyle.Square 方头,端点多出一个方块 特殊设计需求

2.5 Line——最基础的线段

Line组件用于绘制直线段。虽然简单,但在很多场景都有用武之地:分割线、进度条底线、连接线等。

@Entry
@Component
struct LineDemo {
  build() {
    Column({ space: 30 }) {
      // 水平线
      Line()
        .width(200)
        .height(2)
        .startPoint([0, 1])
        .endPoint([200, 1])
        .stroke('#636E72')
        .strokeWidth(2)
      Text('水平线').fontSize(14)

      // 垂直线
      Line()
        .width(2)
        .height(100)
        .startPoint([1, 0])
        .endPoint([1, 100])
        .stroke('#00CEC9')
        .strokeWidth(2)
      Text('垂直线').fontSize(14)

      // 斜线
      Line()
        .width(150)
        .height(80)
        .startPoint([0, 0])
        .endPoint([150, 80])
        .stroke('#FF7675')
        .strokeWidth(3)
      Text('斜线').fontSize(14)

      // 虚线
      Line()
        .width(200)
        .height(2)
        .startPoint([0, 1])
        .endPoint([200, 1])
        .stroke('#74B9FF')
        .strokeWidth(2)
        .strokeDashArray([10, 5])
      Text('虚线').fontSize(14)
    }
    .width('100%')
    .padding(30)
  }
}

关键点理解:

Line的坐标系以组件左上角为原点(0,0):

  • startPoint([x1, y1]):起点坐标

  • endPoint([x2, y2]):终点坐标

坐标值是相对于组件宽高的,所以通常需要配合设置width和height。

小邢哥提醒:

画水平或垂直线时,记得给线条留出strokeWidth的空间。比如画一条2vp粗的水平线,height至少要设为2,y坐标设为1(线条中心在y=1的位置)。

2.6 Polyline——折线,连接多个点

当你需要画一条"拐弯"的线时,Polyline就派上用场了。它可以依次连接多个点形成折线。

@Entry
@Component
struct PolylineDemo {
  build() {
    Column({ space: 30 }) {
      // 简单折线
      Polyline()
        .width(200)
        .height(100)
        .points([[0, 100], [50, 20], [100, 80], [150, 10], [200, 60]])
        .stroke('#E84393')
        .strokeWidth(3)
        .fill(Color.Transparent)
      Text('股票走势图效果').fontSize(14)

      // 阶梯线
      Polyline()
        .width(200)
        .height(100)
        .points([[0, 80], [40, 80], [40, 50], [80, 50], [80, 30], [120, 30], [120, 60], [160, 60], [160, 20], [200, 20]])
        .stroke('#00B894')
        .strokeWidth(2)
        .fill(Color.Transparent)
      Text('阶梯线').fontSize(14)

      // 填充的折线区域
      Polyline()
        .width(200)
        .height(100)
        .points([[0, 100], [0, 70], [50, 30], [100, 60], [150, 20], [200, 50], [200, 100]])
        .stroke('#0984E3')
        .strokeWidth(2)
        .fill('#74B9FF')
        .fillOpacity(0.3)
      Text('面积图效果').fontSize(14)
    }
    .width('100%')
    .padding(30)
  }
}

Polyline的核心属性:

  • points:点的数组,格式为[[x1,y1], [x2,y2], [x3,y3], ...]

  • stroke:线条颜色

  • fill:填充颜色(如果不想填充,设为Transparent)

  • fillOpacity:填充透明度,0-1之间

使用场景:

  • 股票/基金走势图

  • 温度变化曲线

  • 运动轨迹

  • 数据统计折线图

2.7 Polygon——多边形,封闭图形

Polygon和Polyline很像,区别在于:Polygon会自动把最后一个点和第一个点连接起来,形成封闭图形

@Entry
@Component
struct PolygonDemo {
  build() {
    Column({ space: 30 }) {
      // 三角形
      Polygon()
        .width(100)
        .height(90)
        .points([[50, 0], [100, 90], [0, 90]])
        .fill('#FF6B6B')
        .stroke('#C0392B')
        .strokeWidth(2)
      Text('三角形').fontSize(14)

      // 正五边形(近似)
      Polygon()
        .width(100)
        .height(100)
        .points([
          [50, 0],      // 顶点
          [97, 35],     // 右上
          [79, 90],     // 右下
          [21, 90],     // 左下
          [3, 35]       // 左上
        ])
        .fill('#4ECDC4')
        .stroke('#1ABC9C')
        .strokeWidth(2)
      Text('五边形').fontSize(14)

      // 六边形
      Polygon()
        .width(100)
        .height(87)
        .points([
          [50, 0],      // 上顶点
          [100, 22],    // 右上
          [100, 65],    // 右下
          [50, 87],     // 下顶点
          [0, 65],      // 左下
          [0, 22]       // 左上
        ])
        .fill('#A29BFE')
        .stroke('#6C5CE7')
        .strokeWidth(2)
      Text('六边形').fontSize(14)

      // 五角星
      Polygon()
        .width(100)
        .height(95)
        .points([
          [50, 0],       // 顶角
          [61, 35],
          [98, 35],      // 右上角
          [68, 57],
          [79, 95],      // 右下角
          [50, 72],
          [21, 95],      // 左下角
          [32, 57],
          [2, 35],       // 左上角
          [39, 35]
        ])
        .fill('#FFEAA7')
        .stroke('#F39C12')
        .strokeWidth(2)
      Text('五角星').fontSize(14)
    }
    .width('100%')
    .padding(30)
  }
}

正多边形的顶点坐标计算:

对于正N边形,如果圆心在(cx, cy),半径为r,则第i个顶点的坐标为:

xi = cx + r * sin(2π * i / N)
yi = cy - r * cos(2π * i / N)

注意:这里的坐标系y轴向下,所以用减号。

小邢哥的数学小课堂:

别被公式吓到!其实就是把圆等分成N份,每份对应一个顶点。

上面的五边形和六边形坐标,我就是这样算出来的。

当然,如果你数学不好,也可以用一些在线工具生成坐标,比如搜索"polygon coordinate generator"。

2.8 Path——终极武器,自由绘制

如果说前面的组件都是"格式化写作",那Path就是"自由创作"。它可以绘制任意形状的图形,是最灵活也是最复杂的绘制方式。

2.8.1 Path的核心概念:路径命令

Path通过一系列"命令"来描述图形的轮廓。这些命令用单个字母表示:

命令 含义 参数 示例
M Move to(移动到) x, y M 50 50
L Line to(画直线到) x, y L 100 100
H Horizontal line(水平线) x H 150
V Vertical line(垂直线) y V 80
A Arc(圆弧) rx ry rotation large-arc sweep x y A 25 25 0 0 1 100 50
Q Quadratic curve(二次贝塞尔曲线) cx cy x y Q 75 0 100 50
C Cubic curve(三次贝塞尔曲线) c1x c1y c2x c2y x y C 25 0 75 100 100 50
Z Close path(闭合路径) Z

大写字母表示绝对坐标,小写字母表示相对坐标(相对于当前位置的偏移)。

2.8.2 从简单开始:用Path画三角形
@Entry
@Component
struct PathTriangleDemo {
  build() {
    Column({ space: 20 }) {
      Path()
        .width(100)
        .height(100)
        .commands('M 50 10 L 90 90 L 10 90 Z')
        .fill('#FF6B6B')
        .stroke('#C0392B')
        .strokeWidth(2)
      
      Text('Path绘制的三角形').fontSize(14)
      
      // 命令解析
      Text('M 50 10 → 移动到顶点(50,10)')
        .fontSize(12).fontColor(Color.Gray)
      Text('L 90 90 → 画线到右下角(90,90)')
        .fontSize(12).fontColor(Color.Gray)
      Text('L 10 90 → 画线到左下角(10,90)')
        .fontSize(12).fontColor(Color.Gray)
      Text('Z → 闭合路径,回到起点')
        .fontSize(12).fontColor(Color.Gray)
    }
    .width('100%')
    .padding(30)
  }
}
2.8.3 画圆弧:A命令详解

圆弧命令是Path中最复杂的命令,但也是最常用的之一。让我详细解释一下:

A rx ry rotation large-arc-flag sweep-flag x y
  • rx, ry:椭圆的x轴和y轴半径

  • rotation:椭圆相对于x轴的旋转角度

  • large-arc-flag:0=小弧,1=大弧

  • sweep-flag:0=逆时针,1=顺时针

  • x, y:终点坐标

@Entry
@Component
struct Study9 {
  build() {
    Column({space: 20}) {
      // 小弧 + 顺时针
      Path()
        .width(100)
        .height(30)
        .commands('M 10 50 A 40 40 0 0 1 90 50')
        .fill(Color.Transparent)
        .stroke('#E84393')
        .strokeWidth(3)
      Text('小弧 + 顺时针').fontSize(14)
      // 大弧 + 顺时针
      Path()
        .width(100)
        .height(30)
        .commands('M 10 50 A 40 40 0 1 1 90 50')
        .fill(Color.Transparent)
        .stroke('#00CEC9')
        .strokeWidth(3)
      Text('大弧 + 顺时针').fontSize(14)
      // 半圆
      Path()
        .width(100)
        .height(30)
        .commands('M 5 50 A 45 45 0 0 1 95 50')
        .fill('#74B9FF')
        .stroke('#0984E3')
        .strokeWidth(2)
      Text('半圆').fontSize(14)
    }
    .width('100%')
    .padding(30)
  }
}

2.8.4 贝塞尔曲线:画出丝滑的曲线

贝塞尔曲线是绘制平滑曲线的利器。有两种:

二次贝塞尔曲线(Q命令):一个控制点

Q cx cy x y

三次贝塞尔曲线(C命令):两个控制点

C c1x c1y c2x c2y x y
@Entry
@Component
struct PathBezierDemo {
  build() {
    Column({ space: 30 }) {
      // 二次贝塞尔曲线
      Path()
        .width(150)
        .height(80)
        .commands('M 0 60 Q 75 0 150 60')
        .fill(Color.Transparent)
        .stroke('#6C5CE7')
        .strokeWidth(3)
      Text('二次贝塞尔曲线').fontSize(14)

      // 三次贝塞尔曲线
      Path()
        .width(150)
        .height(80)
        .commands('M 0 40 C 30 0 120 80 150 40')
        .fill(Color.Transparent)
        .stroke('#00B894')
        .strokeWidth(3)
      Text('三次贝塞尔曲线(S形)').fontSize(14)

      // 波浪线
      Path()
        .width(200)
        .height(50)
        .commands('M 0 25 Q 25 0 50 25 Q 75 50 100 25 Q 125 0 150 25 Q 175 50 200 25')
        .fill(Color.Transparent)
        .stroke('#FD79A8')
        .strokeWidth(2)
      Text('波浪线').fontSize(14)
    }
    .width('100%')
    .padding(30)
  }
}

2.8.5 实战:用Path画一个爱心
@Entry
@Component
struct HeartDemo {
  build() {
    Column({ space: 20 }) {
      Path()
        .width(100)
        .height(90)
        .commands(
          'M 50 20 ' +
          'C 50 10 40 0 25 0 ' +
          'C 10 0 0 15 0 30 ' +
          'C 0 50 15 70 50 90 ' +
          'C 85 70 100 50 100 30 ' +
          'C 100 15 90 0 75 0 ' +
          'C 60 0 50 10 50 20 ' +
          'Z'
        )
        .fill('#FF6B6B')
        .stroke('#C0392B')
        .strokeWidth(2)
      
      Text('Path画的爱心').fontSize(16)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

小邢哥解析这颗爱心:

爱心本质上是两个相切的圆弧加上一个尖角。

我用三次贝塞尔曲线来拟合这个形状:

  1. 从顶部凹陷处开始(M 50 20)

  2. 画左半边的弧线(两个C命令)

  3. 画右半边的弧线(两个C命令)1

  4. 回到起点(Z)

2.8.6 Path命令的简写规则

Path命令支持一些简写规则,让你的代码更简洁:

  1. 连续相同命令可省略命令字母

L 10 20 L 30 40 L 50 60
// 可以写成
L 10 20 30 40 50 60
  1. M后面连续的坐标会被视为L

M 0 0 10 20 30 40
// 等价于
M 0 0 L 10 20 L 30 40
  1. 数字之间的逗号可以省略(用空格分隔)

M 50,20 L 100,80
// 等价于
M 50 20 L 100 80

2.9 Shape容器——组合多个图形

Shape组件本身是一个容器,可以把多个基础图形组合在一起:

@Entry
@Component
struct ShapeContainerDemo {
  build() {
    Column({ space: 30 }) {
      // 笑脸
      Shape() {
        // 脸
        Circle({ width: 120, height: 120 })
          .fill('#FFEAA7')
          .stroke('#F39C12')
          .strokeWidth(3)

        // 左眼
        Circle({ width: 15, height: 15 })
          .fill('#2D3436')
          .offset({ x: 35, y: 35 })

        // 右眼
        Circle({ width: 15, height: 15 })
          .fill('#2D3436')
          .offset({ x: 70, y: 35 })

        // 嘴巴(用Path画弧线)
        Path()
          .commands('M 25 75 Q 60 100 105 75')
          .stroke('#2D3436')
          .strokeWidth(3)
          .fill(Color.Transparent)
          .offset({ x: 40, y: 55 })
      }
      .width(120)
      .height(120)

      Text('用Shape组合的笑脸').fontSize(16)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

Shape容器的关键属性:

属性 作用 示例
viewPort 定义形状的视口 viewPort({ x: 0, y: 0, width: 100, height: 100 })
fill 统一设置所有子图形的填充色 fill(Color.Blue)
stroke 统一设置所有子图形的描边色 stroke(Color.Red)

小技巧: 使用offset({ x: number, y: number })可以调整子图形在容器内的位置。


第三章:形状裁剪clipShape——给组件"整容"

3.1 clipShape是什么?

前面讲的Shape家族是"画图形",而clipShape是"裁图形"。

打个比方:

  • Shape = 用画笔在白纸上画一个圆

  • clipShape = 用圆形模具把一张照片裁成圆形

clipShape是一个通用属性,几乎所有组件都可以使用它。它的作用是:把组件的可见区域裁剪成指定的形状

3.2 基础用法

@Entry
@Component
struct ClipShapeBasicDemo {
  build() {
    Column({ space: 30 }) {
      // 原始图片
      Image($r('app.media.sample'))
        .width(150)
        .height(150)
        .objectFit(ImageFit.Cover)
      Text('原始图片').fontSize(14)

      // 圆形裁剪
      Image($r('app.media.sample'))
        .width(150)
        .height(150)
        .objectFit(ImageFit.Cover)
        .clipShape(new Circle({ width: 150, height: 150 }))
      Text('圆形裁剪').fontSize(14)

      // 椭圆裁剪
      Image($r('app.media.sample'))
        .width(150)
        .height(100)
        .objectFit(ImageFit.Cover)
        .clipShape(new Ellipse({ width: 150, height: 100 }))
      Text('椭圆裁剪').fontSize(14)
    }
    .width('100%')
    .padding(20)
  }
}

语法要点:

clipShape的参数需要传入形状对象实例,而不是组件。注意写法的区别:

// ❌ 错误写法(组件写法)
.clipShape(Circle({ width: 100, height: 100 }))

// ✅ 正确写法(对象实例)
.clipShape(new Circle({ width: 100, height: 100 }))

3.3 支持的裁剪形状

clipShape支持以下形状:

形状 创建方式 示例
圆形 new Circle() new Circle({ width: 100, height: 100 })
椭圆 new Ellipse() new Ellipse({ width: 150, height: 100 })
矩形 new Rect() new Rect({ width: 100, height: 80 })
圆角矩形 new Rect() + radius new Rect({ width: 100, height: 80 }).radius(10)
自由路径 new Path() new Path().commands('...')

3.4 实战案例:各种裁剪效果

@Entry
@Component
struct ClipShapeGalleryDemo {
  build() {
    Column({ space: 20 }) {
      Text('clipShape效果展示').fontSize(20).fontWeight(FontWeight.Bold)

      Row({ space: 20 }) {
        // 圆形头像
        Column({ space: 5 }) {
          Image($r('app.media.startIcon'))
            .width(80)
            .height(80)
            .objectFit(ImageFit.Cover)
            .clipShape(new Circle({ width: 80, height: 80 }))
          Text('圆形头像').fontSize(12)
        }

        // 圆角矩形卡片
        Column({ space: 5 }) {
          Image($r('app.media.background'))
            .width(120)
            .height(80)
            .objectFit(ImageFit.Cover)
            .clipShape(new Rect({ width: 120, height: 80 }).radius(15))
          Text('圆角卡片').fontSize(12)
        }
      }

      Row({ space: 20 }) {
        // 菱形
        Column({ space: 5 }) {
          Image($r('app.media.background'))
            .width(40)
            .height(30)
            .objectFit(ImageFit.Cover)
            .clipShape(new Path().commands(
              'M 40 0 L 80 40 L 40 80 L 0 40 Z'
            ))
          Text('菱形').fontSize(12)
        }

        // 六边形
        Column({ space: 5 }) {
          Image($r('app.media.background'))
            .width(40)
            .height(30)
            .objectFit(ImageFit.Cover)
            .clipShape(new Path().commands(
              'M 40 0 L 80 17 L 80 53 L 40 70 L 0 53 L 0 17 Z'
            ))
          Text('六边形').fontSize(12)
        }

        // 五角星
        Column({ space: 5 }) {
          Image($r('app.media.background'))
            .width(40)
            .height(36)
            .objectFit(ImageFit.Cover)
            .clipShape(new Path().commands(
              'M 40 0 L 49 28 L 80 28 L 55 45 L 64 76 L 40 57 L 16 76 L 25 45 L 0 28 L 31 28 Z'
            ))
          Text('五角星').fontSize(12)
        }
      }

      // 心形裁剪
      Column({ space: 5 }) {
        Image($r('app.media.background'))
          .width(50)
          .height(50)
          .objectFit(ImageFit.Cover)
          .clipShape(new Path().commands(
            'M 60 20 ' +
              'C 60 10 48 0 30 0 ' +
              'C 12 0 0 15 0 33 ' +
              'C 0 55 18 80 60 110 ' +
              'C 102 80 120 55 120 33 ' +
              'C 120 15 108 0 90 0 ' +
              'C 72 0 60 10 60 20 Z'
          ))
        Text('心形情侣照').fontSize(12)
      }
    }
    .width('100%')
    .padding(20)
  }
}

3.5 clipShape vs clip属性

你可能注意到,HarmonyOS还有一个clip属性。它们有什么区别呢?

属性 作用 用法
clip(true) 简单开启裁剪,使用组件自身边界 .clip(true)
clipShape 使用自定义形状裁剪 .clipShape(new Circle(...))
// clip(true):按组件边界裁剪
Row() {
  Text('超长文本会被裁剪掉不会溢出到外面去')
}
.width(100)
.clip(true)

// clipShape:按自定义形状裁剪
Image($r('app.media.photo'))
  .width(100)
  .height(100)
  .clipShape(new Circle({ width: 100, height: 100 }))

3.6 clipShape的性能考虑

clipShape本质上是在渲染管线中增加了一个遮罩层,会有一定的性能开销。

小邢哥的优化建议:

  1. 简单形状优先:Circle/Ellipse/Rect的性能比Path好

  2. 避免大量使用:如果列表中每个item都用clipShape,考虑用预处理图片代替

  3. 静态优先:如果形状不会变化,系统可以缓存裁剪结果

  4. 合理尺寸:裁剪区域越大,开销越大


第四章:综合实战——打造实用UI组件

理论讲完了,现在进入实战环节。我会带你用Shape和clipShape打造几个实用的UI组件。

4.1 实战一:圆形进度条

@Entry
@Component
struct CircularProgressDemo {
  @State progress: number = 0.75 // 75%进度

  build() {
    Column({ space: 20 }) {
      Stack() {
        // 背景圆环
        Circle({ width: 120, height: 120 })
          .fill(Color.Transparent)
          .stroke('#E0E0E0')
          .strokeWidth(10)

        // 进度圆弧
        Path()
          .width(120)
          .height(120)
          .commands(this.getProgressPath())
          .fill(Color.Transparent)
          .stroke('#4CAF50')
          .strokeWidth(10)
          .strokeLineCap(LineCapStyle.Round)

        // 中间的百分比文字
        Text(`${Math.round(this.progress * 100)}%`)
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          .fontColor('#333333')
      }
      .width(120)
      .height(120)

      // 控制进度的滑块
      Slider({
        value: this.progress * 100,
        min: 0,
        max: 100,
        step: 1
      })
        .width(200)
        .onChange((value: number) => {
          this.progress = value / 100
        })

      Text('拖动滑块调整进度').fontSize(14).fontColor(Color.Gray)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }

  // 计算进度弧线的Path命令
  getProgressPath(): string {
    const centerX = 60
    const centerY = 60
    const radius = 55 // 略小于容器一半,留出strokeWidth空间

    // 起点:12点钟方向
    const startX = centerX
    const startY = centerY - radius

    // 终点角度计算
    const angle = this.progress * 360 - 90 // -90是因为从12点钟开始
    const angleRad = angle * Math.PI / 180
    const endX = centerX + radius * Math.cos(angleRad)
    const endY = centerY + radius * Math.sin(angleRad)

    // 大弧标志:进度>50%时使用大弧
    const largeArcFlag = this.progress > 0.5 ? 1 : 0

    if (this.progress === 0) {
      return '' // 0%时不画任何东西
    }

    if (this.progress >= 1) {
      // 100%时画完整圆,需要分两个弧
      return `M ${startX} ${startY} A ${radius} ${radius} 0 1 1 ${startX} ${startY + 0.01}`
    }

    return `M ${startX} ${startY} A ${radius} ${radius} 0 ${largeArcFlag} 1 ${endX} ${endY}`
  }
}

代码解析:

  1. 底层是一个灰色圆环:用Circle配合stroke实现

  2. 上层是一个彩色弧线:用Path的A命令绘制

  3. 中间是百分比文字:用Stack布局叠加

  4. 弧线的终点用三角函数计算:根据进度百分比换算成角度

这个组件可以直接用在健康App的数据展示、下载进度显示等场景。

4.2 实战二:六边形头像

@Entry
@Component
struct HexagonAvatarDemo {
  build() {
    Column({ space: 30 }) {
      Text('六边形头像').fontSize(20).fontWeight(FontWeight.Bold)

      // 普通六边形头像
      Image($r('app.media.avatar'))
        .width(100)
        .height(87)
        .objectFit(ImageFit.Cover)
        .clipShape(new Path().commands(
          'M 50 0 L 100 22 L 100 65 L 50 87 L 0 65 L 0 22 Z'
        ))

      // 带边框的六边形头像
      Stack() {
        // 边框(稍大的六边形)
        Path()
          .width(110)
          .height(96)
          .commands('M 55 0 L 110 24 L 110 72 L 55 96 L 0 72 L 0 24 Z')
          .fill('#4ECDC4')

        // 头像
        Image($r('app.media.avatar'))
          .width(100)
          .height(87)
          .objectFit(ImageFit.Cover)
          .clipShape(new Path().commands(
            'M 50 0 L 100 22 L 100 65 L 50 87 L 0 65 L 0 22 Z'
          ))
      }
      .width(110)
      .height(96)

      // 蜂窝布局的头像组
      Row({ space: -20 }) {
        ForEach([1, 2, 3, 4], (item: number, index: number) => {
          Image($r('app.media.avatar'))
            .width(60)
            .height(52)
            .objectFit(ImageFit.Cover)
            .clipShape(new Path().commands(
              'M 30 0 L 60 13 L 60 39 L 30 52 L 0 39 L 0 13 Z'
            ))
            .offset({ y: index % 2 === 1 ? 26 : 0 })
        })
      }
      Text('蜂窝布局').fontSize(14).fontColor(Color.Gray)
    }
    .width('100%')
    .padding(30)
  }
}

4.3 实战三:波浪背景动画

@Entry
@Component
struct WaveBackgroundDemo {
  @State waveOffset: number = 0

  build() {
    Stack({ alignContent: Alignment.Bottom }) {
      // 背景色
      Column()
        .width('100%')
        .height('100%')
        .backgroundColor('#87CEEB')

      // 波浪
      Path()
        .width('200%')
        .height(150)
        .commands(this.getWavePath())
        .fill('#4169E1')
        .offset({ x: this.waveOffset })

      // 第二层波浪(更浅的颜色,增加层次感)
      Path()
        .width('200%')
        .height(120)
        .commands(this.getWavePath2())
        .fill('#6495ED')
        .fillOpacity(0.7)
        .offset({ x: -this.waveOffset })

      // 内容
      Column({ space: 20 }) {
        Text('波浪动画')
          .fontSize(28)
          .fontWeight(FontWeight.Bold)
          .fontColor(Color.White)
        Text('Shape绘制 + offset动画')
          .fontSize(16)
          .fontColor(Color.White)
      }
      .width('100%')
      .height('60%')
      .justifyContent(FlexAlign.Center)
    }
    .width('100%')
    .height('100%')
    .onAppear(() => {
      this.startWaveAnimation()
    })
  }

  getWavePath(): string {
    // 绘制两个完整周期的波浪,这样动画可以无缝循环
    return 'M 0 50 ' +
      'Q 90 0 180 50 ' +
      'Q 270 100 360 50 ' +
      'Q 450 0 540 50 ' +
      'Q 630 100 720 50 ' +
      'L 720 150 L 0 150 Z'
  }

  getWavePath2(): string {
    // 第二层波浪,相位不同
    return 'M 0 60 ' +
      'Q 80 20 160 60 ' +
      'Q 240 100 320 60 ' +
      'Q 400 20 480 60 ' +
      'Q 560 100 640 60 ' +
      'Q 720 20 800 60 ' +
      'L 800 150 L 0 150 Z'
  }

  startWaveAnimation() {
    // 使用定时器实现波浪动画
    setInterval(() => {
      this.waveOffset -= 2
      if (this.waveOffset <= -360) {
        this.waveOffset = 0
      }
    }, 30)
  }
}

动画原理:

  1. 绘制两个波浪周期的Path

  2. 宽度设为200%,确保始终覆盖屏幕

  3. 通过offset不断左移

  4. 当移动一个周期后,重置offset,实现无缝循环

4.4 实战四:自定义形状的按钮

@Entry
@Component
struct CustomShapeButtonDemo {
  @State isPressed: boolean = false

  build() {
    Column({ space: 40 }) {
      Text('自定义形状按钮').fontSize(20).fontWeight(FontWeight.Bold)

      // 菱形按钮
      Stack() {
        Path()
          .width(120)
          .height(60)
          .commands('M 60 0 L 120 30 L 60 60 L 0 30 Z')
          .fill(this.isPressed ? '#2980B9' : '#3498DB')
          .stroke('#2471A3')
          .strokeWidth(2)

        Text('菱形按钮')
          .fontSize(14)
          .fontColor(Color.White)
      }
      .width(120)
      .height(60)
      .onTouch((event: TouchEvent) => {
        if (event.type === TouchType.Down) {
          this.isPressed = true
        } else if (event.type === TouchType.Up || event.type === TouchType.Cancel) {
          this.isPressed = false
        }
      })
      .onClick(() => {
        console.log('菱形按钮被点击')
      })

      // 箭头按钮
      Stack() {
        Path()
          .width(140)
          .height(50)
          .commands(
            'M 0 10 L 100 10 L 100 0 L 140 25 L 100 50 L 100 40 L 0 40 Z'
          )
          .fill('#27AE60')
          .stroke('#1E8449')
          .strokeWidth(2)

        Text('下一步')
          .fontSize(14)
          .fontColor(Color.White)
          .offset({ x: -15 })
      }
      .width(140)
      .height(50)

      // 云朵形按钮
      Stack() {
        Path()
          .width(150)
          .height(80)
          .commands(
            'M 30 60 ' +
            'A 30 30 0 1 1 60 30 ' +
            'A 25 25 0 1 1 110 35 ' +
            'A 20 20 0 1 1 130 60 ' +
            'L 30 60 Z'
          )
          .fill('#9B59B6')
          .stroke('#7D3C98')
          .strokeWidth(2)

        Text('云端上传')
          .fontSize(14)
          .fontColor(Color.White)
      }
      .width(150)
      .height(80)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

4.5 实战五:数据可视化——雷达图

@Entry
@Component
struct RadarChartDemo {
  // 数据:5个维度,每个维度0-100分
  private data: number[] = [80, 65, 90, 55, 75]
  private labels: string[] = ['攻击', '防御', '速度', '智力', '耐力']
  private maxValue: number = 100
  private centerX: number = 120
  private centerY: number = 120
  private radius: number = 100

  build() {
    Column({ space: 20 }) {
      Text('能力雷达图').fontSize(20).fontWeight(FontWeight.Bold)

      Shape() {
        // 绘制背景网格(5层)
        ForEach([0.2, 0.4, 0.6, 0.8, 1], (scale: number) => {
          Polygon()
            .points(this.getPentagonPoints(scale))
            .fill(Color.Transparent)
            .stroke('#E0E0E0')
            .strokeWidth(1)
        })

        // 绘制5条轴线
        ForEach([0, 1, 2, 3, 4], (index: number) => {
          Line()
            .startPoint([this.centerX, this.centerY])
            .endPoint(this.getAxisEndPoint(index))
            .stroke('#BDBDBD')
            .strokeWidth(1)
        })

        // 绘制数据多边形
        Polygon()
          .points(this.getDataPoints())
          .fill('#3498DB')
          .fillOpacity(0.3)
          .stroke('#2980B9')
          .strokeWidth(2)

        // 绘制数据点
        ForEach([0, 1, 2, 3, 4], (index: number) => {
          Circle({ width: 8, height: 8 })
            .fill('#E74C3C')
            .offset(this.getDataPointOffset(index))
        })
      }
      .width(240)
      .height(240)

      // 图例
      Row({ space: 15 }) {
        ForEach(this.labels, (label: string, index: number) => {
          Text(`${label}: ${this.data[index]}`)
            .fontSize(12)
            .fontColor('#666666')
        })
      }
      .width('100%')
      .justifyContent(FlexAlign.Center)
    }
    .width('100%')
    .padding(20)
  }

  // 获取正五边形顶点坐标
  getPentagonPoints(scale: number): number[][] {
    const points: number[][] = []
    for (let i = 0; i < 5; i++) {
      const angle = (i * 72 - 90) * Math.PI / 180
      const x = this.centerX + this.radius * scale * Math.cos(angle)
      const y = this.centerY + this.radius * scale * Math.sin(angle)
      points.push([x, y])
    }
    return points
  }

  // 获取轴线终点
  getAxisEndPoint(index: number): number[] {
    const angle = (index * 72 - 90) * Math.PI / 180
    return [
      this.centerX + this.radius * Math.cos(angle),
      this.centerY + this.radius * Math.sin(angle)
    ]
  }

  // 获取数据多边形顶点
  getDataPoints(): number[][] {
    const points: number[][] = []
    for (let i = 0; i < 5; i++) {
      const angle = (i * 72 - 90) * Math.PI / 180
      const value = this.data[i] / this.maxValue
      const x = this.centerX + this.radius * value * Math.cos(angle)
      const y = this.centerY + this.radius * value * Math.sin(angle)
      points.push([x, y])
    }
    return points
  }

  // 获取数据点的偏移量(用于Circle的offset)
  getDataPointOffset(index: number): Position {
    const angle = (index * 72 - 90) * Math.PI / 180
    const value = this.data[index] / this.maxValue
    return {
      x: this.centerX + this.radius * value * Math.cos(angle) - 4,
      y: this.centerY + this.radius * value * Math.sin(angle) - 4
    }
  }
}

第五章:进阶知识——深入理解渲染原理

5.1 Shape组件的渲染流程

了解渲染流程,有助于我们写出性能更好的代码。

用户代码定义Shape
      ↓
ArkUI框架解析Shape组件树
      ↓
计算图形顶点、路径等几何数据
      ↓
生成绘制指令(Draw Commands)
      ↓
Skia图形引擎执行绘制
      ↓
GPU光栅化输出像素
      ↓
显示到屏幕

关键点:

  • Shape组件最终由Skia图形引擎渲染

  • 简单图形(Circle、Rect)有GPU加速优化

  • Path组件因为形状自由,需要更多CPU计算

5.2 抗锯齿与渲染质量

你有没有注意过,有些圆形边缘很光滑,有些则有明显的锯齿?

这就涉及到抗锯齿(Anti-aliasing)。好消息是,HarmonyOS的Shape组件默认开启了抗锯齿,你不需要手动设置。

但在某些极端情况下(比如非常细的线条、超小的图形),可能仍会看到锯齿。这时可以:

  1. 适当增加strokeWidth

  2. 使用更大的尺寸,然后缩放显示

5.3 坐标系统详解

HarmonyOS的坐标系:

  • 原点(0,0)在左上角

  • X轴向右为正

  • Y轴向下为正

这与传统数学坐标系不同(数学坐标系Y轴向上),在用三角函数计算坐标时要注意转换。

(0,0) ─────────────→ X
  │
  │
  │
  │
  ↓
  Y

5.4 viewPort与坐标缩放

Shape容器的viewPort属性可以定义一个"虚拟坐标系",让你用更方便的数值来绘图:

Shape() {
  Path()
    .commands('M 0 0 L 100 0 L 100 100 L 0 100 Z')
    .fill(Color.Blue)
}
.width(200)  // 实际显示200vp
.height(200) // 实际显示200vp
.viewPort({ x: 0, y: 0, width: 100, height: 100 }) // 虚拟坐标系100x100

上面的代码中,虽然Path的坐标只用到0-100,但会被自动缩放到200vp的显示区域。

使用场景:

  • 引用SVG的path数据时,保持原始坐标

  • 需要精确控制缩放比例时


第六章:性能优化指南

6.1 图形复杂度与性能关系

性能消耗(从低到高):
Circle/Rect < Ellipse < Line < Polyline < Polygon < Path(简单)< Path(复杂)

小邢哥的经验法则:

  • 能用简单图形就不用Path

  • Path的commands越长,性能越差

  • 避免在列表中使用复杂Path

6.2 动态更新的优化

如果图形需要频繁更新(比如动画),注意以下几点:

1. 使用@State而不是重新创建组件

// ✅ 好的做法
@State progress: number = 0
Circle({ width: 100, height: 100 })
  .strokeDashOffset(this.progress) // 只更新属性

// ❌ 避免的做法
// 每次更新都重新计算整个Path

2. 减少Path commands的动态拼接

// ❌ 每帧都拼接字符串,性能差
getPath(): string {
  return `M ${this.x} ${this.y} L ${this.endX} ${this.endY}`
}

// ✅ 使用变换代替重新计算
Path()
  .commands('M 0 0 L 100 100')
  .translate({ x: this.offsetX, y: this.offsetY }) // 使用translate

6.3 离屏渲染注意事项

clipShape会触发离屏渲染,这在某些情况下会影响性能。

什么是离屏渲染? 正常渲染是直接把图像画到屏幕缓冲区。离屏渲染是先画到一个临时缓冲区,应用遮罩后,再转移到屏幕缓冲区。多了一步转移操作。

优化建议:

  • 如果只是需要圆角,优先用borderRadius而不是clipShape

  • 列表中的item尽量避免使用clipShape

  • 对于静态图片,考虑预处理成目标形状

6.4 内存优化

每个Shape组件都会占用一定内存。如果你有大量图形:

// ❌ 创建100个Circle组件
ForEach(Array(100).fill(0), (_, index) => {
  Circle({ width: 10, height: 10 })
    .fill(Color.Blue)
    .offset({ x: index * 15, y: 0 })
})

// ✅ 使用一个Path绘制100个圆
Path()
  .commands(this.generate100CirclesPath())
  .fill(Color.Blue)

第七章:常见问题FAQ

Q1:Shape组件可以响应点击事件吗?

A: 可以,但默认情况下点击区域是矩形边界框,而不是图形本身的形状。

// 这个三角形的点击区域是一个100x100的正方形
Polygon()
  .points([[50, 0], [100, 100], [0, 100]])
  .width(100)
  .height(100)
  .onClick(() => {
    console.log('点击了三角形区域...或者是三角形外的空白区域')
  })

如果需要精确到形状的点击区域,需要自己在onClick中判断坐标:

.onTouch((event: TouchEvent) => {
  const x = event.touches[0].x
  const y = event.touches[0].y
  if (this.isPointInTriangle(x, y)) {
    // 真正点击在三角形内
  }
})

Q2:如何实现渐变填充?

A: HarmonyOS支持线性渐变和径向渐变:

Circle({ width: 100, height: 100 })
  .fill({
    type: GradientType.Linear,
    direction: GradientDirection.Bottom,
    colors: [['#FF6B6B', 0], ['#4ECDC4', 1]]
  })

Q3:Shape可以加阴影吗?

A: 可以使用通用的shadow属性:

Circle({ width: 80, height: 80 })
  .fill('#3498DB')
  .shadow({
    radius: 10,
    color: '#00000033',
    offsetX: 5,
    offsetY: 5
  })

Q4:Path的commands字符串太长怎么办?

A: 有几种方法:

  1. 模板字符串换行

.commands(`
  M 50 0
  L 100 100
  L 0 100
  Z
`)
  1. 字符串拼接

.commands(
  'M 50 0 ' +
  'L 100 100 ' +
  'L 0 100 ' +
  'Z'
)
  1. 封装成函数

getStarPath(cx: number, cy: number, r: number): string {
  // 计算五角星各点坐标
  const points: number[][] = []
  for (let i = 0; i < 10; i++) {
    const angle = (i * 36 - 90) * Math.PI / 180
    const radius = i % 2 === 0 ? r : r * 0.38
    points.push([
      cx + radius * Math.cos(angle),
      cy + radius * Math.sin(angle)
    ])
  }
  return 'M ' + points.map(p => p.join(' ')).join(' L ') + ' Z'
}

Q5:clipShape裁剪后,能看到组件原来的边界吗?

A: 不能。clipShape真的会把超出区域"剪掉",完全不可见。

如果你想要"镂空"效果(中间透明,周围有内容),需要用到更高级的技术,比如:

  • BlendMode混合模式

  • Canvas API


第八章:总结与展望

8.1 本文知识点回顾

我们从最基础的Circle,一路讲到复杂的Path,再到clipShape的裁剪技术,覆盖了HarmonyOS 6.0几何图形绘制的方方面面:

基础组件篇:

  • Circle:圆形,最简单

  • Ellipse:椭圆

  • Rect:矩形,支持圆角

  • Line:直线

  • Polyline:折线

  • Polygon:多边形

  • Path:自由路径,终极武器

Path命令篇:

  • M:移动

  • L/H/V:直线

  • A:圆弧

  • Q/C:贝塞尔曲线

  • Z:闭合

clipShape篇:

  • 用于裁剪组件可见区域

  • 支持Circle、Ellipse、Rect、Path

  • 注意性能影响

8.2 学习路径建议

如果你是新手,我建议按这个顺序学习:

第一天:掌握Circle、Rect、基础样式(fill、stroke)
    ↓
第二天:学习Line、Polyline、Polygon
    ↓
第三天:攻克Path,重点是A和Q/C命令
    ↓
第四天:学习clipShape,做综合实战
    ↓
进阶:研究性能优化、动画结合

8.3 写在最后

14年的编程生涯教会我一个道理:技术的本质是相通的

今天你学的是HarmonyOS的Shape,但这套知识可以迁移到任何UI框架:

  • Web的SVG/Canvas用的是几乎相同的Path语法

  • Flutter的CustomPainter原理类似

  • 甚至Android的Canvas、iOS的CoreGraphics,底层逻辑都是一样的

所以,不要只是背API,要理解原理。理解了"为什么这样设计",你才能触类旁通。

好了,这篇文章就到这里。如果你觉得有收获,欢迎点赞、收藏、转发。有问题可以评论区留言,我会逐一回复。

我是小邢哥,14年编程老炮,我们下篇文章见!


附录:本文代码仓库

本文所有完整代码已上传到Gitee,欢迎Star:

https://gitcode.com/u014332200/HarmonyShape

Logo

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

更多推荐