1# 控制页面刷新范围
2
3## 场景说明
4在实现页面UI时,业务方需要根据业务逻辑动态更新组件的状态,常见的如在手机桌面长按某个App的图标时,图标背景色、大小等会发生变化。根据业务需要,有时我们需要触发单个组件的状态更新,有时需要触发部分或全部组件的状态更新。那么如何控制组件状态刷新的范围呢?本例将为大家提供一种参考方案。
5
6## 效果呈现
7本例最终效果如下:
8
9![part-and-full-refresh](figures/overall-and-part-refresh.gif)
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![solving-plan](figures/solving-plan.PNG)
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)