# 应用闪å±é—®é¢˜è§£å†³æ–¹æ¡ˆ ## 概述 在开å‘调试过程ä¸ï¼Œæœ‰æ—¶ä¼šé‡åˆ°åº”用出现éžé¢„期的闪动,这些闪动现象统称为闪å±é—®é¢˜ã€‚这些闪å±é—®é¢˜è§¦å‘åŽŸå› ä¸åŒï¼Œè¡¨çŽ°å½¢å¼ä¸åŒï¼Œä½†éƒ½ä¼šå¯¹åº”用的体验性和æµç•…度产生影å“。 æœ¬æ–‡å°†æ¦‚è¿°å¦‚ä¸‹å‡ ç§å¸¸è§çš„é—ªå±åœºæ™¯ï¼Œå¯¹å…¶æˆå› 进行深入分æžï¼Œå¹¶æ供针对性解决方案,以帮助开å‘者有效地应对这些问题。 - åŠ¨ç”»è¿‡ç¨‹é—ªå± - åˆ·æ–°è¿‡ç¨‹é—ªå± ## 常è§é—®é¢˜ ### 动画过程ä¸ï¼Œåº”用连ç»ç‚¹å‡»åœºæ™¯ä¸‹çš„é—ªå±é—®é¢˜ **问题现象** 在ç»è¿‡è¿žç»ç‚¹å‡»åŽï¼Œå›¾æ ‡å¤§å°ä¼šå‡ºçŽ°ä¸æ£å¸¸çš„放大缩å°ï¼Œäº§ç”Ÿé—ªå±é—®é¢˜ã€‚  ```ts @Entry @Component struct ClickError { @State scaleValue: number = 0.5; // 缩放比 @State animated: boolean = true; // æŽ§åˆ¶æ”¾å¤§ç¼©å° build() { Stack() { Stack() { Text('click') .fontSize(45) .fontColor(Color.White) } .borderRadius(50) .width(100) .height(100) .backgroundColor('#e6cfe6') .scale({ x: this.scaleValue, y: this.scaleValue }) .onClick(() => { animateTo({ curve: Curve.EaseInOut, duration: 350, onFinish: () => { // 动画结æŸåˆ¤æ–最åŽç¼©æ”¾å¤§å° const EPSILON: number = 1e-6; if (Math.abs(this.scaleValue - 0.5) < EPSILON) { this.scaleValue = 1; } else { this.scaleValue = 2; } } }, () => { this.animated = !this.animated; this.scaleValue = this.animated ? 0.5 : 2.5; }) }) } .height('100%') .width('100%') } } ``` **å¯èƒ½åŽŸå› ** 应用在动画结æŸå›žè°ƒä¸ï¼Œä¿®æ”¹äº†å±žæ€§çš„å€¼ã€‚åœ¨å›¾æ ‡è¿žç»æ”¾å¤§ç¼©å°è¿‡ç¨‹ä¸ï¼Œæ—¢æœ‰åŠ¨ç”»è¿žç»åœ°æ”¹å˜å±žæ€§çš„值,åˆæœ‰ç»“æŸå›žè°ƒç›´æŽ¥æ”¹å˜å±žæ€§çš„å€¼ï¼Œé€ æˆè¿‡ç¨‹ä¸çš„值异常,效果ä¸ç¬¦åˆé¢„期。一般在所有动画结æŸåŽå¯æ¢å¤æ£å¸¸ï¼Œä½†ä¼šæœ‰è·³å˜ã€‚ **解决措施** - å°½é‡ä¸åœ¨åŠ¨ç”»ç»“æŸå›žè°ƒä¸è®¾å€¼ï¼Œæ‰€æœ‰çš„设值都通过动画下å‘,让系统自动处ç†åŠ¨ç”»çš„衔接; - 如果一定è¦åœ¨åŠ¨ç”»ç»“æŸå›žè°ƒä¸è®¾å€¼ï¼Œå¯ä»¥é€šè¿‡è®¡æ•°å™¨ç‰æ–¹æ³•ï¼Œåˆ¤æ–属性上是å¦è¿˜æœ‰åŠ¨ç”»ã€‚åªæœ‰å±žæ€§ä¸Šæœ€åŽä¸€ä¸ªåŠ¨ç”»ç»“æŸæ—¶ï¼Œç»“æŸå›žè°ƒä¸æ‰è®¾å€¼ï¼Œé¿å…å› åŠ¨ç”»æ‰“æ–é€ æˆå¼‚常。 ```ts @Entry @Component struct ClickRight { @State scaleValue: number = 0.5; // 缩放比 @State animated: boolean = true; // æŽ§åˆ¶æ”¾å¤§ç¼©å° @State cnt: number = 0; // 执行次数计数器 build() { Stack() { Stack() { Text('click') .fontSize(45) .fontColor(Color.White) } .borderRadius(50) .width(100) .height(100) .backgroundColor('#e6cfe6') .scale({ x: this.scaleValue, y: this.scaleValue }) .onClick(() => { // 下å‘åŠ¨ç”»æ—¶ï¼Œè®¡æ•°åŠ 1 this.cnt = this.cnt + 1; animateTo({ curve: Curve.EaseInOut, duration: 350, onFinish: () => { // 动画结æŸæ—¶ï¼Œè®¡æ•°å‡1 this.cnt = this.cnt - 1; // 计数为0表示当å‰æœ€åŽä¸€æ¬¡åŠ¨ç”»ç»“æŸ if (this.cnt === 0) { // 动画结æŸåˆ¤æ–最åŽç¼©æ”¾å¤§å° const EPSILON: number = 1e-6; if (Math.abs(this.scaleValue - 0.5) < EPSILON) { this.scaleValue = 1; } else { this.scaleValue = 2; } } } }, () => { this.animated = !this.animated; this.scaleValue = this.animated ? 0.5 : 2.5; }) }) } .height('100%') .width('100%') } } ``` è¿è¡Œæ•ˆæžœå¦‚下图所示。  ### 动画过程ä¸ï¼ŒTabs页ç¾åˆ‡æ¢åœºæ™¯ä¸‹çš„é—ªå±é—®é¢˜ **问题现象** 滑动Tabsç»„ä»¶æ—¶ï¼Œä¸Šæ–¹æ ‡ç¾ä¸èƒ½åŒæ¥æ›´æ–°ï¼Œåœ¨ä¸‹æ–¹å†…容完全切æ¢åŽæ‰ä¼šé—ªåŠ¨è·³è½¬ï¼Œäº§ç”Ÿé—ªå±é—®é¢˜ã€‚  ```ts @Entry @Component struct TabsError { @State currentIndex: number = 0; @State animationDuration: number = 300; @State indicatorLeftMargin: number = 0; @State indicatorWidth: number = 0; private textInfos: [number, number][] = []; private isStartAnimateTo: boolean = false; @Builder tabBuilder(index: number, name: string) { Column() { Text(name) .fontSize(16) .fontColor(this.currentIndex === index ? $r('sys.color.brand') : $r('sys.color.ohos_id_color_text_secondary')) .fontWeight(this.currentIndex === index ? 500 : 400) .id(index.toString()) .onAreaChange((_oldValue: Area, newValue: Area) => { this.textInfos[index] = [newValue.globalPosition.x as number, newValue.width as number]; if (this.currentIndex === index && !this.isStartAnimateTo) { this.indicatorLeftMargin = this.textInfos[index][0]; this.indicatorWidth = this.textInfos[index][1]; } }) }.width('100%') } build() { Stack({ alignContent: Alignment.TopStart }) { Tabs({ barPosition: BarPosition.Start }) { TabContent() { Column() .width('100%') .height('100%') .backgroundColor(Color.Green) .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) } .tabBar(this.tabBuilder(0, 'green')) .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) TabContent() { Column() .width('100%') .height('100%') .backgroundColor(Color.Blue) .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) } .tabBar(this.tabBuilder(1, 'blue')) .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) TabContent() { Column() .width('100%') .height('100%') .backgroundColor(Color.Yellow) .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) } .tabBar(this.tabBuilder(2, 'yellow')) .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) TabContent() { Column() .width('100%') .height('100%') .backgroundColor(Color.Pink) .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) } .tabBar(this.tabBuilder(3, 'pink')) .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) } .barWidth('100%') .barHeight(56) .width('100%') .backgroundColor('#F1F3F5') .animationDuration(this.animationDuration) .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) .onChange((index: number) => { this.currentIndex = index; // 监å¬ç´¢å¼•indexçš„å˜åŒ–,实现页ç¾å†…容的切æ¢ã€‚ }) Column() .height(2) .borderRadius(1) .width(this.indicatorWidth) .margin({ left: this.indicatorLeftMargin, top: 48 }) .backgroundColor($r('sys.color.brand')) }.width('100%') } } ``` **å¯èƒ½åŽŸå› ** 在Tabså·¦å³ç¿»é¡µåŠ¨ç”»çš„结æŸå›žè°ƒä¸ï¼Œåˆ·æ–°äº†é€‰ä¸é¡µé¢çš„indexå€¼ã€‚é€ æˆå½“页é¢å·¦å³è½¬åœºåŠ¨ç”»ç»“æŸæ—¶ï¼Œé¡µç¾æ ä¸index对应页ç¾çš„æ ·å¼ï¼ˆå—体大å°ã€ä¸‹åˆ’线ç‰ï¼‰ç«‹åˆ»å‘生改å˜ï¼Œå¯¼è‡´äº§ç”Ÿé—ªå±ã€‚ **解决措施** 在左å³è·Ÿæ‰‹ç¿»é¡µè¿‡ç¨‹ä¸ï¼Œé€šè¿‡TabsAnimationEvent事件获å–手指滑动è·ç¦»ï¼Œæ”¹å˜ä¸‹åˆ’线在å‰åŽä¸¤ä¸ªå页ç¾ä¹‹é—´çš„ä½ç½®ã€‚在离手触å‘翻页动画时,一并触å‘下划线动画,ä¿è¯ä¸‹åˆ’线与页é¢å·¦å³è½¬åœºåŠ¨ç”»åŒæ¥è¿›è¡Œã€‚ ```ts build() { Stack({ alignContent: Alignment.TopStart }) { Tabs({ barPosition: BarPosition.Start }) { TabContent() { Column() .width('100%') .height('100%') .backgroundColor(Color.Green) .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) } .tabBar(this.tabBuilder(0, 'green')) .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) TabContent() { Column() .width('100%') .height('100%') .backgroundColor(Color.Blue) .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) } .tabBar(this.tabBuilder(1, 'blue')) .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) TabContent() { Column() .width('100%') .height('100%') .backgroundColor(Color.Yellow) .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) } .tabBar(this.tabBuilder(2, 'yellow')) .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) TabContent() { Column() .width('100%') .height('100%') .backgroundColor(Color.Pink) .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) } .tabBar(this.tabBuilder(3, 'pink')) .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) } .onAreaChange((_oldValue: Area, newValue: Area) => { this.tabsWidth = newValue.width as number; }) .barWidth('100%') .barHeight(56) .width('100%') .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) .backgroundColor('#F1F3F5') .animationDuration(this.animationDuration) .onChange((index: number) => { this.currentIndex = index; // 监å¬ç´¢å¼•indexçš„å˜åŒ–,实现页ç¾å†…容的切æ¢ã€‚ }) .onAnimationStart((_index: number, targetIndex: number) => { // 切æ¢åŠ¨ç”»å¼€å§‹æ—¶è§¦å‘该回调。下划线跟ç€é¡µé¢ä¸€èµ·æ»‘动,åŒæ—¶å®½åº¦æ¸å˜ã€‚ this.currentIndex = targetIndex; this.startAnimateTo(this.animationDuration, this.textInfos[targetIndex][0], this.textInfos[targetIndex][1]); }) .onAnimationEnd((index: number, event: TabsAnimationEvent) => { // 切æ¢åŠ¨ç”»ç»“æŸæ—¶è§¦å‘该回调。下划线动画åœæ¢ã€‚ let currentIndicatorInfo = this.getCurrentIndicatorInfo(index, event); this.startAnimateTo(0, currentIndicatorInfo.left, currentIndicatorInfo.width); }) .onGestureSwipe((index: number, event: TabsAnimationEvent) => { // 在页é¢è·Ÿæ‰‹æ»‘动过程ä¸ï¼Œé€å¸§è§¦å‘该回调。 let currentIndicatorInfo = this.getCurrentIndicatorInfo(index, event); this.currentIndex = currentIndicatorInfo.index; this.indicatorLeftMargin = currentIndicatorInfo.left; this.indicatorWidth = currentIndicatorInfo.width; }) Column() .height(2) .borderRadius(1) .width(this.indicatorWidth) .margin({ left: this.indicatorLeftMargin, top: 48 }) .backgroundColor($r('sys.color.brand')) } .width('100%') } ``` TabsAnimationEvent方法如下所示。 ```ts private getCurrentIndicatorInfo(index: number, event: TabsAnimationEvent): Record<string, number> { let nextIndex = index; if (index > 0 && event.currentOffset > 0) { nextIndex--; } else if (index < 3 && event.currentOffset < 0) { nextIndex++; } let indexInfo = this.textInfos[index]; let nextIndexInfo = this.textInfos[nextIndex]; let swipeRatio = Math.abs(event.currentOffset / this.tabsWidth); let currentIndex = swipeRatio > 0.5 ? nextIndex : index; // 页é¢æ»‘动超过一åŠï¼ŒtabBar切æ¢åˆ°ä¸‹ä¸€é¡µã€‚ let currentLeft = indexInfo[0] + (nextIndexInfo[0] - indexInfo[0]) * swipeRatio; let currentWidth = indexInfo[1] + (nextIndexInfo[1] - indexInfo[1]) * swipeRatio; return { 'index': currentIndex, 'left': currentLeft, 'width': currentWidth }; } private startAnimateTo(duration: number, leftMargin: number, width: number) { this.isStartAnimateTo = true; animateTo({ duration: duration, // 动画时长 curve: Curve.Linear, // 动画曲线 iterations: 1, // æ’放次数 playMode: PlayMode.Normal, // åŠ¨ç”»æ¨¡å¼ onFinish: () => { this.isStartAnimateTo = false; console.info('play end'); } }, () => { this.indicatorLeftMargin = leftMargin; this.indicatorWidth = width; }) } ``` è¿è¡Œæ•ˆæžœå¦‚下图所示。  ### 刷新过程ä¸ï¼ŒForEach键值生æˆå‡½æ•°æœªè®¾ç½®å¯¼è‡´çš„é—ªå±é—®é¢˜ **问题现象** 下拉刷新时,应用产生å¡é¡¿ï¼Œå‡ºçŽ°é—ªå±é—®é¢˜ã€‚  ```ts @Builder private getListView() { List({ space: 12, scroller: this.scroller }) { // ä½¿ç”¨æ‡’åŠ è½½ç»„ä»¶æ¸²æŸ“æ•°æ® ForEach(this.newsData, (item: NewsData) => { ListItem() { newsItem({ newsTitle: item.newsTitle, newsContent: item.newsContent, newsTime: item.newsTime, img: item.img }) } .backgroundColor(Color.White) .borderRadius(16) }); } .width('100%') .height('100%') .padding({ left: 16, right: 16 }) .backgroundColor('#F1F3F5') // å¿…é¡»è®¾ç½®åˆ—è¡¨ä¸ºæ»‘åŠ¨åˆ°è¾¹ç¼˜æ— æ•ˆæžœï¼Œå¦åˆ™æ— 法触å‘pullToRefresh组件的上滑下拉方法。 .edgeEffect(EdgeEffect.None) } ``` **å¯èƒ½åŽŸå› ** ForEachæ供了一个å为keyGeneratorçš„å‚数,这是一个函数,开å‘者å¯ä»¥é€šè¿‡å®ƒè‡ªå®šä¹‰é”®å€¼çš„生æˆè§„则。如果开å‘者没有定义keyGenerator函数,则ArkUI框架会使用默认的键值生æˆå‡½æ•°ï¼Œå³(item: Object, index: number) => { return index + '__' + JSON.stringify(item); }。å¯å‚考[键值生æˆè§„则](../quick-start/arkts-rendering-control-foreach.md#键值生æˆè§„则)。 在使用ForEach的过程ä¸ï¼Œè‹¥å¯¹äºŽé”®å€¼ç”Ÿæˆè§„则的ç†è§£ä¸å¤Ÿå……分,å¯èƒ½ä¼šå‡ºçŽ°é”™è¯¯çš„使用方å¼ã€‚错误使用一方é¢ä¼šå¯¼è‡´åŠŸèƒ½å±‚é¢é—®é¢˜ï¼Œä¾‹å¦‚渲染结果éžé¢„期,å¦ä¸€æ–¹é¢ä¼šå¯¼è‡´æ€§èƒ½å±‚é¢é—®é¢˜ï¼Œä¾‹å¦‚渲染性能é™ä½Žã€‚ **解决措施** 在ForEach第三个å‚æ•°ä¸å®šä¹‰è‡ªå®šä¹‰é”®å€¼çš„生æˆè§„则,å³(item: NewsData, index?: number) => item.idï¼Œè¿™æ ·å¯ä»¥åœ¨æ¸²æŸ“æ—¶é™ä½Žé‡å¤ç»„件的渲染开销,从而消除闪å±é—®é¢˜ã€‚å¯å‚考[ForEach组件使用建议](../quick-start/arkts-rendering-control-foreach.md#使用建议)。 ```ts @Builder private getListView() { List({ space: 12, scroller: this.scroller }) { // ä½¿ç”¨æ‡’åŠ è½½ç»„ä»¶æ¸²æŸ“æ•°æ® ForEach(this.newsData, (item: NewsData) => { ListItem() { newsItem({ newsTitle: item.newsTitle, newsContent: item.newsContent, newsTime: item.newsTime, img: item.img }) } .backgroundColor(Color.White) .borderRadius(16) }, (item: NewsData) => item.newsId); } .width('100%') .height('100%') .padding({ left: 16, right: 16 }) .backgroundColor('#F1F3F5') // å¿…é¡»è®¾ç½®åˆ—è¡¨ä¸ºæ»‘åŠ¨åˆ°è¾¹ç¼˜æ— æ•ˆæžœï¼Œå¦åˆ™æ— 法触å‘pullToRefresh组件的上滑下拉方法。 .edgeEffect(EdgeEffect.None) } ``` è¿è¡Œæ•ˆæžœå¦‚下图所示。  ## 总结 当出现应用闪å±ç›¸å…³é—®é¢˜æ—¶ï¼Œé¦–先定ä½å¯èƒ½å‡ºçŽ°çš„åŽŸå› ï¼Œåˆ†åˆ«æµ‹è¯•æ˜¯å¦ä¸ºå½“å‰åŽŸå› 导致。定ä½åˆ°é—®é¢˜åŽå°è¯•ä½¿ç”¨å¯¹åº”解决方案,从而消除对应问题现象。 - 应用连ç»ç‚¹å‡»åœºæ™¯ä¸‹ï¼Œé€šè¿‡è®¡æ•°å™¨ä¼˜åŒ–动画逻辑。 - Tabs页ç¾åˆ‡æ¢åœºæ™¯ä¸‹ï¼Œå®Œå–„动画细粒度,æ高æµç•…表现。 - ForEach刷新内容过程ä¸ï¼Œæ ¹æ®ä¸šåŠ¡åœºæ™¯è°ƒæ•´é”®å€¼ç”Ÿæˆå‡½æ•°ã€‚