DEV Community

HarmonyOS
HarmonyOS

Posted on

How to solve the problem of page flickering when sliding Swiper to router route jump

Read the original article:How to solve the problem of page flickering when sliding Swiper to router route jump

Context

When implementing a Swiper component with custom animations in HarmonyOS, developers may encounter a screen flashing issue during page transitions. Specifically, when monitoring the onAnimationStart callback to detect when the user swipes to the last page and triggering router.back() to return to the previous page, the transition works smoothly without custom animations. However, after adding custom content transitions (such as opacity changes, scaling, and translation effects), the page flashes during the router navigation, creating a poor user experience.

Description

The Swiper component is a sliding view container that provides the ability to display child components in a carousel manner with swipe interactions.

Key Callback Methods:

  • onAnimationStart: Triggered when the switch animation begins. After this callback is invoked, the switching animation logic executes on the render thread, allowing the idle main thread to load resources required by child components and reduce preloading time for nodes within the cachedCount range.
  • onAnimationEnd: Triggered when the switch animation completes.

The Problem:

When using customContentTransition to create sophisticated animations (opacity fading, scaling, translation) and calling router.back() in the onAnimationStart callback upon reaching the last page, a visible flash occurs during the page transition. This happens because the animation has just started but hasn't completed, creating a conflict between the ongoing custom animation and the router navigation rendering.

Problem Code Structure:

Index.ets:

@Entry
@Component
struct Index {
  build() {
    Column() {
      Button('Click to Navigate').onClick(() => {
        this.getUIContext().getRouter().pushUrl({ url: 'pages/SecondPage' })
      })
    }.width('100%').height('100%')
  }
}
Enter fullscreen mode Exit fullscreen mode

SecondPage.ets (Problem Code):

import { Router, UIContext } from '@kit.ArkUI';

let uiContext: UIContext = new UIContext();
let router: Router = uiContext.getRouter();

@Entry
@Component
struct SecondPage {
  private DISPLAY_COUNT: number = 1
  private MIN_SCALE: number = 0.75
  @State backgroundColors: string[] =
    ['#ffd2bf82', '#ff7db5db', '#ff95b784', '#ff867aa7', '#ffae8080', '#ffa98b6a', '#ffa9a9a9']
  @State opacityList: number[] = []
  @State scaleList: number[] = []
  @State translateList: number[] = []
  @State zIndexList: number[] = []

  aboutToAppear(): void {
    for (let i = 0; i < this.backgroundColors.length; i++) {
      this.opacityList.push(1.0)
      this.scaleList.push(1.0)
      this.translateList.push(0.0)
      this.zIndexList.push(0)
    }
  }

