1# 控制页面刷新范围 2 3## 场景说明 4在实现页面UI时,业务方需要根据业务逻辑动态更新组件的状态,常见的如在手机桌面长按某个App的图标时,图标背景色、大小等会发生变化。根据业务需要,有时我们需要触发单个组件的状态更新,有时需要触发部分或全部组件的状态更新。那么如何控制组件状态刷新的范围呢?本例将为大家提供一种参考方案。 5 6## 效果呈现 7本例最终效果如下: 8 9 10 11## 运行环境 12本例基于以下环境开发,开发者也可以基于其他适配的版本进行开发: 13 14- IDE: DevEco Studio 3.1 Release 15- SDK: Ohos_sdk_public 3.2.12.5(API Version 9 Release) 16 17 18## 实现思路 19ArkUI可以通过页面的状态数据驱动UI的更新,其UI更新机制可以通过如下表达式来体现: 20 21***UI=f(state)*** 22 23用户构建了UI模型,其中参数state代表页面组件的运行时状态。当state改变时,UI作为返回结果,也将进行对应的改变刷新。f作为状态管理机制,维护组件运行时的状态变化所带来的UI重新渲染。组件的状态改变可通过状态变量进行控制。 24 25基于上述理论,如果要控制页面的更新范围,我们必须要:**定义准确状态变量,并控制状态变量影响的组件范围。** 26 27本例中包含8个APP图标,其中涉及5种状态变化,按照局部刷新和全局刷新可分为: 28 29- 局部刷新(单个卡片变化) 30 31 - 点击卡片,卡片背景色变为红色。 32 - 点击卡片,卡片进行缩放。 33 - 拖拽卡片,卡片位置变化。 34 35- 全局刷新(全部卡片变化) 36 37 - 长按某个卡片,为所有卡片添加删除图标。 38 - 点击删除图标外的任意地方,删除图标消失。 39 40所以处理思路为,控制局部刷新的状态变量在子组件中定义,绑定子组件,控制全局刷新的状态变量在父组件中进行定义,并由父组件传递给所有子组件。如下图: 41 42 43 44## 开发步骤 45由于本例重点讲解刷新区域的控制,所以开发步骤会着重讲解相关实现,不相关的内容不做介绍,全量代码可参考完整代码章节。 461. 创建APP卡片组件作为子组件,每个卡片包含文本和删除图标。 47具体代码如下: 48 ```ts 49 @Component 50 export struct AppItem { 51 ... 52 build() { 53 Stack({ alignContent: Alignment.TopEnd }) { 54 Image($r('app.media.ic_public_close')) 55 .height(30) 56 .width(30) 57 .zIndex(2) 58 .offset({ 59 x: -12, 60 y: 12 61 }) 62 Text(this.data.title) 63 .width(100) 64 .height(100) 65 .fontSize(16) 66 .margin(10) 67 .textAlign(TextAlign.Center) 68 .borderRadius(10) 69 } 70 } 71 } 72 ``` 732. 创建父组件,并在父组件中引用子组件。 74具体代码如下: 75 ```ts 76 @Entry 77 @Component 78 struct Sample { 79 ... 80 build() { 81 Stack({ alignContent: Alignment.Bottom }) { 82 Flex({ wrap: FlexWrap.Wrap }) { 83 // 通过循环渲染加载所有子组件 84 ForEach(this.items, (item: ItemProps, index: number) => { 85 // 引用App卡片子组件 86 AppItem({data: this.items[index]}) 87 }, (item: ItemProps) => item.id.toString()) 88 } 89 .width('100%') 90 .height('100%') 91 } 92 .width('100%') 93 .height('100%') 94 .backgroundColor('#ffffff') 95 .margin({ top:50 }) 96 } 97 } 98 ``` 993. 由于卡片背景色变化、卡片缩放、卡片拖拽在触发时都是针对单个卡片的状态变化,所以在卡片子组件中定义相应的状态变量,用来控制单个卡片的状态变化。 100本例中定义状态变量“data”用来控制卡片拖拽时位置的刷新;定义状态变量”downFlag“用来监听卡片是否被按下,从而控制卡片背景色及缩放状态的更新。 101具体代码如下: 102 ```ts 103 @Component 104 export struct AppItem { 105 // 定义状态变量data,用来控制卡片被拖拽时位置的刷新 106 @State data: ItemProps = {}; 107 // 定义状态变量downFlag用来监听卡片是否被按下,从而控制卡片背景色及缩放状态的更新 108 @State downFlag: boolean = false; 109 ... 110 build() { 111 Stack({ alignContent: Alignment.TopEnd }) { 112 Image($r('app.media.ic_public_close')) 113 .height(30) 114 .width(30) 115 .zIndex(2) 116 .offset({ 117 x: -12, 118 y: 12 119 }) 120 Text(this.data.title) 121 .width(100) 122 .height(100) 123 .fontSize(16) 124 .margin(10) 125 .textAlign(TextAlign.Center) 126 .borderRadius(10) 127 // 根据状态变量downFlag的变化,更新背景色 128 .backgroundColor(this.downFlag ? '#EEA8AB' : '#86C7CC') 129 // 背景色更新时添加属性动画 130 .animation({ 131 duration: 500, 132 curve: Curve.Friction 133 }) 134 // 绑定onTouch事件,监听卡片是否被按下,根据不同状态改变downFlag的值 135 .onTouch((event: TouchEvent) => { 136 if (event.type == TouchType.Down) { 137 this.downFlag = true 138 } else if (event.type == TouchType.Up) { 139 this.downFlag = false 140 } 141 }) 142 } 143 // 根据状态变量downFlag的变化,控制卡片的缩放 144 .scale(this.downFlag ? { x: 0.8, y: 0.8 } : { x: 1, y: 1 }) 145 // 通过状态变量data的变化,控制卡片位置的更新 146 .offset({ 147 x: this.data.offsetX, 148 y: this.data.offsetY 149 }) 150 // 拖动触发该手势事件 151 .gesture( 152 GestureGroup(GestureMode.Parallel, 153 ... 154 PanGesture(this.panOption) 155 .onActionStart((event: GestureEvent) => { 156 console.info('Pan start') 157 }) 158 // 拖动卡片时,改变状态变量data的值 159 .onActionUpdate((event: GestureEvent) => { 160 this.data.offsetX = this.data.positionX + event.offsetX 161 this.data.offsetY = this.data.positionY + event.offsetY 162 }) 163 .onActionEnd(() => { 164 this.data.positionX = this.data.offsetX 165 this.data.positionY = this.data.offsetY 166 console.info('Pan end') 167 }) 168 ) 169 ) 170 } 171 } 172 ``` 1734. 长按卡片,卡片右上角会出现删除图标。由于所有卡片右上角都会出现删除图标,所以这里需要做全局的刷新。本例在父组件中定义状态变量“deleteVisibility”,在调用子组件时,将其作为参数传递给所有卡片子组件,并且通过@Link装饰器与子组件进行双向绑定,从而可以控制所有卡片子组件中删除图标的更新。 174**父组件代码:** 175 ```ts 176 @Entry 177 @Component 178 struct Sample { 179 ... 180 // 定义状态变量deleteVisibility,控制App卡片上删除图标的更新 181 @State deleteVisibility: boolean = false 182 ... 183 build() { 184 Stack({ alignContent: Alignment.Bottom }) { 185 Flex({ wrap: FlexWrap.Wrap }) { 186 // 通过循环渲染加载所有子组件 187 ForEach(this.items, (item: ItemProps, index: number) => { 188 // 将状态变量deleteVisibility传递给每一个子组件,从而deleteVisibility变化时可以触发所有子组件的更新 189 AppItem({ deleteVisibility: $deleteVisibility, data: this.items[index], onDeleteClick: this.delete }) 190 }, (item: ItemProps) => item.id.toString()) 191 } 192 .width('100%') 193 .height('100%') 194 } 195 .width('100%') 196 .height('100%') 197 .backgroundColor('#ffffff') 198 .margin({ top:50 }) 199 .onClick(() => { 200 this.deleteVisibility = false 201 }) 202 } 203 ``` 204 **子组件代码:** 205 ```ts 206 @Component 207 export struct AppItem { 208 ... 209 // 定义deleteVisibility状态变量,并通过@Link装饰器与父组件中的同名变量双向绑定,该变量值发生变化时父子组件可双向同步 210 @Link deleteVisibility: boolean; 211 ... 212 build() { 213 Stack({ alignContent: Alignment.TopEnd }) { 214 // 通过deleteVisibility控制删除图标的隐藏和显示,当deleteVisibility值为true时显示,为false时隐藏 215 if(this.deleteVisibility){ 216 Image($r('app.media.ic_public_close')) 217 .height(30) 218 .width(30) 219 .zIndex(2) 220 // 控制删除图标的显隐 221 .visibility(Visibility.Visible) 222 .offset({ 223 x: -12, 224 y: 12 225 }) 226 .onClick(() => this.onDeleteClick(this.data.id)) 227 }else{ 228 Image($r('app.media.ic_public_close')) 229 .height(30) 230 .width(30) 231 .zIndex(2) 232 .visibility(Visibility.Hidden) 233 .offset({ 234 x: -12, 235 y: 12 236 }) 237 .onClick(() => this.onDeleteClick(this.data.id)) 238 } 239 ... 240 .gesture( 241 GestureGroup(GestureMode.Parallel, 242 // 识别长按手势 243 LongPressGesture({ repeat: true }) 244 .onAction((event: GestureEvent) => { 245 if (event.repeat) { 246 // 长按时改变deleteVisibility的值为true,从而更新删除图标为显示状态 247 this.deleteVisibility = true 248 } 249 console.info('LongPress onAction') 250 }), 251 ... 252 ) 253 ) 254 } 255 } 256 ``` 257 258 259## 完整代码 260本例完整代码如下: 261data.ets文件(数据模型文件) 262```ts 263// data.ets 264// AppItem组件接口信息 265export interface ItemProps { 266 id?: number, 267 title?: string, 268 offsetX?: number, // X偏移量 269 offsetY?: number, // Y偏移量 270 positionX?: number, // 在X的位置 271 positionY?: number, // 在Y的位置 272} 273 274// AppItem初始数据 275export const initItemsData: ItemProps[] = [ 276 { 277 id: 1, 278 title: 'APP1', 279 offsetX: 0, 280 offsetY: 0, 281 positionX: 0, 282 positionY: 0 283 }, 284 { 285 id: 2, 286 title: 'APP2', 287 offsetX: 0, 288 offsetY: 0, 289 positionX: 0, 290 positionY: 0 291 }, 292 { 293 id: 3, 294 title: 'APP3', 295 offsetX: 0, 296 offsetY: 0, 297 positionX: 0, 298 positionY: 0 299 }, 300 { 301 id: 4, 302 title: 'APP4', 303 offsetX: 0, 304 offsetY: 0, 305 positionX: 0, 306 positionY: 0 307 }, 308 { 309 id: 5, 310 title: 'APP5', 311 offsetX: 0, 312 offsetY: 0, 313 positionX: 0, 314 positionY: 0 315 }, 316 { 317 id: 6, 318 title: 'APP6', 319 offsetX: 0, 320 offsetY: 0, 321 positionX: 0, 322 positionY: 0 323 }, 324 { 325 id: 7, 326 title: 'APP7', 327 offsetX: 0, 328 offsetY: 0, 329 positionX: 0, 330 positionY: 0 331 }, 332 { 333 id: 8, 334 title: 'APP8', 335 offsetX: 0, 336 offsetY: 0, 337 positionX: 0, 338 positionY: 0 339 }, 340] 341``` 342AppItem.ets文件(卡片子组件) 343```ts 344// AppItem.ets 345import { ItemProps } from '../model/data'; 346 347@Component 348export struct AppItem { 349 // 定义状态变量data,用来控制卡片被拖拽时位置的刷新 350 @State data: ItemProps = {}; 351 // 定义状态变量downFlag用来监听卡片是否被按下,从而控制卡片背景色及缩放状态的更新 352 @State downFlag: boolean = false; 353 // 定义deleteVisibility状态变量,并通过@Link装饰器与父组件中的同名变量双向绑定,该变量值发生变化时父子组件可双向同步 354 @Link deleteVisibility: boolean; 355 356 private onDeleteClick: (id: number) => void; 357 private panOption: PanGestureOptions = new PanGestureOptions({ direction: PanDirection.All }); 358 build() { 359 Stack({ alignContent: Alignment.TopEnd }) { 360 // 通过deleteVisibility控制删除图标的隐藏和显示,当deleteVisibility值为true时显示,为false时隐藏 361 if(this.deleteVisibility){ 362 Image($r('app.media.ic_public_close')) 363 .height(30) 364 .width(30) 365 .zIndex(2) 366 // 控制删除图标的显隐 367 .visibility(Visibility.Visible) 368 .offset({ 369 x: -12, 370 y: 12 371 }) 372 .onClick(() => this.onDeleteClick(this.data.id)) 373 }else{ 374 Image($r('app.media.ic_public_close')) 375 .height(30) 376 .width(30) 377 .zIndex(2) 378 .visibility(Visibility.Hidden) 379 .offset({ 380 x: -12, 381 y: 12 382 }) 383 .onClick(() => this.onDeleteClick(this.data.id)) 384 } 385 386 Text(this.data.title) 387 .width(100) 388 .height(100) 389 .fontSize(16) 390 .margin(10) 391 .textAlign(TextAlign.Center) 392 .borderRadius(10) 393 // 根据状态变量downFlag的变化,更新背景色 394 .backgroundColor(this.downFlag ? '#EEA8AB' : '#86C7CC') 395 // 背景色更新时添加属性动画 396 .animation({ 397 duration: 500, 398 curve: Curve.Friction 399 }) 400 // 绑定onTouch事件,监听卡片是否被按下,根据不同状态改变downFlag的值 401 .onTouch((event: TouchEvent) => { 402 if (event.type == TouchType.Down) { 403 this.downFlag = true 404 } else if (event.type == TouchType.Up) { // 手指抬起 405 this.downFlag = false 406 } 407 }) 408 } 409 // 根据状态变量downFlag的变化,控制卡片的缩放 410 .scale(this.downFlag ? { x: 0.8, y: 0.8 } : { x: 1, y: 1 }) 411 // 通过状态变量data的变化,控制卡片位置的更新 412 .offset({ 413 x: this.data.offsetX, 414 y: this.data.offsetY 415 }) 416 // 拖动触发该手势事件 417 .gesture( 418 GestureGroup(GestureMode.Parallel, 419 // 识别长按手势 420 LongPressGesture({ repeat: true }) 421 .onAction((event: GestureEvent) => { 422 if (event.repeat) { 423 // 长按时改变deleteVisibility的值为true,从而更新删除图标为显示状态 424 this.deleteVisibility = true 425 } 426 console.info('LongPress onAction') 427 }), 428 PanGesture(this.panOption) 429 .onActionStart((event: GestureEvent) => { 430 console.info('Pan start') 431 }) 432 // 拖动卡片时,改变状态变量data的值 433 .onActionUpdate((event: GestureEvent) => { 434 this.data.offsetX = this.data.positionX + event.offsetX 435 this.data.offsetY = this.data.positionY + event.offsetY 436 }) 437 .onActionEnd(() => { 438 this.data.positionX = this.data.offsetX 439 this.data.positionY = this.data.offsetY 440 console.info('Pan end') 441 }) 442 ) 443 ) 444 } 445} 446``` 447Index.ets文件(父组件) 448```ts 449// Index.ets 450import { AppItem } from '../components/MyItem'; 451import { initItemsData } from '../model/data'; 452import { ItemProps } from '../model/data'; 453 454@Entry 455@Component 456struct Sample { 457 @State items: ItemProps[] = []; 458 // 定义状态变量deleteVisibility,控制App卡片上删除图标的更新 459 @State deleteVisibility: boolean = false 460 461 // 删除指定id组件 462 private delete = (id: number) => { 463 const index = this.items.findIndex(item => item.id === id); 464 this.items.splice(index, 1); 465 } 466 467 // 生命周期函数:组件即将出现时调用 468 aboutToAppear() { 469 this.items = [...initItemsData]; 470 } 471 472 build() { 473 Stack({ alignContent: Alignment.Bottom }) { 474 Flex({ wrap: FlexWrap.Wrap }) { 475 // 通过循环渲染加载所有子组件 476 ForEach(this.items, (item: ItemProps, index: number) => { 477 // 将状态变量deleteVisibility传递给每一个子组件,从而deleteVisibility变化时可以触发所有子组件的更新 478 AppItem({ deleteVisibility: $deleteVisibility, data: this.items[index], onDeleteClick: this.delete }) 479 }, (item: ItemProps) => item.id.toString()) 480 } 481 .width('100%') 482 .height('100%') 483 } 484 .width('100%') 485 .height('100%') 486 .backgroundColor('#ffffff') 487 .margin({ top:50 }) 488 .onClick(() => { 489 // 点击组件,deleteVisibility值变为false,从而隐藏所有卡片的删除图标 490 this.deleteVisibility = false 491 }) 492 } 493} 494``` 495 496 497## 参考 498- [@Link](../application-dev/quick-start/arkts-link.md) 499- [显隐控制](../application-dev/reference/apis-arkui/arkui-ts/ts-universal-attributes-visibility.md) 500- [组合手势](../application-dev/reference/apis-arkui/arkui-ts/ts-combined-gestures.md)