1# 绘制图形 2 3绘制能力主要是通过框架提供的绘制组件来支撑,支持svg标准绘制命令。 4 5本节主要学习如何使用绘制组件,绘制详情页食物成分标签(基本几何图形)和应用Logo(自定义图形)。 6 7## 绘制基本几何图形 8 9绘制组件封装了一些常见的基本几何图形,比如矩形Rect、圆形Circle、椭圆形Ellipse等,为开发者省去了路线计算的过程。 10 11FoodDetail页面的食物成分表里,给每一项成分名称前都加上一个圆形的图标作为成分标签。 12 131. 创建Circle组件,在每一项含量成分前增加一个圆形图标作为标签。设置Circle的直径为 6vp。修改FoodDetail页面的ContentTable组件里的IngredientItem方法,在成分名称前添加Circle。 14 15 ```ts 16 // FoodDetail.ets 17 @Component 18 struct ContentTable { 19 private foodItem: FoodData 20 21 @Builder IngredientItem(title:string, colorValue: string, name: string, value: string) { 22 Flex() { 23 Text(title) 24 .fontSize(17.4) 25 .fontWeight(FontWeight.Bold) 26 .layoutWeight(1) 27 Flex({ alignItems: ItemAlign.Center }) { 28 Circle({width: 6, height: 6}) 29 .margin({right: 12}) 30 .fill(colorValue) 31 Text(name) 32 .fontSize(17.4) 33 .flexGrow(1) 34 Text(value) 35 .fontSize(17.4) 36 } 37 .layoutWeight(2) 38 } 39 } 40 41 build() { 42 ...... 43 } 44 } 45 ``` 46 472. 每个成分的标签颜色不一样,所以我们在build方法中,调用IngredientItem,给每个Circle填充不一样的颜色。 48 49 ```ts 50 // FoodDetail.ets 51 @Component 52 struct ContentTable { 53 private foodItem: FoodData 54 55 @Builder IngredientItem(title:string, colorValue: string, name: string, value: string) { 56 Flex() { 57 Text(title) 58 .fontSize(17.4) 59 .fontWeight(FontWeight.Bold) 60 .layoutWeight(1) 61 Flex({ alignItems: ItemAlign.Center }) { 62 Circle({width: 6, height: 6}) 63 .margin({right: 12}) 64 .fill(colorValue) 65 Text(name) 66 .fontSize(17.4) 67 .flexGrow(1) 68 Text(value) 69 .fontSize(17.4) 70 } 71 .layoutWeight(2) 72 } 73 } 74 75 build() { 76 Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Start }) { 77 this.IngredientItem('Calories', '#FFf54040', 'Calories', this.foodItem.calories + 'kcal') 78 this.IngredientItem('Nutrition', '#FFcccccc', 'Protein', this.foodItem.protein + 'g') 79 this.IngredientItem(' ', '#FFf5d640', 'Fat', this.foodItem.fat + 'g') 80 this.IngredientItem(' ', '#FF9e9eff', 'Carbohydrates', this.foodItem.carbohydrates + 'g') 81 this.IngredientItem(' ', '#FF53f540', 'VitaminC', this.foodItem.vitaminC + 'mg') 82 } 83 .height(280) 84 .padding({ top: 30, right: 30, left: 30 }) 85 } 86 } 87 ``` 88 89  90 91## 绘制自定义几何图形 92 93除绘制基础几何图形,开发者还可以使用Path组件来绘制自定义的路线,下面进行绘制应用的Logo图案。 94 951. 在pages文件夹下创建新的页面Logo.ets。 96 97  98 992. Logo.ets中删掉模板代码,创建Logo Component。 100 101 ```ts 102 @Entry 103 @Component 104 struct Logo { 105 build() { 106 } 107 } 108 ``` 109 1103. 创建Flex组件为根节点,宽高设置为100%,设置其在主轴方向和交叉轴方向的对齐方式都为Center,创建Shape组件为Flex子组件。 111 112 Shape组件是所有绘制组件的父组件。如果需要组合多个绘制组件成为一个整体,需要创建Shape作为其父组件。 113 114 我们要绘制的Logo的大小630px * 630px。声明式UI范式支持多种长度单位的设置,在前面的章节中,我们直接使用number作为参数,即采用了默认长度单位vp,虚拟像素单位。vp和设备分辨率以及屏幕密度有关。比如设备分辨率为1176 * 2400,屏幕基准密度(resolution)为3,vp = px / resolution,则该设备屏幕宽度是392vp。 115 116 但是绘制组件采用svg标准,默认采取px为单位的,为方便统一,在这绘制Logo这一部分,统一采取px为单位。声明式UI框架同样也支持px单位,入参类型为string,设置宽度为630px,即210vp,设置方式为width('630px')或者width(210)。 117 118 ```ts 119 @Entry 120 @Component 121 struct Logo { 122 build() { 123 Flex({ alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { 124 Shape() { 125 126 } 127 .height('630px') 128 .width('630px') 129 } 130 .width('100%') 131 .height('100%') 132 } 133 } 134 ``` 135 1364. 给页面填充渐变色。设置为线性渐变,偏移角度为180deg,三段渐变 #BDE895 -->95DE7F --> #7AB967,其区间分别为[0, 0.1], (0.1, 0.6], (0.6, 1]。 137 138 ```ts 139 .linearGradient( 140 { 141 angle: 180, 142 colors: [['#BDE895', 0.1], ["#95DE7F", 0.6], ["#7AB967", 1]] 143 }) 144 ``` 145 146  147 148 ```ts 149 @Entry 150 @Component 151 struct Logo { 152 build() { 153 Flex({ alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { 154 Shape() { 155 156 } 157 .height('630px') 158 .width('630px') 159 } 160 .width('100%') 161 .height('100%') 162 .linearGradient( 163 { 164 angle: 180, 165 colors: [['#BDE895', 0.1], ["#95DE7F", 0.6], ["#7AB967", 1]] 166 }) 167 } 168 } 169 ``` 170 171  172 1735. 绘制第一条路线Path,设置其绘制命令。 174 175 ```ts 176 Path() 177 .commands('M162 128.7 a222 222 0 0 1 100.8 374.4 H198 a36 36 0 0 3 -36 -36') 178 ``` 179 180 Path的绘制命令采用svg标准,上述命令可分解为: 181 182 ```ts 183 M162 128.7 184 ``` 185 186 将笔触移动到(Moveto)坐标点(162, 128.7)。 187 188 ```ts 189 a222 222 0 0 1 100.8 374.4 190 ``` 191 192 画圆弧线(elliptical arc)半径rx,ry为222,x轴旋转角度x-axis-rotation为0,角度大小large-arc-flag为0,即小弧度角,弧线方向(sweep-flag)为1,即逆时针画弧线,小写a为相对位置,即终点坐标为(162 + 100.8 = 262.8, 128.7 + 374.4 = 503.1)。 193 194 ```ts 195 H198 196 ``` 197 198 画水平线(horizontal lineto)到198,即画(262.8, 503.1)到(198, 503.1)的水平线。 199 200 ```ts 201 a36 36 0 0 3 -36 -36 202 ``` 203 204 画圆弧线(elliptical arc),含义同上,结束点为(198 - 36 = 162, 503.1 - 36 = 467.1)。 205 206 ```ts 207 V128.7 208 ``` 209 210 画垂直线(vertical lineto)到128.7,即画(162, 467.1)到(162, 128.7)的垂直线。 211 212 ```ts 213 z 214 ``` 215 216 关闭路径(closepath)。 217 218  219 220 填充颜色为白色,线条颜色为透明。 221 222 ```ts 223 .fill(Color.White) 224 .stroke(Color.Transparent) 225 ``` 226 227 ```ts 228 @Entry 229 @Component 230 struct Logo { 231 build() { 232 Flex({ alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { 233 Shape() { 234 Path() 235 .commands('M162 128.7 a222 222 0 0 1 100.8 374.4 H198 a36 36 0 0 3 -36 -36') 236 .fill(Color.White) 237 .stroke(Color.Transparent) 238 } 239 .height('630px') 240 .width('630px') 241 } 242 .width('100%') 243 .height('100%') 244 .linearGradient( 245 { 246 angle: 180, 247 colors: [['#BDE895', 0.1], ["#95DE7F", 0.6], ["#7AB967", 1]] 248 }) 249 } 250 } 251 ``` 252 253  254 2556. 在Shape组件内绘制第二个Path。第二条Path的背景色为渐变色,但是渐变色的填充是其整体的box,所以需要clip将其裁剪,入参为Shape,即按照Shape的形状进行裁剪。 256 257 ```ts 258 Path() 259 .commands('M319.5 128.1 c103.5 0 187.5 84 187.5 187.5 v15 a172.5 172.5 0 0 3 -172.5 172.5 H198 a36 36 0 0 3 -13.8 -1 207 207 0 0 0 87 -372 h48.3 z') 260 .fill('none') 261 .stroke(Corlor.Transparent) 262 .linearGradient( 263 { 264 angle: 30, 265 colors: [["#C4FFA0", 0], ["#ffffff", 1]] 266 }) 267 .clip(new Path().commands('M319.5 128.1 c103.5 0 187.5 84 187.5 187.5 v15 a172.5 172.5 0 0 3 -172.5 172.5 H198 a36 36 0 0 3 -13.8 -1 207 207 0 0 0 87 -372 h48.3 z')) 268 ``` 269 270 Path的绘制命令比较长,可以将其作为组件的成员变量,通过this调用。 271 272 ```ts 273 @Entry 274 @Component 275 struct Logo { 276 private pathCommands1:string = 'M319.5 128.1 c103.5 0 187.5 84 187.5 187.5 v15 a172.5 172.5 0 0 3 -172.5 172.5 H198 a36 36 0 0 3 -13.8 -1 207 207 0 0 0 87 -372 h48.3 z' 277 build() { 278 ...... 279 Path() 280 .commands(this.pathCommands1) 281 .fill('none') 282 .stroke(Color.Transparent) 283 .linearGradient( 284 { 285 angle: 30, 286 colors: [["#C4FFA0", 0], ["#ffffff", 1]] 287 }) 288 .clip(new Path().commands(this.pathCommands1)) 289 ...... 290 } 291 } 292 ``` 293 294  295 2967. 在Shape组件内绘制第二个Path。 297 298 ```ts 299 @Entry 300 @Component 301 struct Logo { 302 private pathCommands1:string = 'M319.5 128.1 c103.5 0 187.5 84 187.5 187.5 v15 a172.5 172.5 0 0 3 -172.5 172.5 H198 a36 36 0 0 3 -13.8 -1 207 207 0 0 0 87 -372 h48.3 z' 303 private pathCommands2:string = 'M270.6 128.1 h48.6 c51.6 0 98.4 21 132.3 54.6 a411 411 0 0 3 -45.6 123 c-25.2 45.6 -56.4 84 -87.6 110.4 a206.1 206.1 0 0 0 -47.7 -288 z' 304 build() { 305 Flex({ alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { 306 Shape() { 307 Path() 308 .commands('M162 128.7 a222 222 0 0 1 100.8 374.4 H198 a36 36 0 0 3 -36 -36') 309 .fill(Color.White) 310 .stroke(Color.Transparent) 311 312 Path() 313 .commands(this.pathCommands1) 314 .fill('none') 315 .stroke(Color.Transparent) 316 .linearGradient( 317 { 318 angle: 30, 319 colors: [["#C4FFA0", 0], ["#ffffff", 1]] 320 }) 321 .clip(new Path().commands(this.pathCommands1)) 322 323 Path() 324 .commands(this.pathCommands2) 325 .fill('none') 326 .stroke(Color.Transparent) 327 .linearGradient( 328 { 329 angle: 50, 330 colors: [['#8CC36A', 0.1], ["#B3EB90", 0.4], ["#ffffff", 0.7]] 331 }) 332 .clip(new Path().commands(this.pathCommands2)) 333 } 334 .height('630px') 335 .width('630px') 336 } 337 .width('100%') 338 .height('100%') 339 .linearGradient( 340 { 341 angle: 180, 342 colors: [['#BDE895', 0.1], ["#95DE7F", 0.6], ["#7AB967", 1]] 343 }) 344 } 345 } 346 ``` 347 348  349 350 完成应用Logo的绘制。Shape组合了三个Path组件,通过svg命令绘制出一个艺术的叶子,寓意绿色健康饮食方式。 351 3528. 添加应用的标题和slogan。 353 354 ```ts 355 @Entry 356 @Component 357 struct Logo { 358 private pathCommands1: string = 'M319.5 128.1 c103.5 0 187.5 84 187.5 187.5 v15 a172.5 172.5 0 0 3 -172.5 172.5 H198 a36 36 0 0 3 -13.8 -1 207 207 0 0 0 87 -372 h48.3 z' 359 private pathCommands2: string = 'M270.6 128.1 h48.6 c51.6 0 98.4 21 132.3 54.6 a411 411 0 0 3 -45.6 123 c-25.2 45.6 -56.4 84 -87.6 110.4 a206.1 206.1 0 0 0 -47.7 -288 z' 360 361 build() { 362 Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { 363 Shape() { 364 Path() 365 .commands('M162 128.7 a222 222 0 0 1 100.8 374.4 H198 a36 36 0 0 3 -36 -36') 366 .fill(Color.White) 367 .stroke(Color.Transparent) 368 369 Path() 370 .commands(this.pathCommands1) 371 .fill('none') 372 .stroke(Color.Transparent) 373 .linearGradient( 374 { 375 angle: 30, 376 colors: [["#C4FFA0", 0], ["#ffffff", 1]] 377 }) 378 .clip(new Path().commands(this.pathCommands1)) 379 380 Path() 381 .commands(this.pathCommands2) 382 .fill('none') 383 .stroke(Color.Transparent) 384 .linearGradient( 385 { 386 angle: 50, 387 colors: [['#8CC36A', 0.1], ["#B3EB90", 0.4], ["#ffffff", 0.7]] 388 }) 389 .clip(new Path().commands(this.pathCommands2)) 390 } 391 .height('630px') 392 .width('630px') 393 394 Text('Healthy Diet') 395 .fontSize(26) 396 .fontColor(Color.White) 397 .margin({ top: 300 }) 398 399 Text('Healthy life comes from a balanced diet') 400 .fontSize(17) 401 .fontColor(Color.White) 402 .margin({ top: 4 }) 403 } 404 .width('100%') 405 .height('100%') 406 .linearGradient( 407 { 408 angle: 180, 409 colors: [['#BDE895', 0.1], ["#95DE7F", 0.6], ["#7AB967", 1]] 410 }) 411 } 412 } 413 ``` 414 415 