  build() {
    Column() {
      Swiper() {
        ForEach(this.backgroundColors, (backgroundColor: Color, index: number) => {
          Text(index.toString())
            .width('100%')
            .height('100%')
            .fontSize(50)
            .textAlign(TextAlign.Center)
            .backgroundColor(backgroundColor) // Custom animation properties: opacity, scale, translation, z-index
            .opacity(this.opacityList[index])
            .scale({ x: this.scaleList[index], y: this.scaleList[index] })
            .translate({ x: this.translateList[index] })
            .zIndex(this.zIndexList[index])
        })
      }
      .height(300)
      .indicator(false)
      .displayCount(this.DISPLAY_COUNT, true)
      // Problem code: router.back() called during animation start
      .onAnimationStart((index: number, targetIndex: number) => {
        // Return when target page is the last page
        if (targetIndex === this.backgroundColors.length - 1) {
          router.back()
        }
      })
      .customContentTransition({
        // Remove page from render tree after 1000ms timeout when leaving viewport
        timeout: 1000,
        // Frame-by-frame callback for all pages in viewport, modifying opacity, scale, translate, zIndex for custom animations
        transition: (proxy: SwiperContentTransitionProxy) => {
          if (proxy.position <= proxy.index % this.DISPLAY_COUNT ||
            proxy.position >= this.DISPLAY_COUNT + proxy.index % this.DISPLAY_COUNT) {
            // Reset property values when pages in same group slide left or completely out of viewport
            this.opacityList[proxy.index] = 1.0
            this.scaleList[proxy.index] = 1.0
            this.translateList[proxy.index] = 0.0
            this.zIndexList[proxy.index] = 0
          } else {
            // When pages in same group slide right and haven't left viewport, modify properties frame-by-frame based on position
            if (proxy.index % this.DISPLAY_COUNT === 0) {
              this.opacityList[proxy.index] = 1 - proxy.position / this.DISPLAY_COUNT
              this.scaleList[proxy.index] =
                this.MIN_SCALE + (1 - this.MIN_SCALE) * (1 - proxy.position / this.DISPLAY_COUNT)
              this.translateList[proxy.index] =
                -proxy.position * proxy.mainAxisLength + (1 - this.scaleList[proxy.index]) * proxy.mainAxisLength / 2.0
            } else {
              this.opacityList[proxy.index] = 1 - (proxy.position - 1) / this.DISPLAY_COUNT
              this.scaleList[proxy.index] =
                this.MIN_SCALE + (1 - this.MIN_SCALE) * (1 - (proxy.position - 1) / this.DISPLAY_COUNT)
              this.translateList[proxy.index] = -(proxy.position - 1) * proxy.mainAxisLength -
                (1 - this.scaleList[proxy.index]) * proxy.mainAxisLength / 2.0
            }
            this.zIndexList[proxy.index] = -1
          }
        }
      })
      .onContentDidScroll((selectedIndex: number, index: number, position: number, mainAxisLength: number) => {
        // Monitor Swiper page scrolling events for custom navigation dot animations, etc.
        console.info('onContentDidScroll selectedIndex: ' + selectedIndex + ', index: ' + index + ', position: ' +
          position + ', mainAxisLength: ' + mainAxisLength)
      })
    }.width('100%')
  }

  pageTransition() {
    PageTransitionEnter({ duration: 500 }).opacity(1)
    PageTransitionExit({ duration: 400 }).opacity(0)
  }
}
Enter fullscreen mode Exit fullscreen mode

The screen flashes when reaching the last page due to the conflict between the starting animation and the router navigation.

Solution

Testing reveals that executing router.back() in onAnimationStart conflicts with the custom transition effects, causing the rendering flash. Currently, there's no way to resolve this timing conflict directly. The workaround is to use onAnimationEnd instead of onAnimationStart to execute router.back(), allowing the animation to complete before navigation occurs.

Key Change:

Replace the onAnimationStart method in SecondPage.ets with:

.onAnimationEnd((index: number, extraInfo: SwiperAnimationEvent) => {
  if (index === this.backgroundColors.length - 1) {
    router.back()
  }
})
Enter fullscreen mode Exit fullscreen mode

Complete Fixed Code:

import { Router, UIContext } from '@kit.ArkUI';

let uiContext: UIContext = new UIContext();
let router: Router = uiContext.getRouter();

@Entry
@Component
struct SecondPage {
  private DISPLAY_COUNT: number = 1
  private MIN_SCALE: number = 0.75
  @State backgroundColors: string[] =
    ['#ffd2bf82', '#ff7db5db', '#ff95b784', '#ff867aa7', '#ffae8080', '#ffa98b6a', '#ffa9a9a9']
  @State opacityList: number[] = []
  @State scaleList: number[] = []
  @State translateList: number[] = []
  @State zIndexList: number[] = []

  aboutToAppear(): void {
    for (let i = 0; i < this.backgroundColors.length; i++) {
      this.opacityList.push(1.0)
      this.scaleList.push(1.0)
      this.translateList.push(0.0)
      this.zIndexList.push(0)
    }
  }

