• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# 应用闪屏问题解决方案
2
3## 概述
4
5在开发调试过程中,有时会遇到应用出现非预期的闪动,这些闪动现象统称为闪屏问题。这些闪屏问题触发原因不同,表现形式不同,但都会对应用的体验性和流畅度产生影响。
6
7本文将概述如下几种常见的闪屏场景,对其成因进行深入分析,并提供针对性解决方案,以帮助开发者有效地应对这些问题。
8
9- 动画过程闪屏
10- 刷新过程闪屏
11
12## 常见问题
13
14### 动画过程中,应用连续点击场景下的闪屏问题
15
16**问题现象**
17
18在经过连续点击后,图标大小会出现不正常的放大缩小,产生闪屏问题。
19
20![](figures/screen_flicker_solution_click_error.gif)
21
22```ts
23@Entry
24@Component
25struct ClickError {
26  @State scaleValue: number = 0.5; // 缩放比
27  @State animated: boolean = true; // 控制放大缩小
28
29  build() {
30    Stack() {
31      Stack() {
32        Text('click')
33          .fontSize(45)
34          .fontColor(Color.White)
35      }
36      .borderRadius(50)
37      .width(100)
38      .height(100)
39      .backgroundColor('#e6cfe6')
40      .scale({ x: this.scaleValue, y: this.scaleValue })
41      .onClick(() => {
42        this.getUIContext().animateTo({
43          curve: Curve.EaseInOut,
44          duration: 350,
45          onFinish: () => {
46            // 动画结束判断最后缩放大小
47            const EPSILON: number = 1e-6;
48            if (Math.abs(this.scaleValue - 0.5) < EPSILON) {
49              this.scaleValue = 1;
50            } else {
51              this.scaleValue = 2;
52            }
53          }
54        }, () => {
55          this.animated = !this.animated;
56          this.scaleValue = this.animated ? 0.5 : 2.5;
57        })
58      })
59    }
60    .height('100%')
61    .width('100%')
62  }
63}
64```
65
66**可能原因**
67
68应用在动画结束回调中,修改了属性的值。在图标连续放大缩小过程中,既有动画连续地改变属性的值,又有结束回调直接改变属性的值,造成过程中的值异常,效果不符合预期。一般在所有动画结束后可恢复正常,但会有跳变。
69
70**解决措施**
71
72- 尽量不在动画结束回调中设值,所有的设值都通过动画下发,让系统自动处理动画的衔接;
73- 如果一定要在动画结束回调中设值,可以通过计数器等方法,判断属性上是否还有动画。只有属性上最后一个动画结束时,结束回调中才设值,避免因动画打断造成异常。
74
75```ts
76@Entry
77@Component
78struct ClickRight {
79  @State scaleValue: number = 0.5; // 缩放比
80  @State animated: boolean = true; // 控制放大缩小
81  @State cnt: number = 0; // 执行次数计数器
82
83  build() {
84    Stack() {
85      Stack() {
86        Text('click')
87          .fontSize(45)
88          .fontColor(Color.White)
89      }
90      .borderRadius(50)
91      .width(100)
92      .height(100)
93      .backgroundColor('#e6cfe6')
94      .scale({ x: this.scaleValue, y: this.scaleValue })
95      .onClick(() => {
96        // 下发动画时,计数加1
97        this.cnt = this.cnt + 1;
98        this.getUIContext().animateTo({
99          curve: Curve.EaseInOut,
100          duration: 350,
101          onFinish: () => {
102            // 动画结束时,计数减1
103            this.cnt = this.cnt - 1;
104            // 计数为0表示当前最后一次动画结束
105            if (this.cnt === 0) {
106              // 动画结束判断最后缩放大小
107              const EPSILON: number = 1e-6;
108              if (Math.abs(this.scaleValue - 0.5) < EPSILON) {
109                this.scaleValue = 1;
110              } else {
111                this.scaleValue = 2;
112              }
113            }
114          }
115        }, () => {
116          this.animated = !this.animated;
117          this.scaleValue = this.animated ? 0.5 : 2.5;
118        })
119      })
120    }
121    .height('100%')
122    .width('100%')
123  }
124}
125```
126
127运行效果如下图所示。
128
129![](figures/screen_flicker_solution_click_right.gif)
130
131### 动画过程中,Tabs页签切换场景下的闪屏问题
132
133**问题现象**
134
135滑动Tabs组件时,上方标签不能同步更新,在下方内容完全切换后才会闪动跳转,产生闪屏问题。
136
137![](figures/screen_flicker_solution_tabs_error.gif)
138
139```ts
140@Entry
141@Component
142struct TabsError {
143  tabsWidth: number = 100;
144  @State currentIndex: number = 0;
145  @State animationDuration: number = 300;
146  @State indicatorLeftMargin: number = 0;
147  @State indicatorWidth: number = 0;
148  private textInfos: [number, number][] = [];
149  private isStartAnimateTo: boolean = false;
150
151  @Builder
152  tabBuilder(index: number, name: string) {
153    Column() {
154      Text(name)
155        .fontSize(16)
156        .fontColor(this.currentIndex === index ? $r('sys.color.brand') : $r('sys.color.ohos_id_color_text_secondary'))
157        .fontWeight(this.currentIndex === index ? 500 : 400)
158        .id(index.toString())
159        .onAreaChange((_oldValue: Area, newValue: Area) => {
160          this.textInfos[index] = [newValue.globalPosition.x as number, newValue.width as number];
161          if (this.currentIndex === index && !this.isStartAnimateTo) {
162            this.indicatorLeftMargin = this.textInfos[index][0];
163            this.indicatorWidth = this.textInfos[index][1];
164          }
165        })
166    }.width('100%')
167  }
168
169  build() {
170    Stack({ alignContent: Alignment.TopStart }) {
171      Tabs({ barPosition: BarPosition.Start }) {
172        TabContent() {
173          Column()
174            .width('100%')
175            .height('100%')
176            .backgroundColor(Color.Green)
177            .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
178        }
179        .tabBar(this.tabBuilder(0, 'green'))
180        .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
181
182        TabContent() {
183          Column()
184            .width('100%')
185            .height('100%')
186            .backgroundColor(Color.Blue)
187            .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
188        }
189        .tabBar(this.tabBuilder(1, 'blue'))
190        .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
191
192        TabContent() {
193          Column()
194            .width('100%')
195            .height('100%')
196            .backgroundColor(Color.Yellow)
197            .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
198        }
199        .tabBar(this.tabBuilder(2, 'yellow'))
200        .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
201
202        TabContent() {
203          Column()
204            .width('100%')
205            .height('100%')
206            .backgroundColor(Color.Pink)
207            .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
208        }
209        .tabBar(this.tabBuilder(3, 'pink'))
210        .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
211      }
212      .barWidth('100%')
213      .barHeight(56)
214      .width('100%')
215      .backgroundColor('#F1F3F5')
216      .animationDuration(this.animationDuration)
217      .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
218      .onChange((index: number) => {
219        this.currentIndex = index; // 监听索引index的变化,实现页签内容的切换。
220      })
221
222      Column()
223        .height(2)
224        .borderRadius(1)
225        .width(this.indicatorWidth)
226        .margin({ left: this.indicatorLeftMargin, top: 48 })
227        .backgroundColor($r('sys.color.brand'))
228    }.width('100%')
229  }
230}
231```
232
233**可能原因**
234
235在Tabs左右翻页动画的结束回调中,刷新了选中页面的index值。造成当页面左右转场动画结束时,页签栏中index对应页签的样式(字体大小、下划线等)立刻发生改变,导致产生闪屏。
236
237**解决措施**
238
239在左右跟手翻页过程中,通过TabsAnimationEvent事件获取手指滑动距离,改变下划线在前后两个子页签之间的位置。在离手触发翻页动画时,一并触发下划线动画,保证下划线与页面左右转场动画同步进行。
240
241```ts
242build() {
243  Stack({ alignContent: Alignment.TopStart }) {
244    Tabs({ barPosition: BarPosition.Start }) {
245      TabContent() {
246        Column()
247          .width('100%')
248          .height('100%')
249          .backgroundColor(Color.Green)
250          .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
251      }
252      .tabBar(this.tabBuilder(0, 'green'))
253      .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
254
255      TabContent() {
256        Column()
257          .width('100%')
258          .height('100%')
259          .backgroundColor(Color.Blue)
260          .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
261      }
262      .tabBar(this.tabBuilder(1, 'blue'))
263      .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
264
265      TabContent() {
266        Column()
267          .width('100%')
268          .height('100%')
269          .backgroundColor(Color.Yellow)
270          .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
271      }
272      .tabBar(this.tabBuilder(2, 'yellow'))
273      .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
274
275      TabContent() {
276        Column()
277          .width('100%')
278          .height('100%')
279          .backgroundColor(Color.Pink)
280          .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
281      }
282      .tabBar(this.tabBuilder(3, 'pink'))
283      .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
284    }
285    .onAreaChange((_oldValue: Area, newValue: Area) => {
286      this.tabsWidth = newValue.width as number;
287    })
288    .barWidth('100%')
289    .barHeight(56)
290    .width('100%')
291    .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
292    .backgroundColor('#F1F3F5')
293    .animationDuration(this.animationDuration)
294    .onChange((index: number) => {
295      this.currentIndex = index; // 监听索引index的变化,实现页签内容的切换。
296    })
297    .onAnimationStart((_index: number, targetIndex: number) => {
298      // 切换动画开始时触发该回调。下划线跟着页面一起滑动,同时宽度渐变。
299      this.currentIndex = targetIndex;
300      this.startAnimateTo(this.animationDuration, this.textInfos[targetIndex][0], this.textInfos[targetIndex][1]);
301    })
302    .onAnimationEnd((index: number, event: TabsAnimationEvent) => {
303      // 切换动画结束时触发该回调。下划线动画停止。
304      let currentIndicatorInfo = this.getCurrentIndicatorInfo(index, event);
305      this.startAnimateTo(0, currentIndicatorInfo.left, currentIndicatorInfo.width);
306    })
307    .onGestureSwipe((index: number, event: TabsAnimationEvent) => {
308      // 在页面跟手滑动过程中,逐帧触发该回调。
309      let currentIndicatorInfo = this.getCurrentIndicatorInfo(index, event);
310      this.currentIndex = currentIndicatorInfo.index;
311      this.indicatorLeftMargin = currentIndicatorInfo.left;
312      this.indicatorWidth = currentIndicatorInfo.width;
313    })
314
315    Column()
316      .height(2)
317      .borderRadius(1)
318      .width(this.indicatorWidth)
319      .margin({ left: this.indicatorLeftMargin, top: 48 })
320      .backgroundColor($r('sys.color.brand'))
321  }
322  .width('100%')
323}
324```
325
326TabsAnimationEvent方法如下所示。
327
328```ts
329private getCurrentIndicatorInfo(index: number, event: TabsAnimationEvent): Record<string, number> {
330  let nextIndex = index;
331  if (index > 0 && event.currentOffset > 0) {
332    nextIndex--;
333  } else if (index < 3 && event.currentOffset < 0) {
334    nextIndex++;
335  }
336  let indexInfo = this.textInfos[index];
337  let nextIndexInfo = this.textInfos[nextIndex];
338  let swipeRatio = Math.abs(event.currentOffset / this.tabsWidth);
339  let currentIndex = swipeRatio > 0.5 ? nextIndex : index; // 页面滑动超过一半,tabBar切换到下一页。
340  let currentLeft = indexInfo[0] + (nextIndexInfo[0] - indexInfo[0]) * swipeRatio;
341  let currentWidth = indexInfo[1] + (nextIndexInfo[1] - indexInfo[1]) * swipeRatio;
342  return { 'index': currentIndex, 'left': currentLeft, 'width': currentWidth };
343}
344private startAnimateTo(duration: number, leftMargin: number, width: number) {
345  this.isStartAnimateTo = true;
346  this.getUIContext().animateTo({
347    duration: duration, // 动画时长
348    curve: Curve.Linear, // 动画曲线
349    iterations: 1, // 播放次数
350    playMode: PlayMode.Normal, // 动画模式
351    onFinish: () => {
352      this.isStartAnimateTo = false;
353      console.info('play end');
354    }
355  }, () => {
356    this.indicatorLeftMargin = leftMargin;
357    this.indicatorWidth = width;
358  })
359}
360```
361
362运行效果如下图所示。
363
364![](figures/screen_flicker_solution_tabs_right.gif)
365
366### 刷新过程中,ForEach键值生成函数未设置导致的闪屏问题
367
368**问题现象**
369
370下拉刷新时,应用产生卡顿,出现闪屏问题。
371
372![](figures/screen_flicker_solution_pull_to_refresh_error.gif)
373
374```ts
375@Builder
376private getListView() {
377  List({
378    space: 12, scroller: this.scroller
379  }) {
380    // 使用懒加载组件渲染数据
381    ForEach(this.newsData, (item: NewsData) => {
382      ListItem() {
383        newsItem({
384          newsTitle: item.newsTitle,
385          newsContent: item.newsContent,
386          newsTime: item.newsTime,
387          img: item.img
388        })
389      }
390      .backgroundColor(Color.White)
391      .borderRadius(16)
392    });
393  }
394  .width('100%')
395  .height('100%')
396  .padding({
397    left: 16,
398    right: 16
399  })
400  .backgroundColor('#F1F3F5')
401  // 必须设置列表为滑动到边缘无效果,否则无法触发pullToRefresh组件的上滑下拉方法。
402  .edgeEffect(EdgeEffect.None)
403}
404```
405
406**可能原因**
407
408ForEach提供了一个名为keyGenerator的参数,这是一个函数,开发者可以通过它自定义键值的生成规则。如果开发者没有定义keyGenerator函数,则ArkUI框架会使用默认的键值生成函数,即(item: Object, index: number) => { return index + '__' + JSON.stringify(item); }。可参考[键值生成规则](../ui/state-management/arkts-rendering-control-foreach.md#键值生成规则)。
409
410在使用ForEach的过程中,若对于键值生成规则的理解不够充分,可能会出现错误的使用方式。错误使用一方面会导致功能层面问题,例如渲染结果非预期,另一方面会导致性能层面问题,例如渲染性能降低。
411
412**解决措施**
413
414在ForEach第三个参数中定义自定义键值的生成规则,即(item: NewsData, index?: number) => item.id,这样可以在渲染时降低重复组件的渲染开销,从而消除闪屏问题。可参考[ForEach组件使用建议](../ui/state-management/arkts-rendering-control-foreach.md#使用建议)。
415
416```ts
417@Builder
418private getListView() {
419  List({
420    space: 12, scroller: this.scroller
421  }) {
422    // 使用懒加载组件渲染数据
423    ForEach(this.newsData, (item: NewsData) => {
424      ListItem() {
425        newsItem({
426          newsTitle: item.newsTitle,
427          newsContent: item.newsContent,
428          newsTime: item.newsTime,
429          img: item.img
430        })
431      }
432      .backgroundColor(Color.White)
433      .borderRadius(16)
434    }, (item: NewsData) => item.newsId);
435  }
436  .width('100%')
437  .height('100%')
438  .padding({
439    left: 16,
440    right: 16
441  })
442  .backgroundColor('#F1F3F5')
443  // 必须设置列表为滑动到边缘无效果,否则无法触发pullToRefresh组件的上滑下拉方法。
444  .edgeEffect(EdgeEffect.None)
445}
446```
447
448运行效果如下图所示。
449
450![](figures/screen_flicker_solution_pull_to_refresh_right.gif)
451
452## 总结
453
454当出现应用闪屏相关问题时,首先定位可能出现的原因,分别测试是否为当前原因导致。定位到问题后尝试使用对应解决方案,从而消除对应问题现象。
455
456- 应用连续点击场景下,通过计数器优化动画逻辑。
457- Tabs页签切换场景下,完善动画细粒度,提高流畅表现。
458- ForEach刷新内容过程中,根据业务场景调整键值生成函数。
459