1# 一多开发实例(短信) 2 3 4本章从系统预置的应用中,选择短信应用作为典型的案例,从页面开发和工程结构的角度,介绍"一多"的具体实践。系统的产品形态在不断丰富中,当前主要有默认设备和平板两种产品形态,本章的具体实践也将围绕这两种产品形态展开。 5 6 7## 概览 8 9短信是系统中预置的应用,主要包含信息查看、发送短信、接收短信、短信送达报告、删除短信等功能。在不同类型设备上,短信应用的功能完全相同,故短信应用适合使用[部署模型A](introduction.md#部署模型)(即:不同类型的设备上安装运行相同的HAP或HAP组合)。 10 11本案例中,在会话详情页面利用[方舟开发框架](introduction.md#方舟开发框架)提供的“一多”能力,用一套代码同时适配默认设备和平板。 12 13 14### 工程结构 15 16短信应用的工程结构如下图所示,当前该应用的功能较少,所以直接使用了DevEco Studio创建出的默认工程结构。具体采用何种形式的工程结构,并不影响应用的开发。但是使用推荐的工程结构,目录结构更清晰,拓展性也更好。 17 18短信应用UI相关的逻辑集中在views和pages两个目录,分别存放公共组件及页面。当前短信应用主要包含如下页面: 19 20- 信息列表页面:首页,展示信息列表。 21 22- 通知信息列表页面:将通知类信息集中在一起展示,与信息列表页面类似。 23 24- 会话详情页面:展示与某联系人的所有信息往来。 25 26- 报告详情页面:信息发送报告的详情页面。 27 28- 设置页面:消息设置页面,如是否展示送达报告等。 29 30 31``` 32/Mms/ 33 ├── doc # 资料 34 ├── entry 35 │ └── src 36 │ └── main 37 │ ├── resources # 资源配置文件存放目录 38 │ ├── config.json # 全局配置文件 39 │ └── ets # ets代码目录 40 │ ├── ServiceAbility # 后台常驻服务 41 │ └── default # 业务代码目录 42 │ ├── data # 自定义数据类型 43 │ ├── model # 对接数据库 44 │ ├── pages # 所有页面 45 │ │ ├── conversation # 会话详情页面 46 │ │ ├── conversationlist # 信息列表页面 47 │ │ ├── index # 初始页面 48 │ │ ├── info_msg # 通知信息列表页面 49 │ │ ├── query_report # 报告详情页面 50 │ │ └── settings # 设置页面 51 │ ├── service # 业务逻辑 52 │ ├── utils # 工具类 53 │ ├── views # 自定义组件 54 │ └── app.ets # 应用生命周期 55 ├── signs # 签名 56 └── LICENSE 57``` 58 59短信应用在开发阶段,采用了一层工程结构。由于功能较为简单,所以并没有规划共用的feature和common目录,仅采用了一层product目录。 60 61- 业务形态层(product) 62 该目录采用DevEco Studio工程默认创建的entry目录,开发者可根据需要在创建Module时自行更改该目录名。不同产品形态,编译出相同的短信HAP。 63 64 65 66## 会话详情页面 67 68 69### 页面结构 70 71| 默认设备 | 平板 | 72| ---------------------------------------- | ---------------------------------------- | 73|  |  | 74 75会话详情页面在默认设备和平板上的样式如上图所示,会话详情页面可以划分为三个部分: 76 77| 页面组成 | 介绍 | 78| ----- | ---------------------------------------- | 79| 顶部标题栏 |  | 80| 信息列表 |  | 81| 底部输入栏 |  | 82 83接下来我们详细介绍各部分的实现。 84 85> **说明:** 86> 为了方便理解,我们对会话详情页面做了一定的精简,本小节仅介绍会话详情页面最基础的实现。 87 88 89### 顶部标题栏 90 91| 默认设备 | 平板 | 92| ---------------------------------------- | ---------------------------------------- | 93|  |  | 94 95顶部标题栏是一个简单的行布局,包含返回图标、联系人头像、联系人姓名和号码、拨号图标、设置图标共5个元素。其中,联系人姓名和号码以列布局的形式放在一起。 96 97在默认设备和平板上,顶部标题栏的组件结构是相同的,仅联系人姓名和号码与拨号图标的间距不同。回顾方舟开发框架一多能力介绍,这个场景可以借助Blank组件使用拉伸能力。 98 99 我们先实现联系人姓名和号码,用Flex组件作为父容器,其包含两个Text子组件,分别用于存放联系人姓名和号码。Flex组件的属性设置如下: 100- direction: FlexDirection.Column:子组件在Flex容器上以列的方式排布,即主轴是垂直方向。 101 102- justifyContent: FlexAlign.Center:子组件在Flex容器主轴(垂直方向)上居中对齐。 103 104- alignItems: ItemAlign.Start:子组件在Flex容器交叉轴(水平方向)上首部对齐。 105 106可以查看[Flex组件](../../reference/apis-arkui/arkui-ts/ts-container-flex.md)及[Text组件](../../reference/apis-arkui/arkui-ts/ts-basic-components-text.md)了解这两个组件各个属性的含义及详细用法。 107 108| 默认设备 | 平板 | 109| ---------------------------------------- | ---------------------------------------- | 110|  |  | 111 112 113```ts 114@Entry 115@Component 116 struct TopArea { 117 build() { 118 Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Center, 119 alignItems: ItemAlign.Start}) { 120 Text('张三').fontSize(16).fontColor("#182431") 121 Text('+123 4567 8901').fontSize(14).fontColor("#66182431") 122 } 123 } 124 } 125``` 126 127接下来我们通过width属性和height属性设置四个图标的宽高(详见[尺寸设置](../../reference/apis-arkui/arkui-ts/ts-universal-attributes-size.md)),并将它们与联系人姓名和电话以及Blank组件一起放到Flex父容器中。为了便于查看效果,对顶部标题栏设置了淡蓝色的背景色。 128 129| 默认设备 | 平板 | 130| ---------------------------------------- | ---------------------------------------- | 131|  |  | 132 133 134```ts 135@Entry 136@Component 137 struct TopArea { 138 build() { 139 Flex({ alignItems: ItemAlign.Center }) { 140 Image($r('app.media.back')) //在应用的resources/base/media目录放置名为back的资源文件 141 .width(24) 142 .height(24) 143 Image($r('app.media.contact')) //在应用的resources/base/media目录放置名为contact的资源文件 144 .width(40) 145 .height(40) 146 Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Center, 147 alignItems: ItemAlign.Start}) { 148 Text('张三').fontSize(16).fontColor("#182431") 149 Text('+123 4567 8901').fontSize(14).fontColor("#66182431") 150 } 151 Blank() // 拉伸能力 152 Image($r("app.media.phone")) //在应用的resources/base/media目录放置名为phone的资源文件 153 .width(24) 154 .height(24) 155 Image($r('app.media.dots')) //在应用的resources/base/media目录放置名为dots的资源文件 156 .width(24) 157 .height(24) 158 } 159 .width('100%') 160 .height(56) 161 .backgroundColor('#87CEFA') // 顶部标题栏背景色,仅用于开发测试 162 } 163 } 164``` 165 166当前标题栏中子组件的布局同预期还有些差异,接下来通过margin属性,设置各个元素的左右间距(详见[尺寸设置](../../reference/apis-arkui/arkui-ts/ts-universal-attributes-size.md))。如下图所示,最终顶部工具栏在默认设备和平板上都可以达到预期显示效果。 167 168| 默认设备 | 平板 | 169| ---------------------------------------- | ---------------------------------------- | 170|  |  | 171 172 173```ts 174@Entry 175@Component 176 struct TopArea { 177 build() { 178 Flex({ alignItems: ItemAlign.Center }) { 179 Image($r('app.media.back')) //在应用的resources/base/media目录放置名为back的资源文件 180 .width(24) 181 .height(24) 182 .margin({ left:24 }) // 设置间距 183 Image($r('app.media.contact')) //在应用的resources/base/media目录放置名为contact的资源文件 184 .width(40) 185 .height(40) 186 .margin({ left:16, right:16 }) // 设置间距 187 Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Center, 188 alignItems: ItemAlign.Start}) { 189 Text('张三').fontSize(16).fontColor("#182431") 190 Text('+123 4567 8901').fontSize(14).fontColor("#66182431") 191 } 192 Blank() 193 Image($r("app.media.phone")) //在应用的resources/base/media目录放置名为phone的资源文件 194 .width(24) 195 .height(24) 196 Image($r('app.media.dots')) //在应用的resources/base/media目录放置名为dots的资源文件 197 .width(24) 198 .height(24) 199 .margin({ left:16, right:24 }) // 设置间距 200 } 201 .width('100%') 202 .height(56) 203 .backgroundColor('#87CEFA') // 顶部标题栏背景色,仅用于开发测试 204 } 205 } 206``` 207 208 209### 底部输入栏 210 211有了顶部工具栏的开发经验,可以发现底部输入栏的结构更为简单,它同样以Flex组件作为父容器,同时包含文本输入框(请访问[文本输入组件](../../reference/apis-arkui/arkui-ts/ts-basic-components-textarea.md)查看详细介绍)和消息发送图标两个子节点。 212 213 214 215为了便于查看的效果,我们同样给底部输入栏设置了淡蓝色到背景色。注意这里有一个特殊的地方,我们给TextArea设置了flexGrow(1)属性。flexGrow属性仅在父组件是Flex组件时生效,表示Flex容器的剩余空间分配给此属性所在的组件的比例,flexGrow(1)表示父容器的剩余空间全部分配给此组件,详见[Flex布局](../../reference/apis-arkui/arkui-ts/ts-universal-attributes-flex-layout.md)。 216 217| 默认设备 | 平板 | 218| ---------------------------------------- | ---------------------------------------- | 219|  |  | 220 221 222```ts 223@Entry 224@Component 225 struct BottomArea { 226 build() { 227 Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { 228 TextArea({ placeholder:'短信' }) 229 .placeholderColor("#99000000") 230 .caretColor("#007DFF") 231 .backgroundColor("#F1F3F5") 232 .borderRadius(20) 233 .height(40) 234 .flexGrow(1) // 将父容器的剩余空间全部分配给此组件 235 236 Image($r("app.media.send")) //在应用的resources/base/media目录放置名为send的资源文件 237 .height(36) 238 .width(36) 239 .opacity(0.4) 240 .margin({ left:12 }) 241 } 242 .height(72) 243 .width('100%') 244 .padding({ left:24, right:24, bottom:8, top:8 }) 245 .backgroundColor('#87CEFA') // 底部输入栏背景色,仅用于开发测试 246 } 247 } 248``` 249 250 251### 信息列表 252 253观察信息列表区域,可以发现它是由一个个消息气泡组成的,另外消息气泡在默认设备和平板上的布局有差异。本小节将围绕如下两个主题介绍如何实现消息列表。 254 255- 如何实现自定义消息气泡组件。 256 257- 如何在默认设备和平板上自适应布局。 258 259 | 默认设备 | 平板 | 260 | ---------------------------------------- | ---------------------------------------- | 261 |  |  | 262 263**消息气泡** 264 265先做一个最简单的消息气泡,通过borderRadius属性可以设置边框的圆角半径(详见[边框设置](../../reference/apis-arkui/arkui-ts/ts-universal-attributes-border.md))。 266 267| 默认设备 | 平板 | 268| ---------------------------------------- | ---------------------------------------- | 269|  |  | 270 271 272```ts 273@Entry 274@Component 275struct MessageBubble { 276 private content: string = "Introduction" 277 278 build() { 279 Column() { 280 Flex({ alignItems: ItemAlign.Center, justifyContent: FlexAlign.End }) { 281 Text(this.content) 282 .fontSize(16) 283 .lineHeight(21) 284 .padding({ left: 12, right: 12, top: 8, bottom: 8 }) 285 .backgroundColor("#C0EBDF") 286 .borderRadius(24) 287 .fontColor("#182431") 288 }.width('100%') 289 } 290 .margin({left: 24, right: 24 }) 291 .backgroundColor('#87CEFA') // 消息背景色,仅用于开发和测试 292 } 293} 294``` 295 296注意这个简单的消息气泡,左上角(或右上角)的样式,与实际期望不符。我们先修改发送消息右上角的样式,接收消息左上角的实现与之类似。 297 298[Stack组件](../../reference/apis-arkui/arkui-ts/ts-container-stack.md)是一个堆叠容器,其子组件按照轴方向依次堆叠,后一个子组件覆盖前一个子组件。通过其alignContent接口,可以设置子组件在容器内的对齐方式,如alignContent: Alignment.TopStart代表子组件从左上角对齐。 299 300| 默认设备 | 平板 | 301| ---------------------------------------- | ---------------------------------------- | 302|  |  | 303 304 305```ts 306@Entry 307@Component 308struct MessageBubble { 309 private content: string = "Introduction" 310 311 build() { 312 Column() { 313 Flex({ alignItems: ItemAlign.Center, justifyContent: FlexAlign.End }) { 314 Stack({ alignContent: Alignment.TopEnd }) { // 在左上角堆叠一个小色块 315 Column() 316 .backgroundColor("#C0EBDF") 317 .borderRadius(4) 318 .width(24) 319 .height(24) 320 Text(this.content) 321 .fontSize(16) 322 .lineHeight(21) 323 .padding({ left: 12, right: 12, top: 8, bottom: 8 }) 324 .backgroundColor("#C0EBDF") 325 .borderRadius(24) 326 .fontColor("#182431") 327 } 328 }.width('100%') 329 } 330 .margin({left: 24, right: 24 }) 331 .backgroundColor('#87CEFA') // 消息背景色,仅用于开发和测试 332 } 333} 334``` 335 336接下来我们在消息气泡下方加上时间显示,如下图所示,一个消息气泡自定义组件就基本完成了。 337 338| 默认设备 | 平板 | 339| ---------------------------------------- | ---------------------------------------- | 340|  |  | 341 342 343```ts 344@Entry 345@Component 346struct MessageBubble { 347 private content: string = "Introduction" 348 private time: string = "上午 10:35" 349 350 build() { 351 Column() { 352 Flex({ alignItems: ItemAlign.Center, justifyContent: FlexAlign.End }) { 353 Stack({ alignContent: Alignment.TopEnd }) { 354 Column() 355 .backgroundColor("#C0EBDF") 356 .borderRadius(4) 357 .width(24) 358 .height(24) 359 Text(this.content) 360 .fontSize(16) 361 .lineHeight(21) 362 .padding({ left: 12, right: 12, top: 8, bottom: 8 }) 363 .backgroundColor("#C0EBDF") 364 .borderRadius(24) 365 .fontColor("#182431") 366 } 367 }.width('100%') 368 369 // 在消息气泡底部增加时间显示 370 Flex({ alignItems: ItemAlign.Center, direction: FlexDirection.Row, 371 justifyContent: FlexAlign.End}) { 372 Text(this.time) 373 .textAlign(TextAlign.Start) 374 .fontSize(10) 375 .lineHeight(13) 376 .fontColor("#99182431") 377 }.width('100%').margin({ left: 12, right: 24 }) 378 } 379 .margin({left: 24, right: 24 }) 380 .backgroundColor('#87CEFA') // 消息背景色,仅用于开发和测试 381 } 382} 383``` 384 385发送出的消息和接收到的消息的消息气泡结构基本一致,可以通过增加一个标志位,让两种消息共用MessageBubble这个自定义组件,代码如下所示。将这个标志位设置true,可以查看接收消息的效果。 386 387| 默认设备 | 平板 | 388| ---------------------------------------- | ---------------------------------------- | 389|  |  | 390 391 392```ts 393@Entry 394@Component 395 struct MessageBubble { 396 private isReceived:boolean = true // 通过标志位,判断是发送or接收场景,进而使用不同的样式 397 private content:string = "Introduction" 398 private time:string = "今天 10:35" 399 400 build() { 401 Column() { 402 Flex({ justifyContent:this.isReceived? FlexAlign.Start: FlexAlign.End, 403 alignItems: ItemAlign.Center }) { 404 Stack({ alignContent:this.isReceived? Alignment.TopStart: Alignment.TopEnd }) { 405 Column() 406 .backgroundColor(this.isReceived?"#FFFFFF":"#C0EBDF") 407 .borderRadius(4) 408 .width(24) 409 .height(24) 410 Text(this.content) 411 .fontSize(16) 412 .lineHeight(21) 413 .padding({ left:12, right:12, top:8, bottom:8 }) 414 .backgroundColor(this.isReceived?"#FFFFFF":"#C0EBDF") 415 .borderRadius(24) 416 .fontColor("#182431") 417 } 418 }.width('100%') 419 420 Flex({ alignItems: ItemAlign.Center, direction: FlexDirection.Row, 421 justifyContent:this.isReceived? FlexAlign.Start: FlexAlign.End }) { 422 Text(this.time) 423 .textAlign(TextAlign.Start) 424 .fontSize(10) 425 .lineHeight(13) 426 .fontColor("#99182431") 427 }.width('100%') 428 .margin({ left:this.isReceived?12:0, right:this.isReceived?0:12 }) 429 } 430 .margin({left:24, right:24 }) 431 .backgroundColor('#87CEFA') // 消息背景色,仅用于开发和测试 432 } 433 } 434``` 435 436**栅格布局** 437 438回顾方舟开发框架一多能力,消息气泡在默认设备和平板上布局不同,可以借助栅格布局来解决。为了方便测试,我们预定义一个全局数组。 439 440 441```ts 442 interface globalMessageItem { 443 time:string, 444 content:string, 445 isReceived:boolean 446 } 447 448const globalMessageList:globalMessageItem[] = [ 449 { 450 time:'上午 10:20', 451 content:'项目介绍', 452 isReceived:false 453 }, 454 { 455 time:'上午 10:28', 456 content:'"一次开发,多端部署"支撑开发者快速高效的开发支持多种终端设备形态的应用,实现对不同设备兼容的同时,提供跨设备的流转、迁移和协同的分布式体验', 457 isReceived:false 458 }, 459 { 460 time:'上午 10:32', 461 content:'系统能力', 462 isReceived:true 463 }, 464 { 465 time:'上午 10:35', 466 content:'系统能力(即SystemCapability,缩写为SysCap)指操作系统中每一个相对独立的特性,如蓝牙,WIFI,NFC,摄像头等,都是系统能力之一。每个系统能力对应多个API,随着目标设备是否支持该系统能力共同存在或消失。', 467 isReceived:true 468 } 469] 470``` 471 472结合[栅格组件](../../reference/apis-arkui/arkui-ts/ts-container-gridcontainer.md)的定义,考虑我们当前的实际场景,GridRow的各参数设置如下。 473 474- columns:栅格组件中的列数,当前场景默认12列即可。 475 476- gutter:栅格布局列间距,当前场景未使用该参数,默认设置为0即可。 477 478- margin: 栅格布局两侧间距,在开发消息气泡组件时,已经设置了左右间距,故该属性也默认配置为0。 479 480栅格中仅包含我们自定义的消息气泡组件,该组件在各断点上的参数配置如下。 481 482| 断点 | 窗口宽度(vp) | 栅格总列数 | 消息气泡占用的列数 | 接收场景偏移的列数 | 发送场景偏移的列数 | 483| ---- | --------------- | ----- | --------- | --------- | --------- | 484| sm | [320, 600) | 12 | 12 | 0 | 0 | 485| md | [600, 840) | 12 | 8 | 0 | 4 | 486| lg | [840, +∞) | 12 | 8 | 0 | 4 | 487 488| 默认设备 | 平板 | 489| ---------------------------------------- | ---------------------------------------- | 490|  |  | 491 492 493```ts 494import { globalMessageList } from "../data/globalMessageList"; //将globalMessageList文件export后导入; 495import { MessageBubble } from "./MessageBubble"; //将文件MessageBubble去除@entry作为组件export后导入; 496@Component 497export default struct MessageItem { 498 private isReceived?: boolean 499 private content?: string 500 private time?: string 501 502 build() { 503 GridRow() { 504 GridCol({span: {sm: 12, md: 8, lg: 8}, 505 offset: {sm: 0, md: this.isReceived? 0 : 4, lg: this.isReceived? 0 : 4}}) { 506 Flex({ justifyContent: FlexAlign.End, alignItems: ItemAlign.End }) { 507 MessageBubble({ 508 isReceived: this.isReceived, 509 content: this.content, 510 time: this.time 511 }) 512 } 513 } 514 } 515 } 516} 517 518@Entry 519@Component 520struct Conversation { 521 build() { 522 Column() { // 验证效果 523 MessageItem({ 524 isReceived: globalMessageList[1].isReceived, 525 content: globalMessageList[1].content, 526 time: globalMessageList[1].time 527 }) 528 MessageItem({ 529 isReceived: globalMessageList[3].isReceived, 530 content: globalMessageList[3].content, 531 time: globalMessageList[3].time 532 }) 533 }.backgroundColor('#87CEFA') // 消息背景色,仅用于开发和测试 534 } 535} 536``` 537 538 539### 组合成型 540 541现在会话详情页面的顶部标题栏、信息列表及底部输入栏都已经准备完毕,将这三部分组合起来即可得到完整的页面。 542 543- 通过[Flex组件](../../reference/apis-arkui/arkui-ts/ts-container-flex.md)将三个部分组合起来,注意justifyContent: FlexAlign.SpaceBetween配置项是将Flex组件中的元素按照主轴方向均匀分配,其中第一个元素与顶部对齐,最后一个元素与底部对齐。 544 545- 通过[List组件](../../reference/apis-arkui/arkui-ts/ts-container-list.md)和[ForEach语法](../../quick-start/arkts-rendering-control-foreach.md),显示整个消息列表。 546 547 | 默认设备 | 平板 | 548 | ---------------------------------------- | ---------------------------------------- | 549 |  |  | 550 551 552```ts 553import { globalMessageList,globalMessageItem} from "../data/globalMessageList"; //将globalMessageList、globalMessageItem文件export后导入; 554import { MessageItem } from "./MessageItem"; //将文件MessageItem去除@entry作为组件export后导入; 555import { BottomArea } from "./BottomArea"; //将文件BottomArea去除@entry作为组件export后导入; 556import {TopArea } from "./TopArea"; //将文件TopArea去除@entry作为组件export后导入; 557 @Entry 558 @Component 559 struct Conversation { 560 build() { 561 Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Start, 562 justifyContent: FlexAlign.SpaceBetween }) { 563 Column() { 564 TopArea() // 顶部标题栏 565 List() { // 消息列表 566 ForEach(globalMessageList, (item:globalMessageItem, index) => { 567 ListItem() { 568 MessageItem({ 569 isReceived: item.isReceived, 570 content: item.content, 571 time: item.time 572 }) 573 }}) 574 } 575 .listDirection(Axis.Vertical) 576 .edgeEffect(EdgeEffect.Spring) 577 } 578 BottomArea() // 底部输入栏 579 } 580 .backgroundColor("#F1F3F5") 581 .width('100%') 582 .height('100%') 583 } 584 } 585``` 586 587短信应用在默认设备和平板上的功能完全相同,因此选择了部署模型A。借助方舟开发框架一多能力,短信应用实现了在默认设备和平板上共用同一份代码,同时自然也共用安装包。 588 589在实际开发过程中,会话详情页面需要从底层做数据交互,同时还要支持信息选择、信息删除、信息发送状态、输入框与输入法联动等等功能,会比本小节中介绍的基础版本复杂很多。 590 591## 相关实例 592 593基于短信,可参考以下实例: 594 595- [信息应用](https://gitee.com/openharmony/applications_mms/tree/master) 596 597 598