  build() {
    Column() {
      Swiper() {
        ForEach(this.backgroundColors, (backgroundColor: Color, index: number) => {
          Text(index.toString())
            .width('100%')
            .height('100%')
            .fontSize(50)
            .textAlign(TextAlign.Center)
            .backgroundColor(backgroundColor) // Custom animation properties: opacity, scale, translation, z-index
            .opacity(this.opacityList[index])
            .scale({ x: this.scaleList[index], y: this.scaleList[index] })
            .translate({ x: this.translateList[index] })
            .zIndex(this.zIndexList[index])
        })
      }
      .height(300)
      .indicator(false)
      .displayCount(this.DISPLAY_COUNT, true)
      // Fixed code: Use onAnimationEnd instead of onAnimationStart
      .onAnimationEnd((index: number, extraInfo: SwiperAnimationEvent) => {
        if (index === this.backgroundColors.length - 1) {
          router.back()
        }
      })
      .customContentTransition({
        // Remove page from render tree after 1000ms timeout when leaving viewport
        timeout: 1000,
        // Frame-by-frame callback for all pages in viewport, modifying opacity, scale, translate, zIndex for custom animations
        transition: (proxy: SwiperContentTransitionProxy) => {
          if (proxy.position <= proxy.index % this.DISPLAY_COUNT ||
            proxy.position >= this.DISPLAY_COUNT + proxy.index % this.DISPLAY_COUNT) {
            // Reset property values when pages in same group slide left or completely out of viewport
            this.opacityList[proxy.index] = 1.0
            this.scaleList[proxy.index] = 1.0
            this.translateList[proxy.index] = 0.0
            this.zIndexList[proxy.index] = 0
          } else {
            // When pages in same group slide right and haven't left viewport, modify properties frame-by-frame based on position
            if (proxy.index % this.DISPLAY_COUNT === 0) {
              this.opacityList[proxy.index] = 1 - proxy.position / this.DISPLAY_COUNT
              this.scaleList[proxy.index] =
                this.MIN_SCALE + (1 - this.MIN_SCALE) * (1 - proxy.position / this.DISPLAY_COUNT)
              this.translateList[proxy.index] =
                -proxy.position * proxy.mainAxisLength + (1 - this.scaleList[proxy.index]) * proxy.mainAxisLength / 2.0
            } else {
              this.opacityList[proxy.index] = 1 - (proxy.position - 1) / this.DISPLAY_COUNT
              this.scaleList[proxy.index] =
                this.MIN_SCALE + (1 - this.MIN_SCALE) * (1 - (proxy.position - 1) / this.DISPLAY_COUNT)
              this.translateList[proxy.index] = -(proxy.position - 1) * proxy.mainAxisLength -
                (1 - this.scaleList[proxy.index]) * proxy.mainAxisLength / 2.0
            }
            this.zIndexList[proxy.index] = -1
          }
        }
      })
      .onContentDidScroll((selectedIndex: number, index: number, position: number, mainAxisLength: number) => {
        // Monitor Swiper page scrolling events for custom navigation dot animations, etc.
        console.info('onContentDidScroll selectedIndex: ' + selectedIndex + ', index: ' + index + ', position: ' +
          position + ', mainAxisLength: ' + mainAxisLength)
      })
    }.width('100%')
  }

  pageTransition() {
    PageTransitionEnter({ duration: 500 }).opacity(1)
    PageTransitionExit({ duration: 400 }).opacity(0)
  }
}
Enter fullscreen mode Exit fullscreen mode

After this change, the navigation occurs only after the switch animation completes, resulting in smooth transitions without flashing.

Key Takeaways

  • Timing is Critical: When using customContentTransition with Swiper, triggering router navigation during onAnimationStart creates rendering conflicts that cause screen flashing
  • Use onAnimationEnd for Navigation: Always use onAnimationEnd instead of onAnimationStart when you need to trigger router navigation or other major UI changes after a swipe animation completes
  • Animation Lifecycle Understanding: onAnimationStart executes on the render thread to optimize resource loading, making it unsuitable for operations that change the page stack
  • Custom Transitions Require Completion: Complex custom animations involving opacity, scale, translate, and zIndex need to fully complete before page navigation to avoid visual artifacts
  • Frame-by-Frame Animation Logic: The customContentTransition callback executes frame-by-frame, modifying multiple properties simultaneously to create smooth, custom swipe effects
  • Index vs TargetIndex: In onAnimationStart, use targetIndex to predict the destination; in onAnimationEnd, use index to confirm the final position
  • Render Thread Considerations: Operations in onAnimationStart should focus on resource preparation, not major UI state changes that could conflict with ongoing animations
  • Testing Different Scenarios: Always test navigation triggers both with and without custom animations to identify timing-related rendering issues early in development

Written by Emincan Ozcan

Top comments (0)